Skip to content

Commit 0516d9e

Browse files
authored
feat: Support unhandled rejections by default (#1242)
1 parent 8f51189 commit 0516d9e

File tree

6 files changed

+128
-29
lines changed

6 files changed

+128
-29
lines changed

docs/config.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ Those configuration options are documented below:
227227
By default, Raven captures as many as 100 breadcrumb entries. If you find this too noisy, you can reduce this
228228
number by setting `maxBreadcrumbs`. Note that this number cannot be set higher than the default of 100.
229229

230+
.. describe:: captureUnhandledRejections
231+
232+
By default, Raven captures all unhandled promise rejections using standard ``unhandledrejection`` event.
233+
If you want to disable this behaviour, set this option to ``false``.
234+
230235
.. describe:: transport
231236

232237
Override the default HTTP data transport handler.

docs/usage.rst

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,11 @@ And set an ``Access-Control-Allow-Origin`` HTTP header on that file.
333333
Promises
334334
--------
335335

336-
By default, Raven.js does not capture unhandled promise rejections.
336+
By default, Raven.js capture unhandled promise rejections as described in official ECMAScript 6 standard.
337337

338-
Most Promise libraries have a global hook for capturing unhandled errors. You will need to
339-
manually hook into such an event handler and call ``Raven.captureException`` or ``Raven.captureMessage``
340-
directly.
338+
Most Promise libraries however, have a global hook for capturing unhandled errors. You may want to disable default behaviour
339+
by setting ``captureUnhandledRejections`` option to ``false`` and manually hook into such event handler
340+
and call ``Raven.captureException`` or ``Raven.captureMessage`` directly.
341341

342342
For example, the `RSVP.js library
343343
<https://github.com/tildeio/rsvp.js/>`_ (used by Ember.js) allows you to bind an event handler to a `global error event
@@ -350,13 +350,8 @@ For example, the `RSVP.js library
350350
});
351351
352352
`Bluebird
353-
<http://bluebirdjs.com/>`_ and other promise libraries report unhandled rejections to a global DOM event, ``unhandledrejection``:
354-
355-
.. code-block:: javascript
356-
357-
window.onunhandledrejection = function(evt) {
358-
Raven.captureException(evt.reason);
359-
};
353+
<http://bluebirdjs.com/>`_ and other promise libraries report unhandled rejections to a global DOM event, ``unhandledrejection``.
354+
In this case, you don't need to do anything, we've already got you covered by with default ``captureUnhandledRejections: true`` setting.
360355

361356
Please consult your promise library documentation on how to hook into its global unhandled rejection handler, if it exposes one.
362357

src/raven.js

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function Raven() {
7979
includePaths: [],
8080
headers: null,
8181
collectWindowErrors: true,
82+
captureUnhandledRejections: true,
8283
maxMessageLength: 0,
8384

8485
// By default, truncates URL values to 250 chars
@@ -237,6 +238,10 @@ Raven.prototype = {
237238
self._handleOnErrorStackInfo.apply(self, arguments);
238239
});
239240

241+
if (self._globalOptions.captureUnhandledRejections) {
242+
self._attachPromiseRejectionHandler();
243+
}
244+
240245
self._patchFunctionToString();
241246

242247
if (self._globalOptions.instrument && self._globalOptions.instrument.tryCatch) {
@@ -387,14 +392,15 @@ Raven.prototype = {
387392
return wrapped;
388393
},
389394

390-
/*
391-
* Uninstalls the global error handler.
392-
*
393-
* @return {Raven}
394-
*/
395+
/**
396+
* Uninstalls the global error handler.
397+
*
398+
* @return {Raven}
399+
*/
395400
uninstall: function() {
396401
TraceKit.report.uninstall();
397402

403+
this._detachPromiseRejectionHandler();
398404
this._unpatchFunctionToString();
399405
this._restoreBuiltIns();
400406
this._restoreConsole();
@@ -405,13 +411,47 @@ Raven.prototype = {
405411
return this;
406412
},
407413

408-
/*
409-
* Manually capture an exception and send it over to Sentry
410-
*
411-
* @param {error} ex An exception to be logged
412-
* @param {object} options A specific set of options for this error [optional]
413-
* @return {Raven}
414-
*/
414+
/**
415+
* Callback used for `unhandledrejection` event
416+
*
417+
* @param {PromiseRejectionEvent} event An object containing
418+
* promise: the Promise that was rejected
419+
* reason: the value with which the Promise was rejected
420+
* @return void
421+
*/
422+
_promiseRejectionHandler: function(event) {
423+
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
424+
this.captureException(event.reason);
425+
},
426+
427+
/**
428+
* Installs the global promise rejection handler.
429+
*
430+
* @return {raven}
431+
*/
432+
_attachPromiseRejectionHandler: function() {
433+
this._promiseRejectionHandler = this._promiseRejectionHandler.bind(this);
434+
_window.addEventListener('unhandledrejection', this._promiseRejectionHandler);
435+
return this;
436+
},
437+
438+
/**
439+
* Uninstalls the global promise rejection handler.
440+
*
441+
* @return {raven}
442+
*/
443+
_detachPromiseRejectionHandler: function() {
444+
_window.removeEventListener('unhandledrejection', this._promiseRejectionHandler);
445+
return this;
446+
},
447+
448+
/**
449+
* Manually capture an exception and send it over to Sentry
450+
*
451+
* @param {error} ex An exception to be logged
452+
* @param {object} options A specific set of options for this error [optional]
453+
* @return {Raven}
454+
*/
415455
captureException: function(ex, options) {
416456
options = objectMerge({trimHeadFrames: 0}, options ? options : {});
417457
// Cases for sending ex as a message, rather than an exception

src/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ function supportsReferrerPolicy() {
9797
}
9898
}
9999

100+
function supportsPromiseRejectionEvent() {
101+
return typeof PromiseRejectionEvent === 'function';
102+
}
103+
100104
function wrappedCallback(callback) {
101105
function dataCallback(data, original) {
102106
var normalizedData = callback(data) || data;
@@ -450,6 +454,7 @@ module.exports = {
450454
supportsErrorEvent: supportsErrorEvent,
451455
supportsFetch: supportsFetch,
452456
supportsReferrerPolicy: supportsReferrerPolicy,
457+
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
453458
wrappedCallback: wrappedCallback,
454459
each: each,
455460
objectMerge: objectMerge,

test/raven.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var joinRegExp = utils.joinRegExp;
2525
var supportsErrorEvent = utils.supportsErrorEvent;
2626
var supportsFetch = utils.supportsFetch;
2727
var supportsReferrerPolicy = utils.supportsReferrerPolicy;
28+
var supportsPromiseRejectionEvent = utils.supportsPromiseRejectionEvent;
2829

2930
// window.console must be stubbed in for browsers that don't have it
3031
if (typeof window.console === 'undefined') {
@@ -756,6 +757,40 @@ describe('globals', function() {
756757
});
757758
});
758759

760+
if (supportsPromiseRejectionEvent()) {
761+
describe('captureUnhandledRejections', function() {
762+
it('should capture string rejections', function(done) {
763+
Raven._attachPromiseRejectionHandler();
764+
765+
this.sinon.stub(Raven, 'captureException').callsFake(function(reason) {
766+
assert.equal(reason, 'foo');
767+
Raven._detachPromiseRejectionHandler();
768+
done();
769+
});
770+
771+
new Promise(function(resolve, reject) {
772+
reject('foo');
773+
});
774+
});
775+
776+
it('should capture error rejections', function(done) {
777+
var err = new Error('foo');
778+
779+
Raven._attachPromiseRejectionHandler();
780+
781+
this.sinon.stub(Raven, 'captureException').callsFake(function(reason) {
782+
assert.equal(reason, err);
783+
Raven._detachPromiseRejectionHandler();
784+
done();
785+
});
786+
787+
new Promise(function(resolve, reject) {
788+
reject(err);
789+
});
790+
});
791+
});
792+
}
793+
759794
describe('send', function() {
760795
it('should build a good data payload', function() {
761796
this.sinon.stub(Raven, 'isSetup').returns(true);
@@ -2347,6 +2382,29 @@ describe('Raven (public API)', function() {
23472382
});
23482383
});
23492384

2385+
describe('captureUnhandledRejections', function() {
2386+
it('should be true by default', function() {
2387+
Raven.config(SENTRY_DSN);
2388+
assert.isTrue(Raven._globalOptions.captureUnhandledRejections);
2389+
});
2390+
2391+
it('should be true if set to true', function() {
2392+
Raven.config(SENTRY_DSN, {
2393+
captureUnhandledRejections: true
2394+
});
2395+
2396+
assert.isTrue(Raven._globalOptions.captureUnhandledRejections);
2397+
});
2398+
2399+
it('should be false if set to false', function() {
2400+
Raven.config(SENTRY_DSN, {
2401+
captureUnhandledRejections: false
2402+
});
2403+
2404+
assert.isFalse(Raven._globalOptions.captureUnhandledRejections);
2405+
});
2406+
});
2407+
23502408
describe('maxBreadcrumbs', function() {
23512409
it('should override the default', function() {
23522410
Raven.config(SENTRY_DSN, {maxBreadcrumbs: 50});

test/vendor/tracekit.test.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,7 @@ describe('TraceKit', function() {
181181
});
182182

183183
describe('when no 5th argument (error object)', function() {
184-
it('should seperate name, message for default error types (e.g. ReferenceError)', function(
185-
done
186-
) {
184+
it('should seperate name, message for default error types (e.g. ReferenceError)', function(done) {
187185
subscriptionHandler = function(stackInfo, extra) {
188186
assert.equal(stackInfo.name, 'ReferenceError');
189187
assert.equal(stackInfo.message, 'foo is undefined');
@@ -203,9 +201,7 @@ describe('TraceKit', function() {
203201
done();
204202
});
205203

206-
it('should separate name, message for default error types on Opera Mini (see #546)', function(
207-
done
208-
) {
204+
it('should separate name, message for default error types on Opera Mini (see #546)', function(done) {
209205
subscriptionHandler = function(stackInfo, extra) {
210206
assert.equal(stackInfo.name, 'ReferenceError');
211207
assert.equal(stackInfo.message, 'Undefined variable: foo');

0 commit comments

Comments
 (0)