Skip to content

Commit 584ec0d

Browse files
antonisclaude
andauthored
fix(android): Settle initNativeReactNavigationNewFrameTracking promise (#6823)
* fix(android): Settle initNativeReactNavigationNewFrameTracking promise The Android implementation accepted the Promise but never settled it, so the JS TurboModule tracker's startup frame never popped. Every subsequent native crash was then tagged with the stale `turbo_module.method: initNativeReactNavigationNewFrameTracking` instead of the call in flight. Resolve the promise, mirroring iOS. Fixes #6821 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update PR reference * fix(android): Guard FragmentActivity cast so the promise always settles A brownfield / hybrid host can run a non-FragmentActivity, where `(FragmentActivity) getCurrentActivity()` throws ClassCastException. That would skip `promise.resolve(null)` and re-leak the TurboModule tracker frame — the same bug this PR fixes. Guard the cast with instanceof so a non-FragmentActivity skips fragment tracking gracefully instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bca399f commit 584ec0d

3 files changed

Lines changed: 90 additions & 3 deletions

File tree

‎CHANGELOG.md‎

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

1111
### Fixes
1212

13+
- Fix stale `turbo_module.*` tags on Android native crashes ([#6823](https://github.com/getsentry/sentry-react-native/pull/6823))
1314
- Forward image attachments to Spotlight instead of stripping them from the envelope ([#6818](https://github.com/getsentry/sentry-react-native/pull/6818))
1415
- Don't crash the iOS app when Sentry is initialized with an invalid DSN ([#6825](https://github.com/getsentry/sentry-react-native/pull/6825))
1516
- Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817))

‎packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,10 @@ private void initFragmentInitialFrameTracking() {
176176
final RNSentryReactFragmentLifecycleTracer fragmentLifecycleTracer =
177177
new RNSentryReactFragmentLifecycleTracer(buildInfo, emitNewFrameEvent, logger);
178178

179-
final @Nullable FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
180-
if (fragmentActivity != null) {
179+
final @Nullable Activity currentActivity = getCurrentActivity();
180+
if (currentActivity instanceof FragmentActivity) {
181181
final @Nullable FragmentManager supportFragmentManager =
182-
fragmentActivity.getSupportFragmentManager();
182+
((FragmentActivity) currentActivity).getSupportFragmentManager();
183183
if (supportFragmentManager != null) {
184184
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleTracer, true);
185185
}
@@ -188,6 +188,7 @@ private void initFragmentInitialFrameTracking() {
188188

189189
public void initNativeReactNavigationNewFrameTracking(Promise promise) {
190190
this.initFragmentInitialFrameTracking();
191+
promise.resolve(null);
191192
}
192193

193194
public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.sentry.react;
2+
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.ArgumentMatchers.anyBoolean;
5+
import static org.mockito.ArgumentMatchers.anyInt;
6+
import static org.mockito.ArgumentMatchers.anyString;
7+
import static org.mockito.ArgumentMatchers.isNull;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.never;
10+
import static org.mockito.Mockito.verify;
11+
import static org.mockito.Mockito.when;
12+
13+
import android.app.Activity;
14+
import android.content.pm.PackageInfo;
15+
import android.content.pm.PackageManager;
16+
import androidx.fragment.app.FragmentActivity;
17+
import androidx.fragment.app.FragmentManager;
18+
import com.facebook.react.bridge.Promise;
19+
import com.facebook.react.bridge.ReactApplicationContext;
20+
import org.junit.Before;
21+
import org.junit.Test;
22+
23+
/**
24+
* {@code initNativeReactNavigationNewFrameTracking} must settle its promise so the JS TurboModule
25+
* tracker pops the startup frame instead of leaking it for the process lifetime.
26+
*/
27+
public class RNSentryInitFrameTrackingTest {
28+
29+
private RNSentryModuleImpl module;
30+
private Promise promise;
31+
private ReactApplicationContext reactContext;
32+
33+
@Before
34+
public void setUp() throws Exception {
35+
reactContext = mock(ReactApplicationContext.class);
36+
PackageManager packageManager = mock(PackageManager.class);
37+
when(packageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo());
38+
when(reactContext.getPackageManager()).thenReturn(packageManager);
39+
when(reactContext.getPackageName()).thenReturn("com.test.app");
40+
module = new RNSentryModuleImpl(reactContext);
41+
promise = mock(Promise.class);
42+
}
43+
44+
@Test
45+
public void resolvesPromiseWhenNoCurrentActivity() {
46+
// No activity -> fragment tracking is skipped, but the promise must still settle.
47+
when(reactContext.getCurrentActivity()).thenReturn(null);
48+
49+
module.initNativeReactNavigationNewFrameTracking(promise);
50+
51+
verify(promise).resolve(isNull());
52+
verify(promise, never()).reject(anyString(), anyString());
53+
}
54+
55+
@Test
56+
public void resolvesPromiseAndStillRegistersFragmentCallbacks() {
57+
// Settling the promise must not skip the frame-tracking registration.
58+
FragmentActivity activity = mock(FragmentActivity.class);
59+
FragmentManager fragmentManager = mock(FragmentManager.class);
60+
when(activity.getSupportFragmentManager()).thenReturn(fragmentManager);
61+
when(reactContext.getCurrentActivity()).thenReturn(activity);
62+
63+
module.initNativeReactNavigationNewFrameTracking(promise);
64+
65+
verify(fragmentManager)
66+
.registerFragmentLifecycleCallbacks(
67+
any(FragmentManager.FragmentLifecycleCallbacks.class), anyBoolean());
68+
verify(promise).resolve(isNull());
69+
verify(promise, never()).reject(anyString(), anyString());
70+
}
71+
72+
@Test
73+
public void resolvesPromiseWhenActivityIsNotFragmentActivity() {
74+
// Brownfield / hybrid host: a plain Activity can't do fragment tracking. The
75+
// cast must not throw (ClassCastException would skip resolve and re-leak the
76+
// tracker frame) — tracking is skipped and the promise still settles.
77+
Activity plainActivity = mock(Activity.class);
78+
when(reactContext.getCurrentActivity()).thenReturn(plainActivity);
79+
80+
module.initNativeReactNavigationNewFrameTracking(promise);
81+
82+
verify(promise).resolve(isNull());
83+
verify(promise, never()).reject(anyString(), anyString());
84+
}
85+
}

0 commit comments

Comments
 (0)