diff --git a/CHANGELOG.md b/CHANGELOG.md index 86300d8b65..a64d8297cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- fix: Fix unhandle promise rejections not being tracked #1367 ## 2.2.1 diff --git a/sample/src/screens/EndToEndTestsScreen.tsx b/sample/src/screens/EndToEndTestsScreen.tsx index a1be477ba7..0801c68bae 100644 --- a/sample/src/screens/EndToEndTestsScreen.tsx +++ b/sample/src/screens/EndToEndTestsScreen.tsx @@ -52,6 +52,15 @@ const EndToEndTestsScreen = () => { }}> throw new Error + { + new Promise(() => { + throw new Error('Unhandled Promise Rejection'); + }); + }} + {...getTestProps('unhandledPromiseRejection')}> + Unhandled Promise Rejection + { Sentry.nativeCrash(); diff --git a/sample/src/screens/HomeScreen.tsx b/sample/src/screens/HomeScreen.tsx index f6a516816d..4dc9ade94c 100644 --- a/sample/src/screens/HomeScreen.tsx +++ b/sample/src/screens/HomeScreen.tsx @@ -171,6 +171,15 @@ const HomeScreen = (props: Props) => { Uncaught Thrown Error + { + new Promise(() => { + throw new Error('Unhandled Promise Rejection'); + }); + }}> + Unhandled Promise Rejection + + { Sentry.nativeCrash(); diff --git a/sample/test/e2e.test.ts b/sample/test/e2e.test.ts index 48da3c6ca5..f8f11474b3 100644 --- a/sample/test/e2e.test.ts +++ b/sample/test/e2e.test.ts @@ -93,4 +93,29 @@ describe('End to end tests for common events', () => { expect(sentryEvent.eventID).toMatch(eventId); }); + + test('unhandledPromiseRejection', async () => { + expect( + await driver.hasElementByAccessibilityId('unhandledPromiseRejection'), + ).toBe(true); + + const element = await driver.elementByAccessibilityId( + 'unhandledPromiseRejection', + ); + await element.click(); + + // Promises needs a while to fail + await driver.sleep(5000); + + expect(await driver.hasElementByAccessibilityId('eventId')).toBe(true); + + const eventIdElement = await driver.elementByAccessibilityId('eventId'); + const eventId = await eventIdElement.text(); + + await driver.sleep(10000); + + const sentryEvent = await fetchEvent(eventId); + + expect(sentryEvent.eventID).toMatch(eventId); + }); }); diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index 28c2ce6a63..a557fd9645 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from "@sentry/core"; import { Integration, Severity } from "@sentry/types"; -import { logger } from "@sentry/utils"; +import { getGlobalObject, logger } from "@sentry/utils"; import { ReactNativeClient } from "../client"; @@ -54,27 +54,63 @@ export class ReactNativeErrorHandlers implements Integration { enable: (arg: unknown) => void; // eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-extraneous-dependencies } = require("promise/setimmediate/rejection-tracking"); + tracking.disable(); tracking.enable({ allRejections: true, - onHandled: () => { - // We do nothing - }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any onUnhandled: (id: any, error: any) => { if (__DEV__) { + // We mimic the behavior of unhandled promise rejections showing up as a warning. // eslint-disable-next-line no-console console.warn(id, error); } - getCurrentHub().captureException(error, { data: { id }, originalException: error, }); }, }); + + /* eslint-disable + @typescript-eslint/no-var-requires, + import/no-extraneous-dependencies, + @typescript-eslint/no-explicit-any, + @typescript-eslint/no-unsafe-member-access + */ + const Promise = require("promise/setimmediate/core"); + const _global = getGlobalObject(); + + /* In newer RN versions >=0.63, the global promise is not the same reference as the one imported from the promise library. + Due to this, we need to take the methods that tracking.enable sets, and then set them on the global promise. + Note: We do not want to overwrite the whole promise in case there are extensions present. + + If the global promise is the same as the imported promise (expected in RN <0.63), we do nothing. + */ + const _onHandle = Promise._onHandle ?? Promise._Y; + const _onReject = Promise._onReject ?? Promise._Z; + + if ( + Promise !== _global.Promise && + typeof _onHandle !== "undefined" && + typeof _onReject !== "undefined" + ) { + if ("_onHandle" in _global.Promise && "_onReject" in _global.Promise) { + _global.Promise._onHandle = _onHandle; + _global.Promise._onReject = _onReject; + } else if ("_Y" in _global.Promise && "_Z" in _global.Promise) { + _global.Promise._Y = _onHandle; + _global.Promise._Z = _onReject; + } + } + /* eslint-enable + @typescript-eslint/no-var-requires, + import/no-extraneous-dependencies, + @typescript-eslint/no-explicit-any, + @typescript-eslint/no-unsafe-member-access + */ } } - /** * Handle erros */