-
-
Notifications
You must be signed in to change notification settings - Fork 358
Description
What React Native libraries do you use?
Expo Router
Are you using sentry.io or on-premise?
sentry.io (SaS)
@sentry/react-native SDK Version
7.12.x
How does your development environment look like?
### Environment
@sentry/react-native | 7.12.1 (crash confirmed), 8.0.0 (same code path)
react-native | 0.81.5
expo | 54.0.33
New Architecture | Enabled (newArchEnabled: true)
iOS deployment target | 16.0 (IPHONEOS_DEPLOYMENT_TARGET = 16.0)
Device | iPhone 17 (iPhone17,2)
iOS version | 26.2.1
Build type | Release / TestFlight (does not reproduce in development builds)
Sentry.init()
Sentry.init({
dsn: 'https://...@sentry.io/...',
enabled: true,
tracesSampleRate: 0.1,
// No replaysSessionSampleRate
// No replaysOnErrorSampleRate
// No integrations: [mobileReplayIntegration()]});Steps to Reproduce
Create a React Native app using Expo with New Architecture enabled.
Set IPHONEOS_DEPLOYMENT_TARGET = 16.0 in the Xcode project (or via expo-build-properties: { ios: { deploymentTarget: '16.0' } }).
Add @sentry/react-native and initialize it without mobileReplayIntegration() and without any replay sample rates:
Sentry.init({
dsn: 'https://...@sentry.io/...',
enabled: true,
tracesSampleRate: 0.1,
// No replaysSessionSampleRate
// No replaysOnErrorSampleRate
// No integrations: [mobileReplayIntegration()]
});
Sentry.init({ dsn: 'https://...@sentry.io/...', enabled: true, tracesSampleRate: 0.1, // No replaysSessionSampleRate // No replaysOnErrorSampleRate // No integrations: [mobileReplayIntegration()]});
Build a release binary and distribute via TestFlight.
Launch the app on a physical device.
Expected: App launches normally. Sentry collects errors and breadcrumbs.
Actual: App crashes immediately on launch (EXC_CRASH / SIGABRT).
Crash Stack Trace
Thread 3 Crashed:
0 libsystem_kernel.dylib __pthread_kill + 8
1 libsystem_pthread.dylib pthread_kill + 268
2 libsystem_c.dylib abort + 124
3 libc++abi.dylib __abort_message + 132
4 libc++abi.dylib demangling_terminate_handler() + 280
5 libobjc.A.dylib _objc_terminate() + 172
6 libc++abi.dylib std::__terminate(void ()()) + 16
7 libc++abi.dylib __cxa_rethrow + 188
8 libobjc.A.dylib objc_exception_rethrow + 44
9 React ObjCTurboModule::performVoidMethodInvocation(
facebook::jsi::Runtime&, char const,
NSInvocation*, NSMutableArray*) + 192
(RCTTurboModule.mm:441)
10 React ObjCTurboModule::performVoidMethodInvocation(...)
::$_1::operator()() const
Thread 3 Crashed:0 libsystem_kernel.dylib __pthread_kill + 81 libsystem_pthread.dylib pthread_kill + 2682 libsystem_c.dylib abort + 1243 libc++abi.dylib __abort_message + 1324 libc++abi.dylib demangling_terminate_handler() + 2805 libobjc.A.dylib _objc_terminate() + 1726 libc++abi.dylib std::__terminate(void ()()) + 167 libc++abi.dylib __cxa_rethrow + 1888 libobjc.A.dylib objc_exception_rethrow + 449 React ObjCTurboModule::performVoidMethodInvocation( facebook::jsi::Runtime&, char const, NSInvocation*, NSMutableArray*) + 192 (RCTTurboModule.mm:441)10 React ObjCTurboModule::performVoidMethodInvocation(...) ::$_1::operator()() const
Thread 3 is a _dispatch_lane_serial_drain background thread. The crash happens approximately 0.4 seconds after launch.
Root Cause Analysis
- SENTRY_TARGET_REPLAY_SUPPORTED is gated on deployment target
In sentry-cocoa, SENTRY_TARGET_REPLAY_SUPPORTED evaluates to true when __IPHONE_OS_VERSION_MIN_REQUIRED >= 160000. Setting IPHONEOS_DEPLOYMENT_TARGET = 16.0 flips this macro on at compile time. - postInit is called unconditionally regardless of replay configuration
In RNSentryStart.m (v8) / RNSentry.mm (v7), after the SDK is started:
// RNSentryStart.m (v8.0.0, line 31-33)
#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay postInit];
#endif
// RNSentryStart.m (v8.0.0, line 31-33)#if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay postInit];#endif
This runs without checking whether session replay is actually enabled. The isSessionReplayEnabled flag (computed from replaysSessionSampleRate and replaysOnErrorSampleRate) is computed separately in createOptionsWithDictionary: but is never consulted before calling postInit. - postInit installs a breadcrumb converter that requires a live replay instance
// RNSentryReplay.mm (v7.12.1 and v8.0.0, line 65-72)
- (void)postInit
{
[PrivateSentrySDKOnly setRedactContainerClass:[RNSentryReplay getMaskClass]];
[PrivateSentrySDKOnly setIgnoreContainerClass:[RNSentryReplay getUnmaskClass]];
[RNSentryReplayBreadcrumbConverterHelper configureSessionReplayWithConverter];
}
// RNSentryReplay.mm (v7.12.1 and v8.0.0, line 65-72)+ (void)postInit{ [PrivateSentrySDKOnly setRedactContainerClass:[RNSentryReplay getMaskClass]]; [PrivateSentrySDKOnly setIgnoreContainerClass:[RNSentryReplay getUnmaskClass]]; [RNSentryReplayBreadcrumbConverterHelper configureSessionReplayWithConverter];}
configureSessionReplayWithConverter creates an RNSentryReplayBreadcrumbConverter and installs it as the global breadcrumb processor via [PrivateSentrySDKOnly configureSessionReplayWith:converter screenshotProvider:nil].
The converter's init calls [SentrySessionReplayIntegration createDefaultBreadcrumbConverter], which requires a running session replay instance. Since isSessionReplayEnabled = false (no sample rates configured), setEnableSessionReplayInUnreliableEnvironment:YES is never called, the session replay integration is not started, and the backing instance is nil/invalid.
- addBreadcrumb is a void TurboModule method with no @try/@catch
// RNSentry.mm (v7.12.1 and v8.0.0)
RCT_EXPORT_METHOD(addBreadcrumb : (NSDictionary *)breadcrumb)
{
[SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
[scope addBreadcrumb:[RNSentryBreadcrumb from:breadcrumb]];
}];
// ...
}
// RNSentry.mm (v7.12.1 and v8.0.0)RCT_EXPORT_METHOD(addBreadcrumb : (NSDictionary *)breadcrumb){ [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) { [scope addBreadcrumb:[RNSentryBreadcrumb from:breadcrumb]]; }]; // ...}
When Sentry auto-instruments (navigation events, console logs, network requests) and calls addBreadcrumb within milliseconds of initialization, the breadcrumb is processed through the installed RNSentryReplayBreadcrumbConverter. The converter throws an Objective-C exception because the session replay instance it references is nil.
In React Native's New Architecture (TurboModules), an ObjC exception thrown from a void method has nowhere to go — there is no Promise reject callback. The exception is caught by performVoidMethodInvocation and re-thrown via objc_exception_rethrow → __cxa_rethrow. With no outer catch handler, this propagates to std::terminate → abort → SIGABRT. - Why it only crashes in release builds
The Expo Dev Client initializes the TurboModule bridge differently in development and is more tolerant of native exceptions. Release builds use the full New Architecture TurboModule strict path.
The Conditional That Should Exist But Doesn't
The fix is straightforward: postInit should only run when session replay is actually enabled. The isSessionReplayEnabled boolean is already computed in the same call chain but is never used to guard postInit:
// v7 RNSentry.mm — isSessionReplayEnabled is computed but postInit is unconditional
#if SENTRY_TARGET_REPLAY_SUPPORTED
BOOL isSessionReplayEnabled = [RNSentryReplay updateOptions:mutableOptions];
#else
BOOL isSessionReplayEnabled = NO;
#endif
// ... later, postInit runs regardless:
#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay postInit]; // ← should be: if (isSessionReplayEnabled) { ... }
#endif
// v7 RNSentry.mm — isSessionReplayEnabled is computed but postInit is unconditional#if SENTRY_TARGET_REPLAY_SUPPORTED BOOL isSessionReplayEnabled = [RNSentryReplay updateOptions:mutableOptions];#else BOOL isSessionReplayEnabled = NO;#endif// ... later, postInit runs regardless:#if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay postInit]; // ← should be: if (isSessionReplayEnabled) { ... }#endif
The suggested fix (single line change in RNSentryStart.m for v8 / RNSentry.mm for v7):
// Before:
#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay postInit];
#endif
// After:
#if SENTRY_TARGET_REPLAY_SUPPORTED
if (isSessionReplayEnabled) {
[RNSentryReplay postInit];
}
#endif
// Before:#if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay postInit];#endif// After:#if SENTRY_TARGET_REPLAY_SUPPORTED if (isSessionReplayEnabled) { [RNSentryReplay postInit]; }#endif
Alternatively, the same guard can be placed inside postInit itself by checking PrivateSentrySDKOnly.options.sessionReplay at runtime.
Additional Context
The bug was triggered by bumping IPHONEOS_DEPLOYMENT_TARGET from 15.1 to 16.0. At 15.1, SENTRY_TARGET_REPLAY_SUPPORTED = false and postInit never ran, so the issue was invisible even though the code was always present.
The mobileReplayIntegration() was intentionally removed from this project previously because it was causing a different crash at deployment target 15.1 (where native replay support is not compiled in). So the developer cannot work around this by simply re-adding the integration without also verifying replay stability at 16.0.
The crash does not appear in Sentry (because Sentry itself is what's crashing before it can capture anything).
Other void TurboModule methods with the same lack of @try/@catch — setUser, setTag, setContext, clearBreadcrumbs, setExtra — may be susceptible to similar crashes if they interact with the partially-initialized replay converter.
Expected Result
No crash
Actual Result
Crash
Metadata
Metadata
Assignees
Labels
Projects
Status