diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1726e96c..7d9697840e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Sentry.wrapExpoAsset(Asset); ``` - Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788)) +- Automatically capture a warning event when Expo Updates performs an emergency launch ([#5794](https://github.com/getsentry/sentry-react-native/pull/5794)) - Adds environment configuration in the Expo config plugin. This can be set with the `SENTRY_ENVIRONMENT` env variable or in `sentry.options.json` ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) ```json ["@sentry/react-native/expo", { diff --git a/packages/core/src/js/integrations/expocontext.ts b/packages/core/src/js/integrations/expocontext.ts index e3d9e1630d..ae234d909d 100644 --- a/packages/core/src/js/integrations/expocontext.ts +++ b/packages/core/src/js/integrations/expocontext.ts @@ -19,6 +19,7 @@ export const expoContextIntegration = (): Integration => { } setExpoUpdatesNativeContext(); + captureEmergencyLaunchEvent(client); }); } @@ -37,6 +38,29 @@ export const expoContextIntegration = (): Integration => { } } + function captureEmergencyLaunchEvent(client: ReactNativeClient): void { + if (!isExpo() || isExpoGo()) { + return; + } + + const updatesContext = getExpoUpdatesContextCached(); + if (!updatesContext.is_emergency_launch) { + return; + } + + const message = updatesContext.emergency_launch_reason + ? `Expo Updates emergency launch: ${updatesContext.emergency_launch_reason}` + : 'Expo Updates emergency launch'; + + client.captureEvent({ + level: 'warning', + message, + tags: { + 'expo.updates.emergency_launch': 'true', + }, + }); + } + function processEvent(event: Event): Event { if (!isExpo()) { return event; diff --git a/packages/core/test/integrations/expocontext.test.ts b/packages/core/test/integrations/expocontext.test.ts index 1b9a79c014..faf4c35bb2 100644 --- a/packages/core/test/integrations/expocontext.test.ts +++ b/packages/core/test/integrations/expocontext.test.ts @@ -8,7 +8,7 @@ import * as environment from '../../src/js/utils/environment'; import type { ExpoUpdates } from '../../src/js/utils/expoglobalobject'; import { getExpoDevice } from '../../src/js/utils/expomodules'; import * as expoModules from '../../src/js/utils/expomodules'; -import { setupTestClient } from '../mocks/client'; +import { setupTestClient, TestClient } from '../mocks/client'; import { NATIVE } from '../mockWrapper'; jest.mock('../../src/js/wrapper', () => jest.requireActual('../mockWrapper')); @@ -80,6 +80,111 @@ describe('Expo Context Integration', () => { }); }); + describe('Emergency Launch Event', () => { + it('captures a warning event on emergency launch', () => { + jest.spyOn(environment, 'isExpo').mockReturnValue(true); + jest.spyOn(environment, 'isExpoGo').mockReturnValue(false); + jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({ + isEmergencyLaunch: true, + emergencyLaunchReason: 'The previous launch failed with a fatal error.', + }); + + setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] }); + + const capturedEvent = TestClient.instance?.eventQueue.find( + e => e.tags?.['expo.updates.emergency_launch'] === 'true', + ); + + expect(capturedEvent).toBeDefined(); + expect(capturedEvent?.level).toBe('warning'); + expect(capturedEvent?.message).toBe( + 'Expo Updates emergency launch: The previous launch failed with a fatal error.', + ); + }); + + it('captures a warning event without reason when reason is missing', () => { + jest.spyOn(environment, 'isExpo').mockReturnValue(true); + jest.spyOn(environment, 'isExpoGo').mockReturnValue(false); + jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({ + isEmergencyLaunch: true, + }); + + setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] }); + + const capturedEvent = TestClient.instance?.eventQueue.find( + e => e.tags?.['expo.updates.emergency_launch'] === 'true', + ); + + expect(capturedEvent).toBeDefined(); + expect(capturedEvent?.level).toBe('warning'); + expect(capturedEvent?.message).toBe('Expo Updates emergency launch'); + }); + + it('does not capture event when not emergency launch', () => { + jest.spyOn(environment, 'isExpo').mockReturnValue(true); + jest.spyOn(environment, 'isExpoGo').mockReturnValue(false); + jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({ + isEmergencyLaunch: false, + }); + + setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] }); + + const capturedEvent = TestClient.instance?.eventQueue.find( + e => e.tags?.['expo.updates.emergency_launch'] === 'true', + ); + + expect(capturedEvent).toBeUndefined(); + }); + + it('does not capture event when not expo', () => { + jest.spyOn(environment, 'isExpo').mockReturnValue(false); + jest.spyOn(environment, 'isExpoGo').mockReturnValue(false); + jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({ + isEmergencyLaunch: true, + }); + + setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] }); + + const capturedEvent = TestClient.instance?.eventQueue.find( + e => e.tags?.['expo.updates.emergency_launch'] === 'true', + ); + + expect(capturedEvent).toBeUndefined(); + }); + + it('does not capture event when in Expo Go', () => { + jest.spyOn(environment, 'isExpo').mockReturnValue(true); + jest.spyOn(environment, 'isExpoGo').mockReturnValue(true); + jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({ + isEmergencyLaunch: true, + }); + + setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] }); + + const capturedEvent = TestClient.instance?.eventQueue.find( + e => e.tags?.['expo.updates.emergency_launch'] === 'true', + ); + + expect(capturedEvent).toBeUndefined(); + }); + + it('does not capture event when native is disabled', () => { + jest.spyOn(environment, 'isExpo').mockReturnValue(true); + jest.spyOn(environment, 'isExpoGo').mockReturnValue(false); + jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({ + isEmergencyLaunch: true, + }); + + setupTestClient({ enableNative: false, integrations: [expoContextIntegration()] }); + + const capturedEvent = TestClient.instance?.eventQueue.find( + e => e.tags?.['expo.updates.emergency_launch'] === 'true', + ); + + expect(capturedEvent).toBeUndefined(); + }); + }); + describe('Non Expo App', () => { beforeEach(() => { jest.spyOn(environment, 'isExpo').mockReturnValue(false);