| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- '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, promise) {
- var context = (promise.domain && promise.domain.sentryContext) || {};
- context.extra = context.extra || {};
- context.extra.unhandledPromiseRejection = true;
- self.captureException(reason, context, function(sendErr, eventId) {
- if (!sendErr) {
- var reasonMessage = (reason && reason.message) || reason;
- utils.consoleAlert(
- 'unhandledRejection captured\n' +
- 'Event ID: ' +
- eventId +
- '\n' +
- 'Reason: ' +
- reasonMessage
- );
- }
- });
- });
- }
- 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) || {};
- var globalContext = this._globalContext || {};
- kwargs.user = extend({}, globalContext.user, domainContext.user, kwargs.user);
- kwargs.tags = extend({}, globalContext.tags, domainContext.tags, kwargs.tags);
- kwargs.extra = extend({}, globalContext.extra, domainContext.extra, kwargs.extra);
- // Perform a shallow copy of breadcrums to not send one that we'll capture below through as well
- kwargs.breadcrumbs = {
- values:
- (domainContext.breadcrumbs && domainContext.breadcrumbs.slice()) ||
- (globalContext.breadcrumbs && globalContext.breadcrumbs.slice()) ||
- []
- };
- /*
- `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
- */
- var request = this._createRequestObject(
- globalContext.request,
- domainContext.request,
- kwargs.request
- );
- delete kwargs.request;
- if (Object.keys(request).length === 0) {
- request = this._createRequestObject(
- globalContext.req,
- domainContext.req,
- kwargs.req
- );
- delete kwargs.req;
- }
- if (Object.keys(request).length > 0) {
- var parseUser = Object.keys(kwargs.user).length === 0 ? this.parseUser : false;
- extend(kwargs, parsers.parseRequest(request, parseUser));
- } else {
- kwargs.request = {};
- }
- 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 && 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);
- }
- // Capture breadcrumb before sending it, as we also want to have it even when
- // it was dropped due to sampleRate or shouldSendCallback
- this.captureBreadcrumb({
- category: 'sentry',
- message: kwargs.message,
- event_id: kwargs.event_id,
- level: kwargs.level || 'error' // presume error unless specified
- });
- 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 = utils.isPlainObject(kwargs) ? extend({}, kwargs) : {};
- }
- var eventId = this.generateEventId();
- if (this.stacktrace) {
- var ex = new Error(message);
- console.log(ex);
- utils.parseStack(
- ex,
- function(frames) {
- // We trim last frame, as it's our `new Error(message)` statement 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 = utils.isPlainObject(kwargs) ? extend({}, kwargs) : {};
- }
- if (!utils.isError(err)) {
- 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 message =
- 'Non-Error exception captured with keys: ' +
- utils.serializeKeysForMessage(keys);
- kwargs = extend(kwargs, {
- message: message,
- fingerprint: [md5(keys)],
- extra: kwargs.extra || {}
- });
- kwargs.extra.__serialized__ = utils.serializeException(err);
- 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 (utils.isError(err)) {
- 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 ravenRequestMiddleware(req, res, next) {
- self.context({req: req}, function() {
- domain.active.add(req);
- domain.active.add(res);
- next();
- });
- };
- },
- errorHandler: function() {
- var self = this;
- return function ravenErrorMiddleware(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;
|