1 | 'use strict';
|
2 |
|
3 | var stringify = require('../vendor/json-stringify-safe');
|
4 | var parsers = require('./parsers');
|
5 | var zlib = require('zlib');
|
6 | var utils = require('./utils');
|
7 | var uuid = require('uuid');
|
8 | var transports = require('./transports');
|
9 | var nodeUtil = require('util');
|
10 | var events = require('events');
|
11 | var domain = require('domain');
|
12 | var md5 = require('md5');
|
13 |
|
14 | var instrumentor = require('./instrumentation/instrumentor');
|
15 |
|
16 | var extend = utils.extend;
|
17 |
|
18 | function Raven() {
|
19 | this.breadcrumbs = {
|
20 | record: this.captureBreadcrumb.bind(this)
|
21 | };
|
22 | }
|
23 |
|
24 | nodeUtil.inherits(Raven, events.EventEmitter);
|
25 |
|
26 | extend(Raven.prototype, {
|
27 | config: function config(dsn, options) {
|
28 |
|
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 |
|
41 | dsn = global.process.env.SENTRY_DSN;
|
42 | options = {};
|
43 | }
|
44 | if (typeof dsn === 'object') {
|
45 |
|
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 |
|
65 |
|
66 | this.autoBreadcrumbs = options.autoBreadcrumbs || false;
|
67 |
|
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 |
|
85 | this.ca = options.ca || null;
|
86 | }
|
87 |
|
88 |
|
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 |
|
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 |
|
173 |
|
174 |
|
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 |
|
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 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | caughtSecondError = true;
|
205 | setTimeout(function() {
|
206 | if (!calledFatalError) {
|
207 |
|
208 | calledFatalError = true;
|
209 | self.onFatalError(firstError, err);
|
210 | } else {
|
211 |
|
212 | }
|
213 | }, (self.sendTimeout + 1) * 1000);
|
214 | }
|
215 | };
|
216 | },
|
217 |
|
218 | generateEventId: function generateEventId() {
|
219 | return uuid().replace(/-/g, '');
|
220 | },
|
221 |
|
222 | process: function process(eventId, kwargs, cb) {
|
223 |
|
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 |
|
236 | kwargs.breadcrumbs = {
|
237 | values:
|
238 | (domainContext.breadcrumbs && domainContext.breadcrumbs.slice()) ||
|
239 | (globalContext.breadcrumbs && globalContext.breadcrumbs.slice()) ||
|
240 | []
|
241 | };
|
242 |
|
243 | |
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
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 |
|
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 |
|
301 |
|
302 | this.captureBreadcrumb({
|
303 | category: 'sentry',
|
304 | message: kwargs.message,
|
305 | event_id: kwargs.event_id,
|
306 | level: kwargs.level || 'error'
|
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 |
|
318 |
|
319 |
|
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 |
|
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 |
|
392 |
|
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 |
|
407 |
|
408 |
|
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 |
|
429 |
|
430 |
|
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 |
|
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 |
|
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 |
|
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 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 | setDataCallback: function(callback) {
|
540 | return this.setCallbackHelper('dataCallback', callback);
|
541 | },
|
542 |
|
543 | |
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
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 |
|
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 |
|
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 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
617 |
|
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 |
|
643 |
|
644 |
|
645 |
|
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 |
|
658 | function Client(dsn, options) {
|
659 | if (dsn instanceof Client) return dsn;
|
660 | var ravenInstance = new Raven();
|
661 | return ravenInstance.config.apply(ravenInstance, arguments);
|
662 | }
|
663 | nodeUtil.inherits(Client, Raven);
|
664 |
|
665 |
|
666 |
|
667 | var defaultInstance = new Raven();
|
668 | defaultInstance.Client = Client;
|
669 | defaultInstance.version = require('../package.json').version;
|
670 | defaultInstance.disableConsoleAlerts = utils.disableConsoleAlerts;
|
671 |
|
672 | module.exports = defaultInstance;
|