Mailspring/src/error-reporter.js

290 lines
8.2 KiB
JavaScript

// This file cannot be Coffeescript because it loads before the Coffeescript
// interpreter. Note that it runs in both browser and renderer processes.
var ErrorReporter, raven, _, fs, path, app, os, remote;
raven = require('raven');
os = require('os');
_ = require('underscore');
fs = require('fs-plus');
path = require('path');
if (process.type === 'renderer') {
remote = require('remote');
app = remote.require('app');
} else {
app = require('app');
}
var tmpPath = app.getPath('temp');
var logpid = process.pid;
if (process.type === 'renderer') {
logpid = remote.process.pid + "." + process.pid;
}
var logpath = path.join(tmpPath, 'Nylas-N1-' + logpid + '.log');
// globally define Error.toJSON. This allows us to pass errors via IPC
// and through the Action Bridge. Note:they are not re-inflated into
// Error objects automatically.
Object.defineProperty(Error.prototype, 'toJSON', {
value: function () {
var alt = {};
Object.getOwnPropertyNames(this).forEach(function (key) {
alt[key] = this[key];
}, this);
return alt;
},
configurable: true
});
module.exports = ErrorReporter = (function() {
function ErrorReporter(modes) {
var self = this;
this.inSpecMode = modes.inSpecMode
this.inDevMode = modes.inDevMode
if (!this.inSpecMode) {
this._setupSentry();
this._cleanOldLogFiles();
this._setupNewLogFile();
this._hookProcessOutputs();
this._catchUncaughtErrors();
}
console.debug = _.bind(this.consoleDebug, this);
}
ErrorReporter.prototype._setupSentry = function() {
// Initialize the Sentry connector
this.client = new raven.Client('https://7a32cb0189ff4595a55c98ffb7939c46:f791c3c402b343068bed056b8b504dd5@sentry.nylas.com/4');
this.client.on('error', function(e) {
console.log(e.reason);
console.log(e.statusCode);
return console.log(e.response);
});
}
// If we're the browser process, remove log files that are more than
// two days old. These log files get pretty big because we're logging
// so verbosely.
ErrorReporter.prototype._cleanOldLogFiles = function() {
if (process.type === 'browser') {
fs.readdir(tmpPath, function(err, files) {
if (err) {
console.error(err);
return;
}
var logFilter = new RegExp("Nylas-N1-[.0-9]*.log$");
files.forEach(function(file) {
if (logFilter.test(file) === true) {
var filepath = path.join(tmpPath, file);
fs.stat(filepath, function(err, stats) {
var lastModified = new Date(stats['mtime']);
var fileAge = Date.now() - lastModified.getTime();
if (fileAge > (1000 * 60 * 60 * 24 * 2)) { // two days
fs.unlink(filepath);
}
});
}
});
});
}
}
ErrorReporter.prototype._setupNewLogFile = function() {
this.shipLogsQueued = false;
this.shipLogsTime = 0;
// Open a file write stream to log output from this process
console.log("Streaming log data to "+logpath);
this.loghost = os.hostname();
this.logstream = fs.createWriteStream(logpath, {
flags: 'a',
encoding: 'utf8',
fd: null,
mode: 0666
});
}
ErrorReporter.prototype._hookProcessOutputs = function() {
var self = this;
// Override stdout and stderr to pipe their output to the file
// in addition to calling through to the existing implementation
function hook_process_output(channel, callback) {
var old_write = process[channel].write;
process[channel].write = (function(write) {
return function(string, encoding, fd) {
write.apply(process[channel], arguments)
callback(string, encoding, fd)
}
})(process[channel].write)
// Return a function that can be used to undo this change
return function() {
process[channel].write = old_write
};
}
hook_process_output('stdout', function(string, encoding, fd) {
self.appendLog.apply(self, [string]);
});
hook_process_output('stderr', function(string, encoding, fd) {
self.appendLog.apply(self, [string]);
});
}
ErrorReporter.prototype._catchUncaughtErrors = function() {
var self = this;
// Link to the appropriate error handlers for the browser
// or renderer process
if (process.type === 'renderer') {
atom.onDidThrowError(function(_arg) {
return self.reportError(_arg.originalError, {
'message': _arg.message
});
});
} else if (process.type === 'browser') {
var nslog = require('nslog');
console.log = nslog;
process.on('uncaughtException', function(error) {
if (error == null) {
error = {};
}
self.reportError(error);
if (error.message != null) {
nslog(error.message);
}
if (error.stack != null) {
return nslog(error.stack);
}
});
}
}
// Create a new console.debug option, which takes `true` (print)
// or `false`, don't print in console as the first parameter.
// This makes it easy for developers to turn on and off
// "verbose console" mode.
ErrorReporter.prototype.consoleDebug = function() {
var args = [];
var showIt = arguments[0];
for (var ii = 1; ii < arguments.length; ii++) {
args.push(arguments[ii]);
}
if ((this.inDevMode === true) && (showIt === true)) {
console.log.apply(console, args);
}
this.appendLog.apply(this, [args]);
}
ErrorReporter.prototype.appendLog = function(obj) {
if (this.inSpecMode) { return };
try {
var message = JSON.stringify({
host: this.loghost,
timestamp: (new Date()).toISOString(),
payload: obj
})+"\n";
this.logstream.write(message, 'utf8', function (err) {
if (err) {
console.error("ErrorReporter: Unable to write to the log stream!" + err.toString());
}
});
} catch (err) {
console.error("ErrorReporter: Unable to write to the log stream." + err.toString());
}
};
ErrorReporter.prototype.openLogs = function() {
var shell = require('shell');
shell.openItem(logpath);
};
ErrorReporter.prototype.shipLogs = function(reason) {
if (this.inSpecMode) { return };
if (!this.shipLogsQueued) {
var timeSinceLogShip = Date.now() - this.shipLogsTime;
if (timeSinceLogShip > 20000) {
this.runShipLogsTask(reason);
} else {
this.shipLogsQueued = true;
var self = this;
setTimeout(function() {
self.runShipLogsTask(reason);
self.shipLogsQueued = false;
}, 20000 - timeSinceLogShip);
}
}
};
ErrorReporter.prototype.runShipLogsTask = function(reason) {
if (this.inSpecMode) { return };
var self = this;
this.shipLogsTime = Date.now();
if (!reason) {
reason = "";
}
var logPattern = null;
if (process.type === 'renderer') {
logPattern = "Nylas-N1-"+remote.process.pid+"[.0-9]*.log$";
} else {
logPattern = "Nylas-N1-"+process.pid+"[.0-9]*.log$";
}
console.log("ErrorReporter: Shipping Logs. " + reason);
Task = require('./task');
ship = Task.once(fs.absolute('./tasks/ship-logs-task'), tmpPath, logPattern, function() {
self.appendLog("ErrorReporter: Shipped Logs.");
});
};
ErrorReporter.prototype.getVersion = function() {
var _ref;
return (typeof atom !== "undefined" && atom !== null ? atom.getVersion() : void 0) ||
((_ref = require('app')) != null ? _ref.getVersion() : void 0);
};
ErrorReporter.prototype.reportError = function(err, metadata) {
if (this.inSpecMode || this.inDevMode) { return };
// Never send user auth tokens
if (err.requestOptions && err.requestOptions.auth) {
delete err.requestOptions['auth'];
}
// Never send message bodies
if (err.requestOptions && err.requestOptions.body && err.requestOptions.body.body) {
delete err.requestOptions.body['body'];
}
this.client.captureError(err, {
extra: metadata,
tags: {
'platform': process.platform,
'version': this.getVersion()
}
});
this.appendLog(err, metadata);
this.shipLogs('Exception occurred');
};
return ErrorReporter;
})();