mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-11 10:12:00 +08:00
918090a4e1
Summary:
The goal is to let us see what plugins are throwing errors on Sentry.
We are using a Sentry `tag` to identify and group plugins and their
errors.
Along the way, I cleaned up the error catching and reporting system. There
was a lot of duplicate error logic (that wasn't always right) and some
legacy Atom error handling.
Now, if you catch an error that we should report (like when handling
extensions), call `NylasEnv.reportError`. This used to be called
`emitError` but I changed it to `reportError` to be consistent with the
ErrorReporter and be a bit more indicative of what it does.
In the production version, the `ErrorLogger` will forward the request to
the `nylas-private-error-reporter` which will report to Sentry.
The `reportError` function also now inspects the stack to determine which
plugin(s) it came from. These are passed along to Sentry.
I also cleaned up the `console.log` and `console.error` code. We were
logging errors multiple times making the console confusing to read. Worse
is that we were logging the `error` object, which would print not the
stack of the actual error, but rather the stack of where the console.error
was logged from. Printing `error.stack` instead shows much more accurate
stack traces.
See changes in the Edgehill repo here: 8c4a86eb7e
Test Plan: Manual
Reviewers: juan, bengotow
Reviewed By: bengotow
Differential Revision: https://phab.nylas.com/D2509
211 lines
6.7 KiB
JavaScript
211 lines
6.7 KiB
JavaScript
import {
|
|
Task,
|
|
NylasAPI,
|
|
APIError,
|
|
Metadata,
|
|
DatabaseStore,
|
|
SyncbackModelTask,
|
|
DatabaseTransaction } from 'nylas-exports'
|
|
|
|
class TestTask extends SyncbackModelTask {
|
|
getModelConstructor() {
|
|
return Metadata
|
|
}
|
|
}
|
|
|
|
describe("SyncbackModelTask", () => {
|
|
beforeEach(() => {
|
|
this.testModel = new Metadata({accountId: 'account-123'})
|
|
spyOn(DatabaseTransaction.prototype, "persistModel")
|
|
spyOn(DatabaseStore, "findBy")
|
|
.andReturn(Promise.resolve(this.testModel));
|
|
|
|
spyOn(NylasEnv, "reportError")
|
|
spyOn(NylasAPI, "makeRequest").andReturn(Promise.resolve({
|
|
version: 10,
|
|
id: "server-123",
|
|
}))
|
|
});
|
|
|
|
const performRemote = (fn) => {
|
|
window.waitsForPromise(() => {
|
|
return this.task.performRemote().then(fn)
|
|
});
|
|
}
|
|
|
|
describe("performLocal", () => {
|
|
it("throws if basic fields are missing", () => {
|
|
const t = new SyncbackModelTask()
|
|
try {
|
|
t.performLocal()
|
|
throw new Error("Shouldn't succeed");
|
|
} catch (e) {
|
|
expect(e.message).toMatch(/^Must pass.*/)
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("performRemote", () => {
|
|
beforeEach(() => {
|
|
this.task = new TestTask({
|
|
clientId: "local-123",
|
|
endpoint: "/test",
|
|
})
|
|
});
|
|
|
|
it("fetches the latest model", () => {
|
|
spyOn(this.task, "getLatestModel").andCallThrough()
|
|
spyOn(this.task, "verifyModel").andCallThrough()
|
|
performRemote(() => {
|
|
expect(this.task.getLatestModel).toHaveBeenCalled()
|
|
const model = this.task.verifyModel.calls[0].args[0]
|
|
expect(model).toBe(this.testModel)
|
|
})
|
|
});
|
|
|
|
it("throws an error if getLatestModel hasn't been implemented", () => {
|
|
const bumTask = new SyncbackModelTask({clientId: 'local-123'});
|
|
spyOn(this.task, "getModelConstructor").andCallThrough()
|
|
window.waitsForPromise(() => {
|
|
return bumTask.performRemote().then((err) => {
|
|
expect(err[0]).toBe(Task.Status.Failed)
|
|
expect(err[1].message).toMatch(/must subclass/)
|
|
})
|
|
});
|
|
});
|
|
|
|
it("verifies the model", () => {
|
|
spyOn(this.task, "verifyModel").andCallThrough()
|
|
spyOn(this.task, "makeRequest").andCallThrough()
|
|
performRemote(() => {
|
|
expect(this.task.verifyModel).toHaveBeenCalled()
|
|
const model = this.task.makeRequest.calls[0].args[0]
|
|
expect(model).toBe(this.testModel)
|
|
})
|
|
});
|
|
|
|
it("gets the correct path and method for existing objects", () => {
|
|
jasmine.unspy(DatabaseStore, "findBy")
|
|
const serverModel = new Metadata({localId: 'local-123', serverId: 'server-123'})
|
|
|
|
spyOn(DatabaseStore, "findBy").andReturn(Promise.resolve(serverModel));
|
|
|
|
spyOn(this.task, "getPathAndMethod").andCallThrough();
|
|
|
|
performRemote(() => {
|
|
expect(this.task.getPathAndMethod).toHaveBeenCalled()
|
|
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
|
expect(opts.path).toBe("/test/server-123")
|
|
expect(opts.method).toBe("PUT")
|
|
});
|
|
});
|
|
|
|
it("gets the correct path and method for new objects", () => {
|
|
spyOn(this.task, "getPathAndMethod").andCallThrough();
|
|
|
|
performRemote(() => {
|
|
expect(this.task.getPathAndMethod).toHaveBeenCalled()
|
|
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
|
expect(opts.path).toBe("/test")
|
|
expect(opts.method).toBe("POST")
|
|
});
|
|
});
|
|
|
|
it("lets tasks override path and method", () => {
|
|
class TaskMethodAndPath extends SyncbackModelTask {
|
|
getModelConstructor() {
|
|
return Metadata
|
|
}
|
|
getPathAndMethod = () => {
|
|
return {
|
|
path: `/override`,
|
|
method: "DELETE",
|
|
}
|
|
}
|
|
}
|
|
const task = new TaskMethodAndPath({clientId: 'local-123'});
|
|
spyOn(task, "getPathAndMethod").andCallThrough();
|
|
spyOn(task, "getModelConstructor").andCallThrough()
|
|
window.waitsForPromise(() => {
|
|
return task.performRemote().then(() => {
|
|
expect(task.getPathAndMethod).toHaveBeenCalled()
|
|
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
|
expect(opts.path).toBe("/override")
|
|
expect(opts.method).toBe("DELETE")
|
|
})
|
|
});
|
|
});
|
|
|
|
it("makes a request with the correct data", () => {
|
|
spyOn(this.task, "makeRequest").andCallThrough();
|
|
|
|
// So it doesn't get changed by the time we inspect it
|
|
spyOn(this.task, "updateLocalModel").andReturn(Promise.resolve())
|
|
|
|
performRemote(() => {
|
|
expect(this.task.makeRequest).toHaveBeenCalled()
|
|
const opts = NylasAPI.makeRequest.calls[0].args[0]
|
|
expect(opts.path).toBe("/test")
|
|
expect(opts.method).toBe("POST")
|
|
expect(opts.accountId).toBe("account-123")
|
|
expect(opts.returnsModel).toBe(false)
|
|
expect(opts.body).toEqual(this.testModel.toJSON())
|
|
});
|
|
});
|
|
|
|
it("updates the local model with only the version and serverId", () => {
|
|
spyOn(this.task, "updateLocalModel").andCallThrough()
|
|
performRemote(() => {
|
|
expect(this.task.updateLocalModel).toHaveBeenCalled();
|
|
const opts = this.task.updateLocalModel.calls[0].args[0]
|
|
expect(opts.version).toBe(10)
|
|
expect(opts.id).toBe("server-123")
|
|
expect(DatabaseTransaction.prototype.persistModel).toHaveBeenCalled()
|
|
const model = DatabaseTransaction.prototype.persistModel.calls[0].args[0]
|
|
expect(model.serverId).toBe('server-123')
|
|
expect(model.version).toBe(10)
|
|
});
|
|
});
|
|
|
|
it("retries on retry-able API errors", () => {
|
|
jasmine.unspy(NylasAPI, "makeRequest");
|
|
const err = new APIError({statusCode: 429});
|
|
spyOn(NylasAPI, "makeRequest").andReturn(Promise.reject(err))
|
|
performRemote((status) => {
|
|
expect(status).toBe(Task.Status.Retry)
|
|
});
|
|
});
|
|
|
|
it("failes on permanent errors", () => {
|
|
jasmine.unspy(NylasAPI, "makeRequest");
|
|
const err = new APIError({statusCode: 500});
|
|
spyOn(NylasAPI, "makeRequest").andReturn(Promise.reject(err))
|
|
performRemote((status) => {
|
|
expect(status[0]).toBe(Task.Status.Failed)
|
|
expect(status[1].statusCode).toBe(500)
|
|
});
|
|
});
|
|
|
|
it("fails and notifies us on other types of errors", () => {
|
|
const errMsg = "This is a test error"
|
|
spyOn(this.task, "updateLocalModel").andCallFake(() => {
|
|
throw new Error(errMsg)
|
|
})
|
|
performRemote((status) => {
|
|
expect(status[0]).toBe(Task.Status.Failed)
|
|
expect(status[1].message).toBe(errMsg)
|
|
expect(NylasEnv.reportError).toHaveBeenCalled()
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("undo/redo", () => {
|
|
it("cant be undone", () => {
|
|
expect(this.task.canBeUndone()).toBe(false)
|
|
});
|
|
|
|
it("isn't an undo task", () => {
|
|
expect(this.task.isUndo()).toBe(false)
|
|
});
|
|
});
|
|
});
|