diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cdeeabd2..15890baa6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Added + +- Add new `reportUnhandledPromiseRejectionsAsHandled` config option [#2225](https://github.com/bugsnag/bugsnag-js/pull/2225) + ## [8.0.0] - 2024-08-29 ### Summary diff --git a/packages/core/config.js b/packages/core/config.js index ecea363547..e47c1e3bba 100644 --- a/packages/core/config.js +++ b/packages/core/config.js @@ -168,5 +168,10 @@ module.exports.schema = { isArray(value) && value.length === filter(value, feature => feature && typeof feature === 'object' && typeof feature.name === 'string' ).length + }, + reportUnhandledPromiseRejectionsAsHandled: { + defaultValue: () => false, + message: 'should be true|false', + validate: value => value === true || value === false } } diff --git a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts index be8465a0f3..0500cbd643 100644 --- a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts +++ b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts @@ -61,6 +61,34 @@ describe('plugin: node unhandled rejection handler', () => { process.listeners('unhandledRejection')[0](new Error('never gonna catch me'), Promise.resolve()) }) + it('should report unhandledRejection events as handled when reportUnhandledPromiseRejectionsAsHandled is true', (done) => { + const c = new Client({ + apiKey: 'api_key', + reportUnhandledPromiseRejectionsAsHandled: true, + onUnhandledRejection: (err: Error, event: EventWithInternals) => { + expect(err.message).toBe('never gonna catch me') + expect(event._handledState.unhandled).toBe(false) + expect(event._handledState.severity).toBe('error') + expect(event._handledState.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) + plugin.destroy() + done() + }, + plugins: [plugin] + }, { + ...schema, + onUnhandledRejection: { + validate: (val: unknown) => typeof val === 'function', + message: 'should be a function', + defaultValue: () => {} + } + }) + c._setDelivery(client => ({ + sendEvent: (payload, cb) => cb(), + sendSession: (payload, cb) => cb() + })) + process.listeners('unhandledRejection')[0](new Error('never gonna catch me'), Promise.resolve()) + }) + it('should tolerate delivery errors', done => { const c = new Client({ apiKey: 'api_key', diff --git a/packages/plugin-node-unhandled-rejection/unhandled-rejection.js b/packages/plugin-node-unhandled-rejection/unhandled-rejection.js index 493f35014c..f850315e7e 100644 --- a/packages/plugin-node-unhandled-rejection/unhandled-rejection.js +++ b/packages/plugin-node-unhandled-rejection/unhandled-rejection.js @@ -7,9 +7,12 @@ module.exports = { const ctx = client._clientContext && client._clientContext.getStore() const c = ctx || client + // Report unhandled promise rejections as handled if the user has configured it + const unhandled = !client._config.reportUnhandledPromiseRejectionsAsHandled + const event = c.Event.create(err, false, { severity: 'error', - unhandled: true, + unhandled, severityReason: { type: 'unhandledPromiseRejection' } }, 'unhandledRejection handler', 1) diff --git a/packages/plugin-react-native-unhandled-rejection/rejection-handler.js b/packages/plugin-react-native-unhandled-rejection/rejection-handler.js index 055b45f287..23a935465a 100644 --- a/packages/plugin-react-native-unhandled-rejection/rejection-handler.js +++ b/packages/plugin-react-native-unhandled-rejection/rejection-handler.js @@ -11,6 +11,9 @@ module.exports = { // Do not attach any listeners if autoDetectErrors is disabled or unhandledRejections are not an enabled error type if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledRejections) return () => { } + // Report unhandled promise rejections as handled if the user has configured it + const unhandled = !client._config.reportUnhandledPromiseRejectionsAsHandled + // Check if Hermes is available and is being used for promises // React Native v0.63 and v0.64 include global.HermesInternal but not 'hasPromise' if (global?.HermesInternal?.hasPromise?.() && global.HermesInternal.enablePromiseRejectionTracker) { @@ -19,7 +22,7 @@ module.exports = { onUnhandled: (id, rejection = {}) => { const event = client.Event.create(rejection, false, { severity: 'error', - unhandled: true, + unhandled, severityReason: { type: 'unhandledPromiseRejection' } }, 'promise rejection tracking', 1) @@ -39,7 +42,7 @@ module.exports = { onUnhandled: (id, error) => { const event = client.Event.create(error, false, { severity: 'error', - unhandled: true, + unhandled, severityReason: { type: 'unhandledPromiseRejection' } }, 'promise rejection tracking', 1) client._notify(event) diff --git a/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts b/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts index bcf2dfd0ca..609cf65878 100644 --- a/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts +++ b/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts @@ -49,6 +49,30 @@ describe('plugin: react native rejection handler', () => { stop() }) + it('should report unhandledRejection events as handled when reportUnhandledPromiseRejectionsAsHandled is true', (done) => { + expect.assertions(1) + + const c = new Client({ apiKey: 'api_key', reportUnhandledPromiseRejectionsAsHandled: true }) + c._setDelivery(client => ({ + sendEvent: (payload) => { + const r = JSON.parse(JSON.stringify(payload)) + expect(r.events[0].unhandled).toBe(false) + done() + }, + sendSession: () => { } + })) + const stop = plugin.load(c) + // in the interests of keeping the tests quick, TypeErrors get rejected quicker + // see: https://github.com/then/promise/blob/d980ed01b7a383bfec416c96095e2f40fd18ab34/src/rejection-tracking.js#L48-L54 + try { + // @ts-ignore + String.floop() + } catch (e) { + RnPromise.reject(e) + } + stop() + }) + it('should hook in to the hermes promise rejection tracker', (done) => { // @ts-ignore global.HermesInternal = { diff --git a/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts b/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts index 9b5b6da1c5..608c8ca6dc 100644 --- a/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts +++ b/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts @@ -43,6 +43,30 @@ describe('plugin: unhandled rejection', () => { handler({ reason: new Error('BAD_PROMISE') }) }) + it('should report unhandledRejection events as handled when reportUnhandledPromiseRejectionsAsHandled is true', (done) => { + const p = plugin(window) + const client = new Client({ + apiKey: 'API_KEY_YEAH', + reportUnhandledPromiseRejectionsAsHandled: true, + plugins: [p] + }) + + client._setDelivery(client => ({ + sendEvent: (payload) => { + const event = payload.events[0].toJSON() + expect(event.unhandled).toBe(false) + expect(event.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) + // @ts-ignore + p.destroy(window) + done() + }, + sendSession: () => {} + })) + + // simulate an UnhandledRejection event + getUnhandledRejectionHandler()({ reason: new Error('BAD_PROMISE') }) + }) + it('handles bad user input', done => { expect.assertions(6) diff --git a/packages/plugin-window-unhandled-rejection/unhandled-rejection.js b/packages/plugin-window-unhandled-rejection/unhandled-rejection.js index f766ab2707..22df627f8d 100644 --- a/packages/plugin-window-unhandled-rejection/unhandled-rejection.js +++ b/packages/plugin-window-unhandled-rejection/unhandled-rejection.js @@ -21,9 +21,12 @@ module.exports = (win = window) => { } } catch (e) {} + // Report unhandled promise rejections as handled if the user has configured it + const unhandled = !client._config.reportUnhandledPromiseRejectionsAsHandled + const event = client.Event.create(error, false, { severity: 'error', - unhandled: true, + unhandled, severityReason: { type: 'unhandledPromiseRejection' } }, 'unhandledrejection handler', 1, client._logger)