652 lines
21 KiB
JavaScript
652 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
var stringify = require('../vendor/json-stringify-safe');
|
|
var parsers = require('./parsers');
|
|
var zlib = require('zlib');
|
|
var utils = require('./utils');
|
|
var uuid = require('uuid');
|
|
var transports = require('./transports');
|
|
var nodeUtil = require('util'); // nodeUtil to avoid confusion with "utils"
|
|
var events = require('events');
|
|
var domain = require('domain');
|
|
var md5 = require('md5');
|
|
|
|
var instrumentor = require('./instrumentation/instrumentor');
|
|
|
|
var extend = utils.extend;
|
|
|
|
function Raven() {
|
|
this.breadcrumbs = {
|
|
record: this.captureBreadcrumb.bind(this)
|
|
};
|
|
}
|
|
|
|
nodeUtil.inherits(Raven, events.EventEmitter);
|
|
|
|
extend(Raven.prototype, {
|
|
config: function config(dsn, options) {
|
|
// We get lots of users using raven-node when they want raven-js, hence this warning if it seems like a browser
|
|
if (
|
|
typeof window !== 'undefined' &&
|
|
typeof document !== 'undefined' &&
|
|
typeof navigator !== 'undefined'
|
|
) {
|
|
utils.consoleAlertOnce(
|
|
"This looks like a browser environment; are you sure you don't want Raven.js for browser JavaScript? https://sentry.io/for/javascript"
|
|
);
|
|
}
|
|
|
|
if (arguments.length === 0) {
|
|
// no arguments, use default from environment
|
|
dsn = global.process.env.SENTRY_DSN;
|
|
options = {};
|
|
}
|
|
if (typeof dsn === 'object') {
|
|
// They must only be passing through options
|
|
options = dsn;
|
|
dsn = global.process.env.SENTRY_DSN;
|
|
}
|
|
options = options || {};
|
|
|
|
this.raw_dsn = dsn;
|
|
this.dsn = utils.parseDSN(dsn);
|
|
this.name =
|
|
options.name || global.process.env.SENTRY_NAME || require('os').hostname();
|
|
this.root = options.root || global.process.cwd();
|
|
this.transport = options.transport || transports[this.dsn.protocol];
|
|
this.sendTimeout = options.sendTimeout || 1;
|
|
this.release = options.release || global.process.env.SENTRY_RELEASE;
|
|
this.environment =
|
|
options.environment ||
|
|
global.process.env.SENTRY_ENVIRONMENT ||
|
|
global.process.env.NODE_ENV;
|
|
|
|
// autoBreadcrumbs: true enables all, autoBreadcrumbs: false disables all
|
|
// autoBreadcrumbs: { http: true } enables a single type
|
|
this.autoBreadcrumbs = options.autoBreadcrumbs || false;
|
|
// default to 30, don't allow higher than 100
|
|
this.maxBreadcrumbs = Math.max(0, Math.min(options.maxBreadcrumbs || 30, 100));
|
|
|
|
this.captureUnhandledRejections = options.captureUnhandledRejections;
|
|
this.loggerName = options.logger;
|
|
this.dataCallback = options.dataCallback;
|
|
this.shouldSendCallback = options.shouldSendCallback;
|
|
this.sampleRate = typeof options.sampleRate === 'undefined' ? 1 : options.sampleRate;
|
|
this.maxReqQueueCount = options.maxReqQueueCount || 100;
|
|
this.parseUser = options.parseUser;
|
|
this.stacktrace = options.stacktrace || false;
|
|
|
|
if (!this.dsn) {
|
|
utils.consoleAlert('no DSN provided, error reporting disabled');
|
|
}
|
|
|
|
if (this.dsn.protocol === 'https') {
|
|
// In case we want to provide our own SSL certificates / keys
|
|
this.ca = options.ca || null;
|
|
}
|
|
|
|
// enabled if a dsn is set
|
|
this._enabled = !!this.dsn;
|
|
|
|
var globalContext = (this._globalContext = {});
|
|
if (options.tags) {
|
|
globalContext.tags = options.tags;
|
|
}
|
|
if (options.extra) {
|
|
globalContext.extra = options.extra;
|
|
}
|
|
|
|
this.onFatalError = this.defaultOnFatalError = function(err, sendErr, eventId) {
|
|
console.error(err && err.stack ? err.stack : err);
|
|
global.process.exit(1);
|
|
};
|
|
this.uncaughtErrorHandler = this.makeErrorHandler();
|
|
|
|
this.on('error', function(err) {
|
|
utils.consoleAlert('failed to send exception to sentry: ' + err.message);
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
install: function install(cb) {
|
|
if (this.installed) return this;
|
|
|
|
if (typeof cb === 'function') {
|
|
this.onFatalError = cb;
|
|
}
|
|
|
|
global.process.on('uncaughtException', this.uncaughtErrorHandler);
|
|
|
|
if (this.captureUnhandledRejections) {
|
|
var self = this;
|
|
global.process.on('unhandledRejection', function(reason) {
|
|
self.captureException(reason, function(sendErr, eventId) {
|
|
if (!sendErr) utils.consoleAlert('unhandledRejection captured: ' + eventId);
|
|
});
|
|
});
|
|
}
|
|
|
|
instrumentor.instrument(this, this.autoBreadcrumbs);
|
|
|
|
this.installed = true;
|
|
|
|
return this;
|
|
},
|
|
|
|
uninstall: function uninstall() {
|
|
if (!this.installed) return this;
|
|
|
|
instrumentor.deinstrument(this);
|
|
|
|
// todo: this works for tests for now, but isn't what we ultimately want to be doing
|
|
global.process.removeAllListeners('uncaughtException');
|
|
global.process.removeAllListeners('unhandledRejection');
|
|
|
|
this.installed = false;
|
|
|
|
return this;
|
|
},
|
|
|
|
makeErrorHandler: function() {
|
|
var self = this;
|
|
var caughtFirstError = false;
|
|
var caughtSecondError = false;
|
|
var calledFatalError = false;
|
|
var firstError;
|
|
return function(err) {
|
|
if (!caughtFirstError) {
|
|
// this is the first uncaught error and the ultimate reason for shutting down
|
|
// we want to do absolutely everything possible to ensure it gets captured
|
|
// also we want to make sure we don't go recursion crazy if more errors happen after this one
|
|
firstError = err;
|
|
caughtFirstError = true;
|
|
self.captureException(err, {level: 'fatal'}, function(sendErr, eventId) {
|
|
if (!calledFatalError) {
|
|
calledFatalError = true;
|
|
self.onFatalError(err, sendErr, eventId);
|
|
}
|
|
});
|
|
} else if (calledFatalError) {
|
|
// we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down
|
|
utils.consoleAlert(
|
|
'uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown'
|
|
);
|
|
self.defaultOnFatalError(err);
|
|
} else if (!caughtSecondError) {
|
|
// two cases for how we can hit this branch:
|
|
// - capturing of first error blew up and we just caught the exception from that
|
|
// - quit trying to capture, proceed with shutdown
|
|
// - a second independent error happened while waiting for first error to capture
|
|
// - want to avoid causing premature shutdown before first error capture finishes
|
|
// it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff
|
|
// so let's instead just delay a bit before we proceed with our action here
|
|
// in case 1, we just wait a bit unnecessarily but ultimately do the same thing
|
|
// in case 2, the delay hopefully made us wait long enough for the capture to finish
|
|
// two potential nonideal outcomes:
|
|
// nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError
|
|
// nonideal case 2: case 2 happens, 1st error is captured but slowly, timeout completes before capture and we treat second error as the sendErr of (nonexistent) failure from trying to capture first error
|
|
// note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError)
|
|
// we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish
|
|
caughtSecondError = true;
|
|
setTimeout(function() {
|
|
if (!calledFatalError) {
|
|
// it was probably case 1, let's treat err as the sendErr and call onFatalError
|
|
calledFatalError = true;
|
|
self.onFatalError(firstError, err);
|
|
} else {
|
|
// it was probably case 2, our first error finished capturing while we waited, cool, do nothing
|
|
}
|
|
}, (self.sendTimeout + 1) * 1000); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc
|
|
}
|
|
};
|
|
},
|
|
|
|
generateEventId: function generateEventId() {
|
|
return uuid().replace(/-/g, '');
|
|
},
|
|
|
|
process: function process(eventId, kwargs, cb) {
|
|
// prod codepaths shouldn't hit this branch, for testing
|
|
if (typeof eventId === 'object') {
|
|
cb = kwargs;
|
|
kwargs = eventId;
|
|
eventId = this.generateEventId();
|
|
}
|
|
|
|
var domainContext = (domain.active && domain.active.sentryContext) || {};
|
|
kwargs.user = extend({}, this._globalContext.user, domainContext.user, kwargs.user);
|
|
kwargs.tags = extend({}, this._globalContext.tags, domainContext.tags, kwargs.tags);
|
|
kwargs.extra = extend(
|
|
{},
|
|
this._globalContext.extra,
|
|
domainContext.extra,
|
|
kwargs.extra
|
|
);
|
|
kwargs.breadcrumbs = {
|
|
values: domainContext.breadcrumbs || this._globalContext.breadcrumbs || []
|
|
};
|
|
|
|
/*
|
|
`request` is our specified property name for the http interface: https://docs.sentry.io/clientdev/interfaces/http/
|
|
`req` is the conventional name for a request object in node/express/etc
|
|
we want to enable someone to pass a `request` property to kwargs according to http interface
|
|
but also want to provide convenience for passing a req object and having us parse it out
|
|
so we only parse a `req` property if the `request` property is absent/empty (and hence we won't clobber)
|
|
parseUser returns a partial kwargs object with a `request` property and possibly a `user` property
|
|
*/
|
|
kwargs.request = this._createRequestObject(
|
|
this._globalContext.request,
|
|
domainContext.request,
|
|
kwargs.request
|
|
);
|
|
if (Object.keys(kwargs.request).length === 0) {
|
|
var req = this._createRequestObject(
|
|
this._globalContext.req,
|
|
domainContext.req,
|
|
kwargs.req
|
|
);
|
|
if (Object.keys(req).length > 0) {
|
|
var parseUser = Object.keys(kwargs.user).length === 0 ? this.parseUser : false;
|
|
extend(kwargs, parsers.parseRequest(req, parseUser));
|
|
delete kwargs.req;
|
|
}
|
|
}
|
|
|
|
kwargs.modules = utils.getModules();
|
|
kwargs.server_name = kwargs.server_name || this.name;
|
|
|
|
if (typeof global.process.version !== 'undefined') {
|
|
kwargs.extra.node = global.process.version;
|
|
}
|
|
|
|
kwargs.environment = kwargs.environment || this.environment;
|
|
kwargs.logger = kwargs.logger || this.loggerName;
|
|
kwargs.event_id = eventId;
|
|
kwargs.timestamp = new Date().toISOString().split('.')[0];
|
|
kwargs.project = this.dsn.project_id;
|
|
kwargs.platform = 'node';
|
|
kwargs.release = this.release;
|
|
|
|
// Cleanup empty properties before sending them to the server
|
|
Object.keys(kwargs).forEach(function(key) {
|
|
if (kwargs[key] == null || kwargs[key] === '') {
|
|
delete kwargs[key];
|
|
}
|
|
});
|
|
|
|
if (this.dataCallback) {
|
|
kwargs = this.dataCallback(kwargs);
|
|
}
|
|
|
|
var shouldSend = true;
|
|
if (!this._enabled) shouldSend = false;
|
|
if (this.shouldSendCallback && !this.shouldSendCallback(kwargs)) shouldSend = false;
|
|
if (Math.random() >= this.sampleRate) shouldSend = false;
|
|
|
|
if (shouldSend) {
|
|
this.send(kwargs, cb);
|
|
} else {
|
|
// wish there was a good way to communicate to cb why we didn't send; worth considering cb api change?
|
|
// could be shouldSendCallback, could be disabled, could be sample rate
|
|
// avoiding setImmediate here because node 0.8
|
|
cb &&
|
|
setTimeout(function() {
|
|
cb(null, eventId);
|
|
}, 0);
|
|
}
|
|
},
|
|
|
|
send: function send(kwargs, cb) {
|
|
var self = this;
|
|
var skwargs = stringify(kwargs);
|
|
var eventId = kwargs.event_id;
|
|
|
|
zlib.deflate(skwargs, function(err, buff) {
|
|
var message = buff.toString('base64'),
|
|
timestamp = new Date().getTime(),
|
|
headers = {
|
|
'X-Sentry-Auth': utils.getAuthHeader(
|
|
timestamp,
|
|
self.dsn.public_key,
|
|
self.dsn.private_key
|
|
),
|
|
'Content-Type': 'application/octet-stream',
|
|
'Content-Length': message.length
|
|
};
|
|
|
|
self.transport.send(self, message, headers, eventId, cb);
|
|
});
|
|
},
|
|
|
|
captureMessage: function captureMessage(message, kwargs, cb) {
|
|
if (!cb && typeof kwargs === 'function') {
|
|
cb = kwargs;
|
|
kwargs = {};
|
|
} else {
|
|
kwargs = kwargs || {};
|
|
}
|
|
|
|
var eventId = this.generateEventId();
|
|
|
|
if (this.stacktrace) {
|
|
var ex;
|
|
// Generate a "synthetic" stack trace
|
|
try {
|
|
throw new Error(message);
|
|
} catch (ex1) {
|
|
ex = ex1;
|
|
}
|
|
|
|
utils.parseStack(
|
|
ex,
|
|
function(frames) {
|
|
// We trim last frame, as it's our `throw new Error(message)` call itself, which is redundant
|
|
kwargs.stacktrace = {
|
|
frames: frames.slice(0, -1)
|
|
};
|
|
this.process(eventId, parsers.parseText(message, kwargs), cb);
|
|
}.bind(this)
|
|
);
|
|
} else {
|
|
this.process(eventId, parsers.parseText(message, kwargs), cb);
|
|
}
|
|
|
|
return eventId;
|
|
},
|
|
|
|
captureException: function captureException(err, kwargs, cb) {
|
|
if (!cb && typeof kwargs === 'function') {
|
|
cb = kwargs;
|
|
kwargs = {};
|
|
} else {
|
|
kwargs = kwargs || {};
|
|
}
|
|
|
|
if (!(err instanceof Error)) {
|
|
if (utils.isPlainObject(err)) {
|
|
// This will allow us to group events based on top-level keys
|
|
// which is much better than creating new group when any key/value change
|
|
var keys = Object.keys(err).sort();
|
|
var hash = md5(keys);
|
|
var message =
|
|
'Non-Error exception captured with keys: ' +
|
|
utils.serializeKeysForMessage(keys);
|
|
var serializedException = utils.serializeException(err);
|
|
|
|
kwargs.message = message;
|
|
kwargs.fingerprint = [hash];
|
|
kwargs.extra = {
|
|
__serialized__: serializedException
|
|
};
|
|
|
|
err = new Error(message);
|
|
} else {
|
|
// This handles when someone does:
|
|
// throw "something awesome";
|
|
// We synthesize an Error here so we can extract a (rough) stack trace.
|
|
err = new Error(err);
|
|
}
|
|
}
|
|
|
|
var self = this;
|
|
var eventId = this.generateEventId();
|
|
parsers.parseError(err, kwargs, function(kw) {
|
|
self.process(eventId, kw, cb);
|
|
});
|
|
|
|
return eventId;
|
|
},
|
|
|
|
context: function(ctx, func) {
|
|
if (!func && typeof ctx === 'function') {
|
|
func = ctx;
|
|
ctx = {};
|
|
}
|
|
|
|
// todo/note: raven-js takes an args param to do apply(this, args)
|
|
// i don't think it's correct/necessary to bind this to the wrap call
|
|
// and i don't know if we need to support the args param; it's undocumented
|
|
return this.wrap(ctx, func).apply(null);
|
|
},
|
|
|
|
wrap: function(options, func) {
|
|
if (!this.installed) {
|
|
utils.consoleAlertOnce(
|
|
'Raven has not been installed, therefore no breadcrumbs will be captured. Call `Raven.config(...).install()` to fix this.'
|
|
);
|
|
}
|
|
if (!func && typeof options === 'function') {
|
|
func = options;
|
|
options = {};
|
|
}
|
|
|
|
var wrapDomain = domain.create();
|
|
// todo: better property name than sentryContext, maybe __raven__ or sth?
|
|
wrapDomain.sentryContext = options;
|
|
|
|
wrapDomain.on('error', this.uncaughtErrorHandler);
|
|
var wrapped = wrapDomain.bind(func);
|
|
|
|
for (var property in func) {
|
|
if ({}.hasOwnProperty.call(func, property)) {
|
|
wrapped[property] = func[property];
|
|
}
|
|
}
|
|
wrapped.prototype = func.prototype;
|
|
wrapped.__raven__ = true;
|
|
wrapped.__inner__ = func;
|
|
// note: domain.bind sets wrapped.domain, but it's not documented, unsure if we should rely on that
|
|
wrapped.__domain__ = wrapDomain;
|
|
|
|
return wrapped;
|
|
},
|
|
|
|
interceptErr: function(options, func) {
|
|
if (!func && typeof options === 'function') {
|
|
func = options;
|
|
options = {};
|
|
}
|
|
var self = this;
|
|
var wrapped = function() {
|
|
var err = arguments[0];
|
|
if (err instanceof Error) {
|
|
self.captureException(err, options);
|
|
} else {
|
|
func.apply(null, arguments);
|
|
}
|
|
};
|
|
|
|
// repetitive with wrap
|
|
for (var property in func) {
|
|
if ({}.hasOwnProperty.call(func, property)) {
|
|
wrapped[property] = func[property];
|
|
}
|
|
}
|
|
wrapped.prototype = func.prototype;
|
|
wrapped.__raven__ = true;
|
|
wrapped.__inner__ = func;
|
|
|
|
return wrapped;
|
|
},
|
|
|
|
setContext: function setContext(ctx) {
|
|
if (domain.active) {
|
|
domain.active.sentryContext = ctx;
|
|
} else {
|
|
this._globalContext = ctx;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
mergeContext: function mergeContext(ctx) {
|
|
extend(this.getContext(), ctx);
|
|
return this;
|
|
},
|
|
|
|
getContext: function getContext() {
|
|
if (domain.active) {
|
|
if (!domain.active.sentryContext) {
|
|
domain.active.sentryContext = {};
|
|
utils.consoleAlert('sentry context not found on active domain');
|
|
}
|
|
return domain.active.sentryContext;
|
|
}
|
|
return this._globalContext;
|
|
},
|
|
|
|
setCallbackHelper: function(propertyName, callback) {
|
|
var original = this[propertyName];
|
|
if (typeof callback === 'function') {
|
|
this[propertyName] = function(data) {
|
|
return callback(data, original);
|
|
};
|
|
} else {
|
|
this[propertyName] = callback;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/*
|
|
* Set the dataCallback option
|
|
*
|
|
* @param {function} callback The callback to run which allows the
|
|
* data blob to be mutated before sending
|
|
* @return {Raven}
|
|
*/
|
|
setDataCallback: function(callback) {
|
|
return this.setCallbackHelper('dataCallback', callback);
|
|
},
|
|
|
|
/*
|
|
* Set the shouldSendCallback option
|
|
*
|
|
* @param {function} callback The callback to run which allows
|
|
* introspecting the blob before sending
|
|
* @return {Raven}
|
|
*/
|
|
setShouldSendCallback: function(callback) {
|
|
return this.setCallbackHelper('shouldSendCallback', callback);
|
|
},
|
|
|
|
requestHandler: function() {
|
|
var self = this;
|
|
return function ravenMiddleware(req, res, next) {
|
|
self.context({req: req}, function() {
|
|
domain.active.add(req);
|
|
domain.active.add(res);
|
|
next();
|
|
});
|
|
};
|
|
},
|
|
|
|
errorHandler: function() {
|
|
var self = this;
|
|
return function(err, req, res, next) {
|
|
var status =
|
|
err.status ||
|
|
err.statusCode ||
|
|
err.status_code ||
|
|
(err.output && err.output.statusCode) ||
|
|
500;
|
|
|
|
// skip anything not marked as an internal server error
|
|
if (status < 500) return next(err);
|
|
|
|
var eventId = self.captureException(err, {req: req});
|
|
res.sentry = eventId;
|
|
return next(err);
|
|
};
|
|
},
|
|
|
|
captureBreadcrumb: function(breadcrumb) {
|
|
// Avoid capturing global-scoped breadcrumbs before instrumentation finishes
|
|
if (!this.installed) return;
|
|
|
|
breadcrumb = extend(
|
|
{
|
|
timestamp: +new Date() / 1000
|
|
},
|
|
breadcrumb
|
|
);
|
|
var currCtx = this.getContext();
|
|
if (!currCtx.breadcrumbs) currCtx.breadcrumbs = [];
|
|
currCtx.breadcrumbs.push(breadcrumb);
|
|
if (currCtx.breadcrumbs.length > this.maxBreadcrumbs) {
|
|
currCtx.breadcrumbs.shift();
|
|
}
|
|
this.setContext(currCtx);
|
|
},
|
|
|
|
_createRequestObject: function() {
|
|
/**
|
|
* When using proxy, some of the attributes of req/request objects are non-enumerable.
|
|
* To make sure, that they are still available to us after we consolidate our sources
|
|
* (eg. globalContext.request + domainContext.request + kwargs.request),
|
|
* we manually pull them out from original objects.
|
|
*
|
|
* Same scenario happens when some frameworks (eg. Koa) decide to use request within
|
|
* request. eg `this.request.req`, which adds aliases to the main `request` object.
|
|
* By manually reassigning them here, we don't need to add additional checks
|
|
* like `req.method || (req.req && req.req.method)`
|
|
*
|
|
* We don't use Object.assign/extend as it's only merging over objects own properties,
|
|
* and we don't want to go through all of the properties as well, as we simply don't
|
|
* need all of them.
|
|
**/
|
|
var sources = Array.from(arguments).filter(function(source) {
|
|
return Object.prototype.toString.call(source) === '[object Object]';
|
|
});
|
|
sources = [{}].concat(sources);
|
|
var request = extend.apply(null, sources);
|
|
var nonEnumerables = [
|
|
'headers',
|
|
'hostname',
|
|
'ip',
|
|
'method',
|
|
'protocol',
|
|
'query',
|
|
'secure',
|
|
'url'
|
|
];
|
|
|
|
nonEnumerables.forEach(function(key) {
|
|
sources.forEach(function(source) {
|
|
if (source[key]) request[key] = source[key];
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Check for 'host' *only* after we checked for 'hostname' first.
|
|
* This way we can avoid the noise coming from Express deprecation warning
|
|
* https://github.com/expressjs/express/blob/b97faff6e2aa4d34d79485fe4331cb0eec13ad57/lib/request.js#L450-L452
|
|
* REF: https://github.com/getsentry/raven-node/issues/96#issuecomment-354748884
|
|
**/
|
|
if (!request.hasOwnProperty('hostname')) {
|
|
sources.forEach(function(source) {
|
|
if (source.host) request.host = source.host;
|
|
});
|
|
}
|
|
|
|
return request;
|
|
}
|
|
});
|
|
|
|
// Maintain old API compat, need to make sure arguments length is preserved
|
|
function Client(dsn, options) {
|
|
if (dsn instanceof Client) return dsn;
|
|
var ravenInstance = new Raven();
|
|
return ravenInstance.config.apply(ravenInstance, arguments);
|
|
}
|
|
nodeUtil.inherits(Client, Raven);
|
|
|
|
// Singleton-by-default but not strictly enforced
|
|
// todo these extra export props are sort of an adhoc mess, better way to manage?
|
|
var defaultInstance = new Raven();
|
|
defaultInstance.Client = Client;
|
|
defaultInstance.version = require('../package.json').version;
|
|
defaultInstance.disableConsoleAlerts = utils.disableConsoleAlerts;
|
|
|
|
module.exports = defaultInstance;
|