diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index 934c201ad7..a5a4e7e487 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; +import type { EventHint, Integration } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; import type { ReactNativeClient } from '../client'; @@ -230,12 +230,19 @@ export class ReactNativeErrorHandlers implements Integration { const event = await client.eventFromException(error, hint); if (isFatal) { - event.level = 'fatal' as SeverityLevel; + event.level = 'fatal'; addExceptionMechanism(event, { handled: false, type: 'onerror', }); + } else { + event.level = 'error'; + + addExceptionMechanism(event, { + handled: true, + type: 'generic', + }); } currentHub.captureEvent(event, hint); diff --git a/test/integrations/reactnativeerrorhandlers.test.ts b/test/integrations/reactnativeerrorhandlers.test.ts index 3a4285f3e0..59dcd35c55 100644 --- a/test/integrations/reactnativeerrorhandlers.test.ts +++ b/test/integrations/reactnativeerrorhandlers.test.ts @@ -1,78 +1,28 @@ -import { BrowserClient, defaultIntegrations, defaultStackParser } from '@sentry/browser'; - -const mockBrowserClient: BrowserClient = new BrowserClient({ - stackParser: defaultStackParser, - integrations: defaultIntegrations, - transport: jest.fn(), -}); - -let mockHubCaptureException: jest.Mock; - -jest.mock('@sentry/core', () => { - const core = jest.requireActual('@sentry/core'); - - const scope = { - getAttachments: jest.fn(), - }; - - const client = { - getOptions: () => ({}), - eventFromException: (_exception: any, _hint?: EventHint): PromiseLike => - mockBrowserClient.eventFromException(_exception, _hint), - }; - - const hub = { - getClient: () => client, - getScope: () => scope, - captureEvent: jest.fn(), - captureException: jest.fn(), - }; - - mockHubCaptureException = hub.captureException; - - return { - ...core, - addGlobalEventProcessor: jest.fn(), - getCurrentHub: () => hub, - }; -}); - -jest.mock('@sentry/utils', () => { - const utils = jest.requireActual('@sentry/utils'); - return { - ...utils, - logger: { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, - }; -}); - -import { getCurrentHub } from '@sentry/core'; -import type { Event, EventHint, ExtendedError, Integration, SeverityLevel } from '@sentry/types'; +import { setCurrentClient } from '@sentry/core'; +import type { ExtendedError, Integration, SeverityLevel } from '@sentry/types'; import { ReactNativeErrorHandlers } from '../../src/js/integrations/reactnativeerrorhandlers'; - -interface MockTrackingOptions { - allRejections: boolean; - onUnhandled: jest.Mock; - onHandled: jest.Mock; -} +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; interface MockedReactNativeErrorHandlers extends Integration { _loadRejectionTracking: jest.Mock< { - disable: jest.Mock; - enable: jest.Mock; + disable: jest.Mock; + enable: jest.Mock; }, [] >; } describe('ReactNativeErrorHandlers', () => { + let client: TestClient; + beforeEach(() => { ErrorUtils.getGlobalHandler = () => jest.fn(); + + client = new TestClient(getDefaultTestClientOptions()); + setCurrentClient(client); + client.init(); }); afterEach(() => { @@ -98,28 +48,31 @@ describe('ReactNativeErrorHandlers', () => { test('Sets handled:false on a fatal error', async () => { await errorHandlerCallback(new Error('Test Error'), true); + await client.flush(); - const [event] = getActualCaptureEventArgs(); + const event = client.event; - expect(event.level).toBe('fatal' as SeverityLevel); - expect(event.exception?.values?.[0].mechanism?.handled).toBe(false); - expect(event.exception?.values?.[0].mechanism?.type).toBe('onerror'); + expect(event?.level).toBe('fatal' as SeverityLevel); + expect(event?.exception?.values?.[0].mechanism?.handled).toBe(false); + expect(event?.exception?.values?.[0].mechanism?.type).toBe('onerror'); }); test('Does not set handled:false on a non-fatal error', async () => { await errorHandlerCallback(new Error('Test Error'), false); + await client.flush(); - const [event] = getActualCaptureEventArgs(); + const event = client.event; - expect(event.level).toBe('error' as SeverityLevel); - expect(event.exception?.values?.[0].mechanism?.handled).toBe(true); - expect(event.exception?.values?.[0].mechanism?.type).toBe('generic'); + expect(event?.level).toBe('error' as SeverityLevel); + expect(event?.exception?.values?.[0].mechanism?.handled).toBe(true); + expect(event?.exception?.values?.[0].mechanism?.type).toBe('generic'); }); test('Includes original exception in hint', async () => { await errorHandlerCallback(new Error('Test Error'), false); + await client.flush(); - const [, hint] = getActualCaptureEventArgs(); + const hint = client.hint; expect(hint).toEqual(expect.objectContaining({ originalException: new Error('Test Error') })); }); @@ -127,10 +80,9 @@ describe('ReactNativeErrorHandlers', () => { describe('onUnhandledRejection', () => { test('unhandled rejected promise is captured with synthetical error', async () => { - mockHubCaptureException.mockClear(); const integration = new ReactNativeErrorHandlers(); const mockDisable = jest.fn(); - const mockEnable = jest.fn(); + const mockEnable = jest.fn(); (integration as unknown as MockedReactNativeErrorHandlers)._loadRejectionTracking = jest.fn(() => ({ disable: mockDisable, enable: mockEnable, @@ -139,7 +91,9 @@ describe('ReactNativeErrorHandlers', () => { const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; actualTrackingOptions?.onUnhandled?.(1, 'Test Error'); - const actualSyntheticError = mockHubCaptureException.mock.calls[0][1].syntheticException; + + await client.flush(); + const actualSyntheticError = client.hint?.syntheticException; expect(mockDisable).not.toHaveBeenCalled(); expect(mockEnable).toHaveBeenCalledWith( @@ -154,10 +108,9 @@ describe('ReactNativeErrorHandlers', () => { }); test('error like unhandled rejected promise is captured without synthetical error', async () => { - mockHubCaptureException.mockClear(); const integration = new ReactNativeErrorHandlers(); const mockDisable = jest.fn(); - const mockEnable = jest.fn(); + const mockEnable = jest.fn(); (integration as unknown as MockedReactNativeErrorHandlers)._loadRejectionTracking = jest.fn(() => ({ disable: mockDisable, enable: mockEnable, @@ -166,7 +119,9 @@ describe('ReactNativeErrorHandlers', () => { const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; actualTrackingOptions?.onUnhandled?.(1, new Error('Test Error')); - const actualSyntheticError = mockHubCaptureException.mock.calls[0][1].syntheticException; + + await client.flush(); + const actualSyntheticError = client.hint?.syntheticException; expect(mockDisable).not.toHaveBeenCalled(); expect(mockEnable).toHaveBeenCalledWith( @@ -181,10 +136,3 @@ describe('ReactNativeErrorHandlers', () => { }); }); }); - -function getActualCaptureEventArgs() { - const hub = getCurrentHub(); - const mockCall = (hub.captureEvent as jest.MockedFunction).mock.calls[0]; - - return mockCall; -} diff --git a/test/mocks/client.ts b/test/mocks/client.ts index 69771204e3..0a83cb404d 100644 --- a/test/mocks/client.ts +++ b/test/mocks/client.ts @@ -39,6 +39,7 @@ export class TestClient extends BaseClient { public static sendEventCalled?: (event: Event) => void; public event?: Event; + public hint?: EventHint; public session?: Session; public constructor(options: TestClientOptions) { @@ -73,6 +74,7 @@ export class TestClient extends BaseClient { public sendEvent(event: Event, hint?: EventHint): void { this.event = event; + this.hint = hint; // In real life, this will get deleted as part of envelope creation. delete event.sdkProcessingMetadata;