GT2/GT2-iOS/node_modules/raven/lib/client.js

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;