UNPKG

22.8 kBJavaScriptView Raw
1'use strict';
2
3var stringify = require('../vendor/json-stringify-safe');
4var parsers = require('./parsers');
5var zlib = require('zlib');
6var utils = require('./utils');
7var uuid = require('uuid');
8var transports = require('./transports');
9var nodeUtil = require('util'); // nodeUtil to avoid confusion with "utils"
10var events = require('events');
11var domain = require('domain');
12var md5 = require('md5');
13
14var instrumentor = require('./instrumentation/instrumentor');
15
16var extend = utils.extend;
17
18function Raven() {
19 this.breadcrumbs = {
20 record: this.captureBreadcrumb.bind(this)
21 };
22}
23
24nodeUtil.inherits(Raven, events.EventEmitter);
25
26extend(Raven.prototype, {
27 config: function config(dsn, options) {
28 // We get lots of users using raven-node when they want raven-js, hence this warning if it seems like a browser
29 if (
30 typeof window !== 'undefined' &&
31 typeof document !== 'undefined' &&
32 typeof navigator !== 'undefined'
33 ) {
34 utils.consoleAlertOnce(
35 "This looks like a browser environment; are you sure you don't want Raven.js for browser JavaScript? https://sentry.io/for/javascript"
36 );
37 }
38
39 if (arguments.length === 0) {
40 // no arguments, use default from environment
41 dsn = global.process.env.SENTRY_DSN;
42 options = {};
43 }
44 if (typeof dsn === 'object') {
45 // They must only be passing through options
46 options = dsn;
47 dsn = global.process.env.SENTRY_DSN;
48 }
49 options = options || {};
50
51 this.raw_dsn = dsn;
52 this.dsn = utils.parseDSN(dsn);
53 this.name =
54 options.name || global.process.env.SENTRY_NAME || require('os').hostname();
55 this.root = options.root || global.process.cwd();
56 this.transport = options.transport || transports[this.dsn.protocol];
57 this.sendTimeout = options.sendTimeout || 1;
58 this.release = options.release || global.process.env.SENTRY_RELEASE;
59 this.environment =
60 options.environment ||
61 global.process.env.SENTRY_ENVIRONMENT ||
62 global.process.env.NODE_ENV;
63
64 // autoBreadcrumbs: true enables all, autoBreadcrumbs: false disables all
65 // autoBreadcrumbs: { http: true } enables a single type
66 this.autoBreadcrumbs = options.autoBreadcrumbs || false;
67 // default to 30, don't allow higher than 100
68 this.maxBreadcrumbs = Math.max(0, Math.min(options.maxBreadcrumbs || 30, 100));
69
70 this.captureUnhandledRejections = options.captureUnhandledRejections;
71 this.loggerName = options.logger;
72 this.dataCallback = options.dataCallback;
73 this.shouldSendCallback = options.shouldSendCallback;
74 this.sampleRate = typeof options.sampleRate === 'undefined' ? 1 : options.sampleRate;
75 this.maxReqQueueCount = options.maxReqQueueCount || 100;
76 this.parseUser = options.parseUser;
77 this.stacktrace = options.stacktrace || false;
78
79 if (!this.dsn) {
80 utils.consoleAlert('no DSN provided, error reporting disabled');
81 }
82
83 if (this.dsn.protocol === 'https') {
84 // In case we want to provide our own SSL certificates / keys
85 this.ca = options.ca || null;
86 }
87
88 // enabled if a dsn is set
89 this._enabled = !!this.dsn;
90
91 var globalContext = (this._globalContext = {});
92 if (options.tags) {
93 globalContext.tags = options.tags;
94 }
95 if (options.extra) {
96 globalContext.extra = options.extra;
97 }
98
99 this.onFatalError = this.defaultOnFatalError = function(err, sendErr, eventId) {
100 console.error(err && err.stack ? err.stack : err);
101 global.process.exit(1);
102 };
103 this.uncaughtErrorHandler = this.makeErrorHandler();
104
105 this.on('error', function(err) {
106 utils.consoleAlert('failed to send exception to sentry: ' + err.message);
107 });
108
109 return this;
110 },
111
112 install: function install(cb) {
113 if (this.installed) return this;
114
115 if (typeof cb === 'function') {
116 this.onFatalError = cb;
117 }
118
119 global.process.on('uncaughtException', this.uncaughtErrorHandler);
120
121 if (this.captureUnhandledRejections) {
122 var self = this;
123 global.process.on('unhandledRejection', function(reason, promise) {
124 var context = (promise.domain && promise.domain.sentryContext) || {};
125 context.extra = context.extra || {};
126 context.extra.unhandledPromiseRejection = true;
127 self.captureException(reason, context, function(sendErr, eventId) {
128 if (!sendErr) {
129 var reasonMessage = (reason && reason.message) || reason;
130 utils.consoleAlert(
131 'unhandledRejection captured\n' +
132 'Event ID: ' +
133 eventId +
134 '\n' +
135 'Reason: ' +
136 reasonMessage
137 );
138 }
139 });
140 });
141 }
142
143 instrumentor.instrument(this, this.autoBreadcrumbs);
144
145 this.installed = true;
146
147 return this;
148 },
149
150 uninstall: function uninstall() {
151 if (!this.installed) return this;
152
153 instrumentor.deinstrument(this);
154
155 // todo: this works for tests for now, but isn't what we ultimately want to be doing
156 global.process.removeAllListeners('uncaughtException');
157 global.process.removeAllListeners('unhandledRejection');
158
159 this.installed = false;
160
161 return this;
162 },
163
164 makeErrorHandler: function() {
165 var self = this;
166 var caughtFirstError = false;
167 var caughtSecondError = false;
168 var calledFatalError = false;
169 var firstError;
170 return function(err) {
171 if (!caughtFirstError) {
172 // this is the first uncaught error and the ultimate reason for shutting down
173 // we want to do absolutely everything possible to ensure it gets captured
174 // also we want to make sure we don't go recursion crazy if more errors happen after this one
175 firstError = err;
176 caughtFirstError = true;
177 self.captureException(err, {level: 'fatal'}, function(sendErr, eventId) {
178 if (!calledFatalError) {
179 calledFatalError = true;
180 self.onFatalError(err, sendErr, eventId);
181 }
182 });
183 } else if (calledFatalError) {
184 // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down
185 utils.consoleAlert(
186 'uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown'
187 );
188 self.defaultOnFatalError(err);
189 } else if (!caughtSecondError) {
190 // two cases for how we can hit this branch:
191 // - capturing of first error blew up and we just caught the exception from that
192 // - quit trying to capture, proceed with shutdown
193 // - a second independent error happened while waiting for first error to capture
194 // - want to avoid causing premature shutdown before first error capture finishes
195 // it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff
196 // so let's instead just delay a bit before we proceed with our action here
197 // in case 1, we just wait a bit unnecessarily but ultimately do the same thing
198 // in case 2, the delay hopefully made us wait long enough for the capture to finish
199 // two potential nonideal outcomes:
200 // nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError
201 // 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
202 // note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError)
203 // we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish
204 caughtSecondError = true;
205 setTimeout(function() {
206 if (!calledFatalError) {
207 // it was probably case 1, let's treat err as the sendErr and call onFatalError
208 calledFatalError = true;
209 self.onFatalError(firstError, err);
210 } else {
211 // it was probably case 2, our first error finished capturing while we waited, cool, do nothing
212 }
213 }, (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
214 }
215 };
216 },
217
218 generateEventId: function generateEventId() {
219 return uuid().replace(/-/g, '');
220 },
221
222 process: function process(eventId, kwargs, cb) {
223 // prod codepaths shouldn't hit this branch, for testing
224 if (typeof eventId === 'object') {
225 cb = kwargs;
226 kwargs = eventId;
227 eventId = this.generateEventId();
228 }
229
230 var domainContext = (domain.active && domain.active.sentryContext) || {};
231 var globalContext = this._globalContext || {};
232 kwargs.user = extend({}, globalContext.user, domainContext.user, kwargs.user);
233 kwargs.tags = extend({}, globalContext.tags, domainContext.tags, kwargs.tags);
234 kwargs.extra = extend({}, globalContext.extra, domainContext.extra, kwargs.extra);
235 // Perform a shallow copy of breadcrums to not send one that we'll capture below through as well
236 kwargs.breadcrumbs = {
237 values:
238 (domainContext.breadcrumbs && domainContext.breadcrumbs.slice()) ||
239 (globalContext.breadcrumbs && globalContext.breadcrumbs.slice()) ||
240 []
241 };
242
243 /*
244 `request` is our specified property name for the http interface: https://docs.sentry.io/clientdev/interfaces/http/
245 `req` is the conventional name for a request object in node/express/etc
246 we want to enable someone to pass a `request` property to kwargs according to http interface
247 but also want to provide convenience for passing a req object and having us parse it out
248 so we only parse a `req` property if the `request` property is absent/empty (and hence we won't clobber)
249 parseUser returns a partial kwargs object with a `request` property and possibly a `user` property
250 */
251 var request = this._createRequestObject(
252 globalContext.request,
253 domainContext.request,
254 kwargs.request
255 );
256 delete kwargs.request;
257
258 if (Object.keys(request).length === 0) {
259 request = this._createRequestObject(
260 globalContext.req,
261 domainContext.req,
262 kwargs.req
263 );
264 delete kwargs.req;
265 }
266
267 if (Object.keys(request).length > 0) {
268 var parseUser = Object.keys(kwargs.user).length === 0 ? this.parseUser : false;
269 extend(kwargs, parsers.parseRequest(request, parseUser));
270 } else {
271 kwargs.request = {};
272 }
273
274 kwargs.modules = utils.getModules();
275 kwargs.server_name = kwargs.server_name || this.name;
276
277 if (typeof global.process.version !== 'undefined') {
278 kwargs.extra.node = global.process.version;
279 }
280
281 kwargs.environment = kwargs.environment || this.environment;
282 kwargs.logger = kwargs.logger || this.loggerName;
283 kwargs.event_id = eventId;
284 kwargs.timestamp = new Date().toISOString().split('.')[0];
285 kwargs.project = this.dsn && this.dsn.project_id;
286 kwargs.platform = 'node';
287 kwargs.release = this.release;
288
289 // Cleanup empty properties before sending them to the server
290 Object.keys(kwargs).forEach(function(key) {
291 if (kwargs[key] == null || kwargs[key] === '') {
292 delete kwargs[key];
293 }
294 });
295
296 if (this.dataCallback) {
297 kwargs = this.dataCallback(kwargs);
298 }
299
300 // Capture breadcrumb before sending it, as we also want to have it even when
301 // it was dropped due to sampleRate or shouldSendCallback
302 this.captureBreadcrumb({
303 category: 'sentry',
304 message: kwargs.message,
305 event_id: kwargs.event_id,
306 level: kwargs.level || 'error' // presume error unless specified
307 });
308
309 var shouldSend = true;
310 if (!this._enabled) shouldSend = false;
311 if (this.shouldSendCallback && !this.shouldSendCallback(kwargs)) shouldSend = false;
312 if (Math.random() >= this.sampleRate) shouldSend = false;
313
314 if (shouldSend) {
315 this.send(kwargs, cb);
316 } else {
317 // wish there was a good way to communicate to cb why we didn't send; worth considering cb api change?
318 // could be shouldSendCallback, could be disabled, could be sample rate
319 // avoiding setImmediate here because node 0.8
320 cb &&
321 setTimeout(function() {
322 cb(null, eventId);
323 }, 0);
324 }
325 },
326
327 send: function send(kwargs, cb) {
328 var self = this;
329 var skwargs = stringify(kwargs);
330 var eventId = kwargs.event_id;
331
332 zlib.deflate(skwargs, function(err, buff) {
333 var message = buff.toString('base64'),
334 timestamp = new Date().getTime(),
335 headers = {
336 'X-Sentry-Auth': utils.getAuthHeader(
337 timestamp,
338 self.dsn.public_key,
339 self.dsn.private_key
340 ),
341 'Content-Type': 'application/octet-stream',
342 'Content-Length': message.length
343 };
344
345 self.transport.send(self, message, headers, eventId, cb);
346 });
347 },
348
349 captureMessage: function captureMessage(message, kwargs, cb) {
350 if (!cb && typeof kwargs === 'function') {
351 cb = kwargs;
352 kwargs = {};
353 } else {
354 kwargs = utils.isPlainObject(kwargs) ? extend({}, kwargs) : {};
355 }
356
357 var eventId = this.generateEventId();
358
359 if (this.stacktrace) {
360 var ex = new Error(message);
361
362 console.log(ex);
363
364 utils.parseStack(
365 ex,
366 function(frames) {
367 // We trim last frame, as it's our `new Error(message)` statement itself, which is redundant
368 kwargs.stacktrace = {
369 frames: frames.slice(0, -1)
370 };
371 this.process(eventId, parsers.parseText(message, kwargs), cb);
372 }.bind(this)
373 );
374 } else {
375 this.process(eventId, parsers.parseText(message, kwargs), cb);
376 }
377
378 return eventId;
379 },
380
381 captureException: function captureException(err, kwargs, cb) {
382 if (!cb && typeof kwargs === 'function') {
383 cb = kwargs;
384 kwargs = {};
385 } else {
386 kwargs = utils.isPlainObject(kwargs) ? extend({}, kwargs) : {};
387 }
388
389 if (!utils.isError(err)) {
390 if (utils.isPlainObject(err)) {
391 // This will allow us to group events based on top-level keys
392 // which is much better than creating new group when any key/value change
393 var keys = Object.keys(err).sort();
394 var message =
395 'Non-Error exception captured with keys: ' +
396 utils.serializeKeysForMessage(keys);
397 kwargs = extend(kwargs, {
398 message: message,
399 fingerprint: [md5(keys)],
400 extra: kwargs.extra || {}
401 });
402 kwargs.extra.__serialized__ = utils.serializeException(err);
403
404 err = new Error(message);
405 } else {
406 // This handles when someone does:
407 // throw "something awesome";
408 // We synthesize an Error here so we can extract a (rough) stack trace.
409 err = new Error(err);
410 }
411 }
412
413 var self = this;
414 var eventId = this.generateEventId();
415 parsers.parseError(err, kwargs, function(kw) {
416 self.process(eventId, kw, cb);
417 });
418
419 return eventId;
420 },
421
422 context: function(ctx, func) {
423 if (!func && typeof ctx === 'function') {
424 func = ctx;
425 ctx = {};
426 }
427
428 // todo/note: raven-js takes an args param to do apply(this, args)
429 // i don't think it's correct/necessary to bind this to the wrap call
430 // and i don't know if we need to support the args param; it's undocumented
431 return this.wrap(ctx, func).apply(null);
432 },
433
434 wrap: function(options, func) {
435 if (!this.installed) {
436 utils.consoleAlertOnce(
437 'Raven has not been installed, therefore no breadcrumbs will be captured. Call `Raven.config(...).install()` to fix this.'
438 );
439 }
440 if (!func && typeof options === 'function') {
441 func = options;
442 options = {};
443 }
444
445 var wrapDomain = domain.create();
446 // todo: better property name than sentryContext, maybe __raven__ or sth?
447 wrapDomain.sentryContext = options;
448
449 wrapDomain.on('error', this.uncaughtErrorHandler);
450 var wrapped = wrapDomain.bind(func);
451
452 for (var property in func) {
453 if ({}.hasOwnProperty.call(func, property)) {
454 wrapped[property] = func[property];
455 }
456 }
457 wrapped.prototype = func.prototype;
458 wrapped.__raven__ = true;
459 wrapped.__inner__ = func;
460 // note: domain.bind sets wrapped.domain, but it's not documented, unsure if we should rely on that
461 wrapped.__domain__ = wrapDomain;
462
463 return wrapped;
464 },
465
466 interceptErr: function(options, func) {
467 if (!func && typeof options === 'function') {
468 func = options;
469 options = {};
470 }
471 var self = this;
472 var wrapped = function() {
473 var err = arguments[0];
474 if (utils.isError(err)) {
475 self.captureException(err, options);
476 } else {
477 func.apply(null, arguments);
478 }
479 };
480
481 // repetitive with wrap
482 for (var property in func) {
483 if ({}.hasOwnProperty.call(func, property)) {
484 wrapped[property] = func[property];
485 }
486 }
487 wrapped.prototype = func.prototype;
488 wrapped.__raven__ = true;
489 wrapped.__inner__ = func;
490
491 return wrapped;
492 },
493
494 setContext: function setContext(ctx) {
495 if (domain.active) {
496 domain.active.sentryContext = ctx;
497 } else {
498 this._globalContext = ctx;
499 }
500 return this;
501 },
502
503 mergeContext: function mergeContext(ctx) {
504 extend(this.getContext(), ctx);
505 return this;
506 },
507
508 getContext: function getContext() {
509 if (domain.active) {
510 if (!domain.active.sentryContext) {
511 domain.active.sentryContext = {};
512 utils.consoleAlert('sentry context not found on active domain');
513 }
514 return domain.active.sentryContext;
515 }
516 return this._globalContext;
517 },
518
519 setCallbackHelper: function(propertyName, callback) {
520 var original = this[propertyName];
521 if (typeof callback === 'function') {
522 this[propertyName] = function(data) {
523 return callback(data, original);
524 };
525 } else {
526 this[propertyName] = callback;
527 }
528
529 return this;
530 },
531
532 /*
533 * Set the dataCallback option
534 *
535 * @param {function} callback The callback to run which allows the
536 * data blob to be mutated before sending
537 * @return {Raven}
538 */
539 setDataCallback: function(callback) {
540 return this.setCallbackHelper('dataCallback', callback);
541 },
542
543 /*
544 * Set the shouldSendCallback option
545 *
546 * @param {function} callback The callback to run which allows
547 * introspecting the blob before sending
548 * @return {Raven}
549 */
550 setShouldSendCallback: function(callback) {
551 return this.setCallbackHelper('shouldSendCallback', callback);
552 },
553
554 requestHandler: function() {
555 var self = this;
556 return function ravenRequestMiddleware(req, res, next) {
557 self.context({req: req}, function() {
558 domain.active.add(req);
559 domain.active.add(res);
560 next();
561 });
562 };
563 },
564
565 errorHandler: function() {
566 var self = this;
567 return function ravenErrorMiddleware(err, req, res, next) {
568 var status =
569 err.status ||
570 err.statusCode ||
571 err.status_code ||
572 (err.output && err.output.statusCode) ||
573 500;
574
575 // skip anything not marked as an internal server error
576 if (status < 500) return next(err);
577
578 var eventId = self.captureException(err, {req: req});
579 res.sentry = eventId;
580 return next(err);
581 };
582 },
583
584 captureBreadcrumb: function(breadcrumb) {
585 // Avoid capturing global-scoped breadcrumbs before instrumentation finishes
586 if (!this.installed) return;
587
588 breadcrumb = extend(
589 {
590 timestamp: +new Date() / 1000
591 },
592 breadcrumb
593 );
594 var currCtx = this.getContext();
595 if (!currCtx.breadcrumbs) currCtx.breadcrumbs = [];
596 currCtx.breadcrumbs.push(breadcrumb);
597 if (currCtx.breadcrumbs.length > this.maxBreadcrumbs) {
598 currCtx.breadcrumbs.shift();
599 }
600 this.setContext(currCtx);
601 },
602
603 _createRequestObject: function() {
604 /**
605 * When using proxy, some of the attributes of req/request objects are non-enumerable.
606 * To make sure, that they are still available to us after we consolidate our sources
607 * (eg. globalContext.request + domainContext.request + kwargs.request),
608 * we manually pull them out from original objects.
609 *
610 * Same scenario happens when some frameworks (eg. Koa) decide to use request within
611 * request. eg `this.request.req`, which adds aliases to the main `request` object.
612 * By manually reassigning them here, we don't need to add additional checks
613 * like `req.method || (req.req && req.req.method)`
614 *
615 * We don't use Object.assign/extend as it's only merging over objects own properties,
616 * and we don't want to go through all of the properties as well, as we simply don't
617 * need all of them.
618 **/
619 var sources = Array.from(arguments).filter(function(source) {
620 return Object.prototype.toString.call(source) === '[object Object]';
621 });
622 sources = [{}].concat(sources);
623 var request = extend.apply(null, sources);
624 var nonEnumerables = [
625 'headers',
626 'hostname',
627 'ip',
628 'method',
629 'protocol',
630 'query',
631 'secure',
632 'url'
633 ];
634
635 nonEnumerables.forEach(function(key) {
636 sources.forEach(function(source) {
637 if (source[key]) request[key] = source[key];
638 });
639 });
640
641 /**
642 * Check for 'host' *only* after we checked for 'hostname' first.
643 * This way we can avoid the noise coming from Express deprecation warning
644 * https://github.com/expressjs/express/blob/b97faff6e2aa4d34d79485fe4331cb0eec13ad57/lib/request.js#L450-L452
645 * REF: https://github.com/getsentry/raven-node/issues/96#issuecomment-354748884
646 **/
647 if (!request.hasOwnProperty('hostname')) {
648 sources.forEach(function(source) {
649 if (source.host) request.host = source.host;
650 });
651 }
652
653 return request;
654 }
655});
656
657// Maintain old API compat, need to make sure arguments length is preserved
658function Client(dsn, options) {
659 if (dsn instanceof Client) return dsn;
660 var ravenInstance = new Raven();
661 return ravenInstance.config.apply(ravenInstance, arguments);
662}
663nodeUtil.inherits(Client, Raven);
664
665// Singleton-by-default but not strictly enforced
666// todo these extra export props are sort of an adhoc mess, better way to manage?
667var defaultInstance = new Raven();
668defaultInstance.Client = Client;
669defaultInstance.version = require('../package.json').version;
670defaultInstance.disableConsoleAlerts = utils.disableConsoleAlerts;
671
672module.exports = defaultInstance;