diff --git a/src/setup/telemetry/setupSentry.ts b/src/setup/telemetry/setupSentry.ts index e31551b4484d..fded8ed2ca86 100644 --- a/src/setup/telemetry/setupSentry.ts +++ b/src/setup/telemetry/setupSentry.ts @@ -57,8 +57,14 @@ function setupSentry(): void { integrations, environment: CONFIG.ENVIRONMENT, release: `${pkg.name}@${pkg.version}`, - // UPDATE_REQUIRED is not a real error and makes our errors in Spotnana spike and get rate limited when we bump the app min version, so ignore it - ignoreErrors: [CONST.ERROR.UPDATE_REQUIRED], + ignoreErrors: [ + // UPDATE_REQUIRED is not a real error and makes our errors in Spotnana spike and get rate limited when we bump the app min version, so ignore it + CONST.ERROR.UPDATE_REQUIRED, + // Bare-string rejections from the Convert Experiments script in web/index.html, which reads OnyxDB directly. + // They carry no stack frames for thirdPartyErrorFilterIntegration to tag; the prefix limits this to the + // browser SDK's rejection wording, so a real Error carrying the same text still reports. + /^Non-Error promise rejection captured with value: No data found for key/, + ], denyUrls: EXTENSION_DENY_URLS, beforeSendTransaction: processBeforeSendTransactions, enableLogs: true, diff --git a/tests/unit/setupSentryTest.ts b/tests/unit/setupSentryTest.ts new file mode 100644 index 000000000000..86c261be177e --- /dev/null +++ b/tests/unit/setupSentryTest.ts @@ -0,0 +1,46 @@ +import setupSentry from '@src/setup/telemetry/setupSentry'; + +import {stringMatchesSomePattern} from '@sentry/core'; + +jest.mock('@sentry/react-native', () => ({ + init: jest.fn(), + setTag: jest.fn(), +})); + +jest.mock('@libs/telemetry/integrations', () => ({})); + +const sentryMock = jest.requireMock<{init: jest.Mock}]>; setTag: jest.Mock}>('@sentry/react-native'); + +/** How the browser SDK words a promise rejected with a string. It carries no stack frames, so `ignoreErrors` is the only filter that can match it. */ +function rejectionMessage(reason: string): string { + return `Non-Error promise rejection captured with value: ${reason}`; +} + +/** Runs Sentry's own matcher over the patterns setupSentry registered, the way `eventFiltersIntegration` does. */ +function isIgnored(message: string): boolean { + const initOptions = sentryMock.init.mock.calls.at(0)?.at(0); + if (!initOptions) { + throw new Error('setupSentry did not call Sentry.init'); + } + + return stringMatchesSomePattern(message, initOptions.ignoreErrors ?? []); +} + +describe('setupSentry', () => { + beforeEach(() => { + jest.clearAllMocks(); + setupSentry(); + }); + + it('drops the bare-string rejection the Convert Experiments script emits when it reads a missing OnyxDB key', () => { + expect(isIgnored(rejectionMessage('No data found for key reportActions_undefined'))).toBe(true); + }); + + it('keeps unhandled rejections that do not come from that third-party OnyxDB read', () => { + expect(isIgnored(rejectionMessage("undefined is not an object (evaluating 'report.reportID')"))).toBe(false); + }); + + it('keeps a thrown Error carrying the same text, which unlike a bare rejection has a stack to act on', () => { + expect(isIgnored('No data found for key reportActions_123')).toBe(false); + }); +});