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
- [x] Create a new AWS account for Merani project
- [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
- [ ] Obtain Windows Verisign Certificate for Merani
- [ ] Deploy new identity API to id.getmerani.com

View file

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

View file

@ -183,25 +183,6 @@ describe("the `NylasEnv` global", function nylasEnvSpec() {
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", () => {
jasmine.unspy(NylasEnv, "inDevMode")
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.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 RavenErrorReporter = require('./error-logger-extensions/raven-error-reporter');
// A globally available ErrorLogger that can report errors to various
// sources and enhance error functionality.
@ -40,13 +41,19 @@ module.exports = ErrorLogger = (function() {
this.inDevMode = args.inDevMode
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 }
@ -113,9 +120,10 @@ module.exports = ErrorLogger = (function() {
ErrorLogger.prototype._startCrashReporter = function(args) {
crashReporter.start({
productName: 'Nylas Mail',
companyName: 'Nylas',
submitURL: 'https://electron-crash-report-server.herokuapp.com/',
productName: 'Merani',
companyName: 'Merani',
submitURL: 'http://merani_prod.bugsplat.com/post/bp/crash/postBP.php',
uploadToServer: true,
autoSubmit: true,
})
}
@ -144,37 +152,10 @@ module.exports = ErrorLogger = (function() {
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() {
var tmpPath = app.getPath('temp');
@ -203,7 +184,7 @@ module.exports = ErrorLogger = (function() {
var filepath = path.join(tmpPath, file);
fs.stat(filepath, function(err, stats) {
if (!err && stats) {
var lastModified = new Date(stats['mtime']);
var lastModified = new Date(stats.mtime);
var fileAge = Date.now() - lastModified.getTime();
if (fileAge > (1000 * 60 * 60 * 24 * 2)) { // two days
fs.unlink(filepath, () => {});
@ -218,7 +199,7 @@ module.exports = ErrorLogger = (function() {
ErrorLogger.prototype._setupNewLogFile = function() {
// 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.logstream = fs.createWriteStream(this._logPath(), {
@ -260,9 +241,9 @@ module.exports = ErrorLogger = (function() {
var command, args;
command = arguments[0]
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]
extension[command].apply(this, args);
extension[command].apply(extension, args);
}
}
@ -283,14 +264,14 @@ module.exports = ErrorLogger = (function() {
}
ErrorLogger.prototype._appendLog = function(obj) {
if (this.inSpecMode) { return };
if (this.inSpecMode) { return; }
try {
var message = JSON.stringify({
host: this.loghost,
timestamp: (new Date()).toISOString(),
payload: obj
})+"\n";
}) + "\n";
this.logstream.write(message, 'utf8', function (err) {
if (err) {
@ -303,5 +284,4 @@ module.exports = ErrorLogger = (function() {
};
return ErrorLogger;
})();

View file

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

View file

@ -1,15 +1,14 @@
/* eslint global-require: 0 */
/* eslint import/no-dynamic-require: 0 */
import _ from 'underscore';
import fs from 'fs';
import path from 'path';
import { ipcRenderer, remote } from 'electron';
import _ from 'underscore';
import { Emitter } from 'event-kit';
import { convertStackTrace } from 'coffeestack';
import { mapSourcePosition } from 'source-map-support';
import WindowEventHandler from './window-event-handler';
import Utils from './flux/models/utils';
function ensureInteger(f, fallback) {
@ -26,9 +25,7 @@ function ensureInteger(f, fallback) {
export default class NylasEnvConstructor {
static initClass() {
this.version = 1;
this.prototype.workspaceViewParentSelector = 'body';
this.prototype.lastUncaughtError = null;
/*
Section: Properties
@ -56,10 +53,6 @@ export default class NylasEnvConstructor {
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
// Returns an NylasEnv instance, fully initialized
static loadOrCreate() {
@ -139,12 +132,6 @@ export default class NylasEnvConstructor {
// Call .loadOrCreate instead
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;
({version: this.version} = this.savedState);
this.emitter = new Emitter();
@ -201,7 +188,9 @@ export default class NylasEnvConstructor {
}
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
// 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});
};
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)
process.on('uncaughtException', e => {
this.reportError(e);
});
// Based on testing, there are some unhandled rejections that don't get
// caught by `process.on('unhandledRejection')`, so we listen for unhandled
// rejections on the`window` as well
window.addEventListener('unhandledrejection', e => {
process.on('unhandledRejection', (error) => {
this._onUnhandledRejection(error, sourceMapCache);
});
window.addEventListener('unhandledrejection', (e) => {
// This event is supposed to look like {reason, promise}, according to
// https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
// In practice, it can have different shapes, so we try to make our best
// guess
// In practice, it can have different shapes, so we make our best guess
if (!e) {
const error = new Error(`Unknown window.unhandledrejection event.`)
this._onUnhandledRejection(error, sourceMapCache)
@ -295,49 +274,19 @@ export default class NylasEnvConstructor {
return null;
}
// Given that we listen to unhandled rejections on both the `window` and the
// `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) => {
_onUnhandledRejection = (error, sourceMapCache) => {
if (this.inDevMode()) {
error.stack = convertStackTrace(error.stack, sourceMapCache);
}
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;
this.reportError(error);
}
// 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
// `NylasEnv.reportError` will hook into the event callbacks and handle
// test failures and dev tool popups.
// `NylasEnv.reportError` hooks into test failures and dev tool popups.
//
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 {
extra.pluginIds = this._findPluginsFromError(error);
} catch (err) {
@ -355,8 +304,6 @@ export default class NylasEnvConstructor {
}
this.errorLogger.reportError(error, extra);
this.emitter.emit('did-throw-error', event);
}
_findPluginsFromError(error) {
@ -377,38 +324,6 @@ export default class NylasEnvConstructor {
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
// 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
@ -708,10 +623,6 @@ export default class NylasEnvConstructor {
return this.setFullScreen(!this.isFullScreen());
}
getAllWindowDimensions() {
return remote.getGlobal('application').getAllWindowDimensions();
}
// Get the dimensions of this window.
//
// 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.
//
// If you pass a `windowKey` in the options, and that windowKey already
@ -974,8 +879,7 @@ export default class NylasEnvConstructor {
}
exit(status) {
const { app } = remote;
app.emit('will-exit');
remote.app.emit('will-exit');
return remote.process.exit(status);
}
@ -1096,4 +1000,5 @@ export default class NylasEnvConstructor {
this.actionBridge.registerGlobalActions(...args);
}
}
NylasEnvConstructor.initClass();