Skip to content

Commit 04207c4

Browse files
antonisclaude
andauthored
fix(core): Retry native module resolution to prevent silent event drops (#5981)
* fix(core): Retry native module resolution to prevent silent event drops The RNSentry TurboModule reference is resolved once at module load time. In production Hermes bytecode builds, JS execution can start before the TurboModule registry is fully populated, causing getRNSentryModule() to return undefined permanently. This silently disables native transport and drops all events. Re-resolve the native module in isNativeAvailable() if the initial resolution returned undefined. By the time Sentry.init() calls this method, TurboModules are registered and the module resolves correctly. Fixes #5508 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 50f4350 commit 04207c4

File tree

3 files changed

+39
-1
lines changed

3 files changed

+39
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
### Fixes
1818

19+
- Retry native module resolution to prevent silent event drops in production Hermes builds ([#5981](https://github.com/getsentry/sentry-react-native/pull/5981))
1920
- Lazy-load Metro internal modules to prevent Expo 55 import errors ([#5958](https://github.com/getsentry/sentry-react-native/pull/5958))
2021
- Fix app start transaction profile offset by using the actual profiling start timestamp instead of the adjusted app start time ([#5962](https://github.com/getsentry/sentry-react-native/issues/5962))
2122
- Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965))

packages/core/src/js/wrapper.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function getRNSentryModule(): Spec | undefined {
4545
: NativeModules.RNSentry;
4646
}
4747

48-
const RNSentry: Spec | undefined = getRNSentryModule();
48+
let RNSentry: Spec | undefined = getRNSentryModule();
4949

5050
export interface Screenshot {
5151
data: Uint8Array;
@@ -649,6 +649,9 @@ export const NATIVE: SentryNativeWrapper = {
649649
},
650650

651651
isNativeAvailable(): boolean {
652+
if (!RNSentry) {
653+
RNSentry = getRNSentryModule();
654+
}
652655
return this._isModuleLoaded(RNSentry);
653656
},
654657

packages/core/test/wrapper.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,4 +1208,38 @@ describe('Tests Native Wrapper', () => {
12081208
});
12091209
});
12101210
});
1211+
1212+
describe('isNativeAvailable', () => {
1213+
test('retries module resolution if initially undefined', () => {
1214+
// Simulate the race condition: RNSentry was undefined at module load time
1215+
// but becomes available when isNativeAvailable() is called during init()
1216+
let mockModule: Spec | undefined = undefined;
1217+
1218+
jest.resetModules();
1219+
jest.doMock('react-native', () => ({
1220+
NativeModules: {
1221+
get RNSentry() {
1222+
return mockModule;
1223+
},
1224+
},
1225+
Platform: { OS: 'ios' },
1226+
}));
1227+
// Ensure TurboModules path is not used so NativeModules.RNSentry is checked
1228+
jest.doMock('../src/js/utils/environment', () => ({
1229+
isTurboModuleEnabled: () => false,
1230+
}));
1231+
1232+
// eslint-disable-next-line @typescript-eslint/no-var-requires
1233+
const { NATIVE: isolatedNATIVE } = require('../src/js/wrapper');
1234+
1235+
// Initially unavailable (simulates race condition)
1236+
expect(isolatedNATIVE.isNativeAvailable()).toBe(false);
1237+
1238+
// Native module becomes available (TurboModule registered)
1239+
mockModule = RNSentry;
1240+
1241+
// isNativeAvailable retries and finds it
1242+
expect(isolatedNATIVE.isNativeAvailable()).toBe(true);
1243+
});
1244+
});
12111245
});

0 commit comments

Comments
 (0)