Clean up error logging and tie to new Merani Sentry account

This commit is contained in:
Ben Gotow 2017-08-10 18:09:05 -07:00
parent d87a602c8b
commit b75aa778c7
6 changed files with 43 additions and 193 deletions

View file

@ -55,7 +55,7 @@ Target Ship Date: Late September
#### Deployment #### Deployment
- [x] Create a new AWS account for Merani project - [x] Create a new AWS account for Merani project
- [x] Register Merani domain(s) - [x] Register Merani domain(s)
- [ ] Setup Sentry for JavaScript error reporting - [x] Setup Sentry for JavaScript error reporting
- [x] Obtain Mac Developer Certificate for Merani - [x] Obtain Mac Developer Certificate for Merani
- [ ] Obtain Windows Verisign Certificate for Merani - [ ] Obtain Windows Verisign Certificate for Merani
- [ ] Deploy new identity API to id.getmerani.com - [ ] Deploy new identity API to id.getmerani.com

View file

@ -60,7 +60,7 @@
"pathwatcher": "~6.2", "pathwatcher": "~6.2",
"pick-react-known-prop": "0.x.x", "pick-react-known-prop": "0.x.x",
"proxyquire": "1.3.1", "proxyquire": "1.3.1",
"raven": "1.1.4", "raven": "2.1.1",
"react": "15.6.1", "react": "15.6.1",
"react-addons-css-transition-group": "15.6.0", "react-addons-css-transition-group": "15.6.0",
"react-addons-perf": "15.6.0-rc.1", "react-addons-perf": "15.6.0-rc.1",

View file

@ -183,25 +183,6 @@ describe("the `NylasEnv` global", function nylasEnvSpec() {
spyOn(console, "error") spyOn(console, "error")
}); });
it("emits will-throw-error", () => {
spyOn(NylasEnv.emitter, "emit")
NylasEnv.reportError(this.testErr);
expect(NylasEnv.emitter.emit).toHaveBeenCalled();
expect(NylasEnv.emitter.emit.callCount).toBe(2);
expect(NylasEnv.emitter.emit.calls[0].args[0]).toBe("will-throw-error")
expect(NylasEnv.emitter.emit.calls[1].args[0]).toBe("did-throw-error")
});
it("returns if the event has its default prevented", () => {
spyOn(NylasEnv.emitter, "emit").andCallFake((name, event) => {
event.preventDefault()
})
NylasEnv.reportError(this.testErr);
expect(NylasEnv.emitter.emit).toHaveBeenCalled();
expect(NylasEnv.emitter.emit.callCount).toBe(1);
expect(NylasEnv.emitter.emit.calls[0].args[0]).toBe("will-throw-error")
});
it("opens dev tools in dev mode", () => { it("opens dev tools in dev mode", () => {
jasmine.unspy(NylasEnv, "inDevMode") jasmine.unspy(NylasEnv, "inDevMode")
spyOn(NylasEnv, "inDevMode").andReturn(true); spyOn(NylasEnv, "inDevMode").andReturn(true);
@ -216,16 +197,6 @@ describe("the `NylasEnv` global", function nylasEnvSpec() {
expect(NylasEnv.errorLogger.reportError.callCount).toBe(1); expect(NylasEnv.errorLogger.reportError.callCount).toBe(1);
expect(NylasEnv.errorLogger.reportError.calls[0].args[0]).toBe(this.testErr); expect(NylasEnv.errorLogger.reportError.calls[0].args[0]).toBe(this.testErr);
}); });
it("emits did-throw-error", () => {
spyOn(NylasEnv.emitter, "emit")
NylasEnv.reportError(this.testErr);
expect(NylasEnv.openDevTools).not.toHaveBeenCalled();
expect(NylasEnv.executeJavaScriptInDevTools).not.toHaveBeenCalled();
expect(NylasEnv.emitter.emit.callCount).toBe(2);
expect(NylasEnv.emitter.emit.calls[0].args[0]).toBe("will-throw-error")
expect(NylasEnv.emitter.emit.calls[1].args[0]).toBe("did-throw-error")
});
}); });
}); });
}); });

View file

@ -16,6 +16,7 @@ if (process.type === 'renderer') {
} }
var crashReporter = require('electron').crashReporter var crashReporter = require('electron').crashReporter
var RavenErrorReporter = require('./error-logger-extensions/raven-error-reporter');
// A globally available ErrorLogger that can report errors to various // A globally available ErrorLogger that can report errors to various
// sources and enhance error functionality. // sources and enhance error functionality.
@ -40,13 +41,19 @@ module.exports = ErrorLogger = (function() {
this.inDevMode = args.inDevMode this.inDevMode = args.inDevMode
this.resourcePath = args.resourcePath this.resourcePath = args.resourcePath
this._startCrashReporter() this._startCrashReporter();
this._extendErrorObject() this._extendErrorObject();
this._extendNativeConsole() this._extendNativeConsole();
this.extensions = this._setupErrorLoggerExtensions(args) this.extensions = [
new RavenErrorReporter({
inSpecMode: args.inSpecMode,
inDevMode: args.inDevMode,
resourcePath: args.resourcePath,
}),
]
if (this.inSpecMode) { return } if (this.inSpecMode) { return }
@ -113,9 +120,10 @@ module.exports = ErrorLogger = (function() {
ErrorLogger.prototype._startCrashReporter = function(args) { ErrorLogger.prototype._startCrashReporter = function(args) {
crashReporter.start({ crashReporter.start({
productName: 'Nylas Mail', productName: 'Merani',
companyName: 'Nylas', companyName: 'Merani',
submitURL: 'https://electron-crash-report-server.herokuapp.com/', submitURL: 'http://merani_prod.bugsplat.com/post/bp/crash/postBP.php',
uploadToServer: true,
autoSubmit: true, autoSubmit: true,
}) })
} }
@ -144,37 +152,10 @@ module.exports = ErrorLogger = (function() {
return alt; return alt;
}, },
configurable: true configurable: true,
}); });
} }
ErrorLogger.prototype._setupErrorLoggerExtensions = function(args) {
var extension, extensionConstructor, extensionPath, extensions, extensionsPath, i, len, ref;
if (args == null) {
args = {};
}
extensions = [];
extensionsPath = path.join(args.resourcePath, 'src', 'error-logger-extensions');
ref = fs.listSync(extensionsPath);
for (i = 0, len = ref.length; i < len; i++) {
extensionPath = ref[i];
if (path.basename(extensionPath)[0] === '.') {
continue;
}
extensionConstructor = require(extensionPath);
if (!(typeof extensionConstructor === "function")) {
throw new Error("Logger Extensions must return an extension constructor");
}
extension = new extensionConstructor({
inSpecMode: args.inSpecMode,
inDevMode: args.inDevMode,
resourcePath: args.resourcePath
});
extensions.push(extension);
}
return extensions;
};
ErrorLogger.prototype._logPath = function() { ErrorLogger.prototype._logPath = function() {
var tmpPath = app.getPath('temp'); var tmpPath = app.getPath('temp');
@ -203,7 +184,7 @@ module.exports = ErrorLogger = (function() {
var filepath = path.join(tmpPath, file); var filepath = path.join(tmpPath, file);
fs.stat(filepath, function(err, stats) { fs.stat(filepath, function(err, stats) {
if (!err && stats) { if (!err && stats) {
var lastModified = new Date(stats['mtime']); var lastModified = new Date(stats.mtime);
var fileAge = Date.now() - lastModified.getTime(); var fileAge = Date.now() - lastModified.getTime();
if (fileAge > (1000 * 60 * 60 * 24 * 2)) { // two days if (fileAge > (1000 * 60 * 60 * 24 * 2)) { // two days
fs.unlink(filepath, () => {}); fs.unlink(filepath, () => {});
@ -218,7 +199,7 @@ module.exports = ErrorLogger = (function() {
ErrorLogger.prototype._setupNewLogFile = function() { ErrorLogger.prototype._setupNewLogFile = function() {
// Open a file write stream to log output from this process // Open a file write stream to log output from this process
console.log("Streaming log data to "+this._logPath()); console.log("Streaming log data to " + this._logPath());
this.loghost = os.hostname(); this.loghost = os.hostname();
this.logstream = fs.createWriteStream(this._logPath(), { this.logstream = fs.createWriteStream(this._logPath(), {
@ -260,9 +241,9 @@ module.exports = ErrorLogger = (function() {
var command, args; var command, args;
command = arguments[0] command = arguments[0]
args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : []; args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
for (var i=0; i < this.extensions.length; i++) { for (var i = 0; i < this.extensions.length; i++) {
const extension = this.extensions[i] const extension = this.extensions[i]
extension[command].apply(this, args); extension[command].apply(extension, args);
} }
} }
@ -283,14 +264,14 @@ module.exports = ErrorLogger = (function() {
} }
ErrorLogger.prototype._appendLog = function(obj) { ErrorLogger.prototype._appendLog = function(obj) {
if (this.inSpecMode) { return }; if (this.inSpecMode) { return; }
try { try {
var message = JSON.stringify({ var message = JSON.stringify({
host: this.loghost, host: this.loghost,
timestamp: (new Date()).toISOString(), timestamp: (new Date()).toISOString(),
payload: obj payload: obj
})+"\n"; }) + "\n";
this.logstream.write(message, 'utf8', function (err) { this.logstream.write(message, 'utf8', function (err) {
if (err) { if (err) {
@ -303,5 +284,4 @@ module.exports = ErrorLogger = (function() {
}; };
return ErrorLogger; return ErrorLogger;
})(); })();

View file

@ -31,13 +31,7 @@ function trimTo(str, size) {
} }
function handleUnrecoverableDatabaseError(err = (new Error(`Manually called handleUnrecoverableDatabaseError`))) { function handleUnrecoverableDatabaseError(err = (new Error(`Manually called handleUnrecoverableDatabaseError`))) {
const fingerprint = ["{{ default }}", "unrecoverable database error", err.message]; NylasEnv.errorLogger.reportError(err);
NylasEnv.errorLogger.reportError(err, {fingerprint,
rateLimit: {
ratePerHour: 30,
key: `handleUnrecoverableDatabaseError:${err.message}`,
},
});
const app = remote.getGlobal('application'); const app = remote.getGlobal('application');
if (!app) { if (!app) {
throw new Error('handleUnrecoverableDatabaseError: `app` is not ready!') throw new Error('handleUnrecoverableDatabaseError: `app` is not ready!')

View file

@ -1,15 +1,14 @@
/* eslint global-require: 0 */ /* eslint global-require: 0 */
/* eslint import/no-dynamic-require: 0 */ /* eslint import/no-dynamic-require: 0 */
import _ from 'underscore';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { ipcRenderer, remote } from 'electron'; import { ipcRenderer, remote } from 'electron';
import _ from 'underscore';
import { Emitter } from 'event-kit'; import { Emitter } from 'event-kit';
import { convertStackTrace } from 'coffeestack'; import { convertStackTrace } from 'coffeestack';
import { mapSourcePosition } from 'source-map-support'; import { mapSourcePosition } from 'source-map-support';
import WindowEventHandler from './window-event-handler'; import WindowEventHandler from './window-event-handler';
import Utils from './flux/models/utils'; import Utils from './flux/models/utils';
function ensureInteger(f, fallback) { function ensureInteger(f, fallback) {
@ -26,9 +25,7 @@ function ensureInteger(f, fallback) {
export default class NylasEnvConstructor { export default class NylasEnvConstructor {
static initClass() { static initClass() {
this.version = 1; this.version = 1;
this.prototype.workspaceViewParentSelector = 'body'; this.prototype.workspaceViewParentSelector = 'body';
this.prototype.lastUncaughtError = null;
/* /*
Section: Properties Section: Properties
@ -56,10 +53,6 @@ export default class NylasEnvConstructor {
this.prototype.styles = null; // Increment this when the serialization format changes this.prototype.styles = null; // Increment this when the serialization format changes
} }
assert(bool, msg) {
if (!bool) { throw new Error(`Assertion error: ${msg}`); }
}
// Load or create the application environment // Load or create the application environment
// Returns an NylasEnv instance, fully initialized // Returns an NylasEnv instance, fully initialized
static loadOrCreate() { static loadOrCreate() {
@ -139,12 +132,6 @@ export default class NylasEnvConstructor {
// Call .loadOrCreate instead // Call .loadOrCreate instead
constructor(savedState = {}) { constructor(savedState = {}) {
this.reportError = this.reportError.bind(this);
this.getConfigDirPath = this.getConfigDirPath.bind(this);
this.storeColumnWidth = this.storeColumnWidth.bind(this);
this.getColumnWidth = this.getColumnWidth.bind(this);
this.startWindow = this.startWindow.bind(this);
this.populateHotWindow = this.populateHotWindow.bind(this);
this.savedState = savedState; this.savedState = savedState;
({version: this.version} = this.savedState); ({version: this.version} = this.savedState);
this.emitter = new Emitter(); this.emitter = new Emitter();
@ -201,7 +188,9 @@ export default class NylasEnvConstructor {
} }
this.windowEventHandler = new WindowEventHandler(); this.windowEventHandler = new WindowEventHandler();
this.extendRxObservables(); // We extend nylas observables with our own methods. This happens on
// require of nylas-observables
require('nylas-observables');
// Nylas exports is designed to provide a lazy-loaded set of globally // Nylas exports is designed to provide a lazy-loaded set of globally
// accessible objects to all packages. Upon require, nylas-exports will // accessible objects to all packages. Upon require, nylas-exports will
@ -247,28 +236,18 @@ export default class NylasEnvConstructor {
return this.reportError(originalError, {url, line: newLine, column: newColumn}); return this.reportError(originalError, {url, line: newLine, column: newColumn});
}; };
process.on('uncaughtException', e => this.reportError(e)); process.on('uncaughtException', e => {
this.reportError(e);
// We use the native Node 'unhandledRejection' instead of Bluebird's
// `Promise.onPossiblyUnhandledRejection`. Testing indicates that
// the Node process method is a strict superset of Bluebird's handler.
// With the introduction of transpiled async/await, it is now possible
// to get a native, non-Bluebird Promise. In that case, Bluebird's
// `onPossiblyUnhandledRejection` gets bypassed and we miss some
// errors. The Node process handler catches all Bluebird promises plus
// those created with a native Promise.
process.on('unhandledRejection', error => {
this._onUnhandledRejection(error, sourceMapCache)
}); });
// Based on testing, there are some unhandled rejections that don't get process.on('unhandledRejection', (error) => {
// caught by `process.on('unhandledRejection')`, so we listen for unhandled this._onUnhandledRejection(error, sourceMapCache);
// rejections on the`window` as well });
window.addEventListener('unhandledrejection', e => {
window.addEventListener('unhandledrejection', (e) => {
// This event is supposed to look like {reason, promise}, according to // This event is supposed to look like {reason, promise}, according to
// https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent // https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
// In practice, it can have different shapes, so we try to make our best // In practice, it can have different shapes, so we make our best guess
// guess
if (!e) { if (!e) {
const error = new Error(`Unknown window.unhandledrejection event.`) const error = new Error(`Unknown window.unhandledrejection event.`)
this._onUnhandledRejection(error, sourceMapCache) this._onUnhandledRejection(error, sourceMapCache)
@ -295,49 +274,19 @@ export default class NylasEnvConstructor {
return null; return null;
} }
// Given that we listen to unhandled rejections on both the `window` and the _onUnhandledRejection = (error, sourceMapCache) => {
// `process`, more often than not both of those will get called almost
// immedaitely with the same error. To prevent double reporting the same
// error, we debounce this function with a very small interval
_onUnhandledRejection = _.debounce((error, sourceMapCache) => {
if (this.inDevMode()) { if (this.inDevMode()) {
error.stack = convertStackTrace(error.stack, sourceMapCache); error.stack = convertStackTrace(error.stack, sourceMapCache);
} }
this.reportError(error, { this.reportError(error);
rateLimit: {
ratePerHour: 30,
key: `UnhandledRejection:${error.stack}`,
},
})
}, 10)
_createErrorCallbackEvent(error, extraArgs = {}) {
const event = Object.assign({}, extraArgs, {
message: error.message,
originalError: error,
defaultPrevented: false,
});
event.preventDefault = () => { event.defaultPrevented = true; return true };
return event;
} }
// Public: report an error through the `ErrorLogger` // Public: report an error through the `ErrorLogger`
// //
// Takes an error and an extra object to report. Hooks into the
// `onWillThrowError` and `onDidThrowError` callbacks. If someone
// registered with `onWillThrowError` calls `preventDefault` on the event
// object it's given, then no error will be reported.
//
// The difference between this and `ErrorLogger.reportError` is that // The difference between this and `ErrorLogger.reportError` is that
// `NylasEnv.reportError` will hook into the event callbacks and handle // `NylasEnv.reportError` hooks into test failures and dev tool popups.
// test failures and dev tool popups. //
reportError(error, extra = {}, {noWindows} = {}) { reportError(error, extra = {}, {noWindows} = {}) {
const event = this._createErrorCallbackEvent(error, extra);
this.emitter.emit('will-throw-error', event);
if (event.defaultPrevented) { return; }
this.lastUncaughtError = error;
try { try {
extra.pluginIds = this._findPluginsFromError(error); extra.pluginIds = this._findPluginsFromError(error);
} catch (err) { } catch (err) {
@ -355,8 +304,6 @@ export default class NylasEnvConstructor {
} }
this.errorLogger.reportError(error, extra); this.errorLogger.reportError(error, extra);
this.emitter.emit('did-throw-error', event);
} }
_findPluginsFromError(error) { _findPluginsFromError(error) {
@ -377,38 +324,6 @@ export default class NylasEnvConstructor {
Section: Event Subscription Section: Event Subscription
*/ */
// Extended: Invoke the given callback when there is an unhandled error, but
// before the devtools pop open
//
// * `callback` {Function} to be called whenever there is an unhandled error
// * `event` {Object}
// * `originalError` {Object} the original error object
// * `message` {String} the original error object
// * `url` {String} Url to the file where the error originated.
// * `line` {Number}
// * `column` {Number}
// * `preventDefault` {Function} call this to avoid popping up the dev tools.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onWillThrowError(callback) {
return this.emitter.on('will-throw-error', callback);
}
// Extended: Invoke the given callback whenever there is an unhandled error.
//
// * `callback` {Function} to be called whenever there is an unhandled error
// * `event` {Object}
// * `originalError` {Object} the original error object
// * `message` {String} the original error object
// * `url` {String} Url to the file where the error originated.
// * `line` {Number}
// * `column` {Number}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidThrowError(callback) {
return this.emitter.on('did-throw-error', callback);
}
// Extended: Run the Chromium content-tracing module for five seconds, and save // Extended: Run the Chromium content-tracing module for five seconds, and save
// the output to a file which is printed to the command-line output of the app. // the output to a file which is printed to the command-line output of the app.
// You can take the file exported by this function and load it into Chrome's // You can take the file exported by this function and load it into Chrome's
@ -708,10 +623,6 @@ export default class NylasEnvConstructor {
return this.setFullScreen(!this.isFullScreen()); return this.setFullScreen(!this.isFullScreen());
} }
getAllWindowDimensions() {
return remote.getGlobal('application').getAllWindowDimensions();
}
// Get the dimensions of this window. // Get the dimensions of this window.
// //
// Returns an {Object} with the following keys: // Returns an {Object} with the following keys:
@ -891,12 +802,6 @@ export default class NylasEnvConstructor {
} }
} }
// We extend nylas observables with our own methods. This happens on
// require of nylas-observables
extendRxObservables() {
return require('nylas-observables');
}
// Launches a new window via the browser/WindowLauncher. // Launches a new window via the browser/WindowLauncher.
// //
// If you pass a `windowKey` in the options, and that windowKey already // If you pass a `windowKey` in the options, and that windowKey already
@ -974,8 +879,7 @@ export default class NylasEnvConstructor {
} }
exit(status) { exit(status) {
const { app } = remote; remote.app.emit('will-exit');
app.emit('will-exit');
return remote.process.exit(status); return remote.process.exit(status);
} }
@ -1096,4 +1000,5 @@ export default class NylasEnvConstructor {
this.actionBridge.registerGlobalActions(...args); this.actionBridge.registerGlobalActions(...args);
} }
} }
NylasEnvConstructor.initClass(); NylasEnvConstructor.initClass();