diff --git a/CHANGELOG.md b/CHANGELOG.md index 39958f463d..89ce0f2d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes +- Retry native module resolution to prevent silent event drops in production Hermes builds ([#5981](https://github.com/getsentry/sentry-react-native/pull/5981)) - Lazy-load Metro internal modules to prevent Expo 55 import errors ([#5958](https://github.com/getsentry/sentry-react-native/pull/5958)) - 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)) - Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965)) diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index 6a71e0ff6f..1bee0fd232 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -45,7 +45,7 @@ export function getRNSentryModule(): Spec | undefined { : NativeModules.RNSentry; } -const RNSentry: Spec | undefined = getRNSentryModule(); +let RNSentry: Spec | undefined = getRNSentryModule(); export interface Screenshot { data: Uint8Array; @@ -649,6 +649,9 @@ export const NATIVE: SentryNativeWrapper = { }, isNativeAvailable(): boolean { + if (!RNSentry) { + RNSentry = getRNSentryModule(); + } return this._isModuleLoaded(RNSentry); }, diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index 0a546bb17b..ae6e3bf9e3 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -1208,4 +1208,38 @@ describe('Tests Native Wrapper', () => { }); }); }); + + describe('isNativeAvailable', () => { + test('retries module resolution if initially undefined', () => { + // Simulate the race condition: RNSentry was undefined at module load time + // but becomes available when isNativeAvailable() is called during init() + let mockModule: Spec | undefined = undefined; + + jest.resetModules(); + jest.doMock('react-native', () => ({ + NativeModules: { + get RNSentry() { + return mockModule; + }, + }, + Platform: { OS: 'ios' }, + })); + // Ensure TurboModules path is not used so NativeModules.RNSentry is checked + jest.doMock('../src/js/utils/environment', () => ({ + isTurboModuleEnabled: () => false, + })); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { NATIVE: isolatedNATIVE } = require('../src/js/wrapper'); + + // Initially unavailable (simulates race condition) + expect(isolatedNATIVE.isNativeAvailable()).toBe(false); + + // Native module becomes available (TurboModule registered) + mockModule = RNSentry; + + // isNativeAvailable retries and finds it + expect(isolatedNATIVE.isNativeAvailable()).toBe(true); + }); + }); });