Skip to content

Commit 86c29dc

Browse files
fix(expo-context): Expo Updates context is passed to native after init (#4808)
1 parent e5d6668 commit 86c29dc

File tree

6 files changed

+181
-3
lines changed

6 files changed

+181
-3
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Fixes
12+
13+
- Expo Updates Context is passed to native after native init to be available for crashes ([#4808](https://github.com/getsentry/sentry-react-native/pull/4808))
14+
1115
### Dependencies
1216

1317
- Bump CLI from v2.43.1 to v2.44.0 ([#4804](https://github.com/getsentry/sentry-react-native/pull/4804))

packages/core/src/js/client.ts

+21
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,26 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
140140
this._initNativeSdk();
141141
}
142142

143+
/**
144+
* Register a hook on this client.
145+
*
146+
* (Generic method signature to allow for custom React Native Client events.)
147+
*/
148+
public on(hook: string, callback: unknown): () => void {
149+
// @ts-expect-error on from the base class doesn't support generic types
150+
return super.on(hook, callback);
151+
}
152+
153+
/**
154+
* Emit a hook that was previously registered via `on()`.
155+
*
156+
* (Generic method signature to allow for custom React Native Client events.)
157+
*/
158+
public emit(hook: string, ...rest: unknown[]): void {
159+
// @ts-expect-error emit from the base class doesn't support generic types
160+
super.emit(hook, ...rest);
161+
}
162+
143163
/**
144164
* Starts native client with dsn and options
145165
*/
@@ -165,6 +185,7 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
165185
)
166186
.then((didCallNativeInit: boolean) => {
167187
this._options.onReady?.({ didCallNativeInit });
188+
this.emit('afterInit');
168189
})
169190
.then(undefined, error => {
170191
logger.error('The OnReady callback threw an error: ', error);

packages/core/src/js/integrations/expocontext.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type DeviceContext, type Event, type Integration, type OsContext, logger } from '@sentry/core';
22

3+
import type { ReactNativeClient } from '../client';
34
import { isExpo, isExpoGo } from '../utils/environment';
45
import { getExpoDevice, getExpoUpdates } from '../utils/expomodules';
56
import { NATIVE } from '../wrapper';
@@ -12,8 +13,14 @@ export const OTA_UPDATES_CONTEXT_KEY = 'ota_updates';
1213
export const expoContextIntegration = (): Integration => {
1314
let _expoUpdatesContextCached: ExpoUpdatesContext | undefined;
1415

15-
function setup(): void {
16-
setExpoUpdatesNativeContext();
16+
function setup(client: ReactNativeClient): void {
17+
client.on('afterInit', () => {
18+
if (!client.getOptions().enableNative) {
19+
return;
20+
}
21+
22+
setExpoUpdatesNativeContext();
23+
});
1724
}
1825

1926
function setExpoUpdatesNativeContext(): void {
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { ReactNativeClient } from '../src/js';
2+
import type { ReactNativeClientOptions } from '../src/js/options';
3+
import { NATIVE } from './mockWrapper';
4+
5+
jest.useFakeTimers({ advanceTimers: true });
6+
7+
jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper'));
8+
9+
describe('ReactNativeClient emits `afterInit` event', () => {
10+
beforeEach(() => {
11+
jest.clearAllMocks();
12+
});
13+
14+
test('emits `afterInit` event when native is enabled', async () => {
15+
const client = setupReactNativeClient({
16+
enableNative: true,
17+
});
18+
19+
const emitSpy = jest.spyOn(client, 'emit');
20+
client.init();
21+
22+
await jest.runOnlyPendingTimersAsync();
23+
24+
expect(emitSpy).toHaveBeenCalledWith('afterInit');
25+
});
26+
27+
test('emits `afterInit` event when native is disabled', async () => {
28+
const client = setupReactNativeClient({
29+
enableNative: false,
30+
});
31+
32+
const emitSpy = jest.spyOn(client, 'emit');
33+
client.init();
34+
35+
await jest.runOnlyPendingTimersAsync();
36+
expect(emitSpy).toHaveBeenCalledWith('afterInit');
37+
});
38+
39+
test('emits `afterInit` event when native init is rejected', async () => {
40+
NATIVE.initNativeSdk = jest.fn().mockRejectedValue(new Error('Test Native Init Rejected'));
41+
42+
const client = setupReactNativeClient({
43+
enableNative: false,
44+
});
45+
46+
const emitSpy = jest.spyOn(client, 'emit');
47+
client.init();
48+
49+
await jest.runOnlyPendingTimersAsync();
50+
expect(emitSpy).toHaveBeenCalledWith('afterInit');
51+
});
52+
});
53+
54+
function setupReactNativeClient(options: Partial<ReactNativeClientOptions> = {}): ReactNativeClient {
55+
return new ReactNativeClient({
56+
...DEFAULT_OPTIONS,
57+
...options,
58+
});
59+
}
60+
61+
const EXAMPLE_DSN = 'https://[email protected]/148053';
62+
63+
const DEFAULT_OPTIONS: ReactNativeClientOptions = {
64+
dsn: EXAMPLE_DSN,
65+
enableNative: true,
66+
enableNativeCrashHandling: true,
67+
enableNativeNagger: true,
68+
autoInitializeNativeSdk: true,
69+
enableAutoPerformanceTracing: true,
70+
enableWatchdogTerminationTracking: true,
71+
patchGlobalPromise: true,
72+
integrations: [],
73+
transport: () => ({
74+
send: jest.fn(),
75+
flush: jest.fn(),
76+
}),
77+
stackParser: jest.fn().mockReturnValue([]),
78+
};

packages/core/test/integrations/expocontext.test.ts

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,80 @@
1-
import type { Client, Event } from '@sentry/core';
1+
import { type Client, type Event, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
22

33
import { expoContextIntegration, OTA_UPDATES_CONTEXT_KEY } from '../../src/js/integrations/expocontext';
44
import * as environment from '../../src/js/utils/environment';
55
import type { ExpoUpdates } from '../../src/js/utils/expoglobalobject';
66
import { getExpoDevice } from '../../src/js/utils/expomodules';
77
import * as expoModules from '../../src/js/utils/expomodules';
8+
import { setupTestClient } from '../mocks/client';
9+
import { NATIVE } from '../mockWrapper';
810

11+
jest.mock('../../src/js/wrapper', () => jest.requireActual('../mockWrapper'));
912
jest.mock('../../src/js/utils/expomodules');
1013

1114
describe('Expo Context Integration', () => {
1215
afterEach(() => {
1316
jest.clearAllMocks();
17+
18+
getCurrentScope().clear();
19+
getIsolationScope().clear();
20+
getGlobalScope().clear();
21+
});
22+
23+
describe('Set Native Context after init()', () => {
24+
beforeEach(() => {
25+
jest.spyOn(expoModules, 'getExpoUpdates').mockReturnValue({
26+
updateId: '123',
27+
channel: 'default',
28+
runtimeVersion: '1.0.0',
29+
checkAutomatically: 'always',
30+
emergencyLaunchReason: 'some reason',
31+
launchDuration: 1000,
32+
createdAt: new Date('2021-01-01T00:00:00.000Z'),
33+
});
34+
});
35+
36+
it('calls setContext when native enabled', () => {
37+
jest.spyOn(environment, 'isExpo').mockReturnValue(true);
38+
jest.spyOn(environment, 'isExpoGo').mockReturnValue(false);
39+
40+
setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] });
41+
42+
expect(NATIVE.setContext).toHaveBeenCalledWith(
43+
OTA_UPDATES_CONTEXT_KEY,
44+
expect.objectContaining({
45+
update_id: '123',
46+
channel: 'default',
47+
runtime_version: '1.0.0',
48+
}),
49+
);
50+
});
51+
52+
it('does not call setContext when native disabled', () => {
53+
jest.spyOn(environment, 'isExpo').mockReturnValue(true);
54+
jest.spyOn(environment, 'isExpoGo').mockReturnValue(false);
55+
56+
setupTestClient({ enableNative: false, integrations: [expoContextIntegration()] });
57+
58+
expect(NATIVE.setContext).not.toHaveBeenCalled();
59+
});
60+
61+
it('does not call setContext when not expo', () => {
62+
jest.spyOn(environment, 'isExpo').mockReturnValue(false);
63+
jest.spyOn(environment, 'isExpoGo').mockReturnValue(false);
64+
65+
setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] });
66+
67+
expect(NATIVE.setContext).not.toHaveBeenCalled();
68+
});
69+
70+
it('does not call setContext when expo go', () => {
71+
jest.spyOn(environment, 'isExpo').mockReturnValue(true);
72+
jest.spyOn(environment, 'isExpoGo').mockReturnValue(true);
73+
74+
setupTestClient({ enableNative: true, integrations: [expoContextIntegration()] });
75+
76+
expect(NATIVE.setContext).not.toHaveBeenCalled();
77+
});
1478
});
1579

1680
describe('Non Expo App', () => {

packages/core/test/mocks/client.ts

+4
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,9 @@ export function setupTestClient(options: Partial<TestClientOptions> = {}): TestC
114114
const client = new TestClient(finalOptions);
115115
setCurrentClient(client);
116116
client.init();
117+
118+
// @ts-expect-error Only available on ReactNativeClient
119+
client.emit('afterInit');
120+
117121
return client;
118122
}

0 commit comments

Comments
 (0)