From 1d724bd6a3300dd2b2e00b2b5d24e26bddb23c53 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 2 Oct 2026 11:02:46 +0200 Subject: [PATCH 1/3] 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 --- CHANGELOG.md | 1 + .../io/sentry/react/RNSentryModuleImpl.java | 1 + .../react/RNSentryInitFrameTrackingTest.java | 70 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e22a82970..d3e8c81eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Fix stale `turbo_module.*` tags on Android native crashes ([#6821](https://github.com/getsentry/sentry-react-native/issues/6821)) - Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817)) ## 8.29.0 diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index e61da008d2..0b5ec9f6f4 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -188,6 +188,7 @@ private void initFragmentInitialFrameTracking() { public void initNativeReactNavigationNewFrameTracking(Promise promise) { this.initFragmentInitialFrameTracking(); + promise.resolve(null); } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { diff --git a/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java b/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java new file mode 100644 index 0000000000..fd92c8752a --- /dev/null +++ b/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java @@ -0,0 +1,70 @@ +package io.sentry.react; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import org.junit.Before; +import org.junit.Test; + +/** + * {@code initNativeReactNavigationNewFrameTracking} must settle its promise so the JS TurboModule + * tracker pops the startup frame instead of leaking it for the process lifetime. + */ +public class RNSentryInitFrameTrackingTest { + + private RNSentryModuleImpl module; + private Promise promise; + private ReactApplicationContext reactContext; + + @Before + public void setUp() throws Exception { + reactContext = mock(ReactApplicationContext.class); + PackageManager packageManager = mock(PackageManager.class); + when(packageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + when(reactContext.getPackageManager()).thenReturn(packageManager); + when(reactContext.getPackageName()).thenReturn("com.test.app"); + module = new RNSentryModuleImpl(reactContext); + promise = mock(Promise.class); + } + + @Test + public void resolvesPromiseWhenNoCurrentActivity() { + // No activity -> fragment tracking is skipped, but the promise must still settle. + when(reactContext.getCurrentActivity()).thenReturn(null); + + module.initNativeReactNavigationNewFrameTracking(promise); + + verify(promise).resolve(isNull()); + verify(promise, never()).reject(anyString(), anyString()); + } + + @Test + public void resolvesPromiseAndStillRegistersFragmentCallbacks() { + // Settling the promise must not skip the frame-tracking registration. + FragmentActivity activity = mock(FragmentActivity.class); + FragmentManager fragmentManager = mock(FragmentManager.class); + when(activity.getSupportFragmentManager()).thenReturn(fragmentManager); + when(reactContext.getCurrentActivity()).thenReturn(activity); + + module.initNativeReactNavigationNewFrameTracking(promise); + + verify(fragmentManager) + .registerFragmentLifecycleCallbacks( + any(FragmentManager.FragmentLifecycleCallbacks.class), anyBoolean()); + verify(promise).resolve(isNull()); + verify(promise, never()).reject(anyString(), anyString()); + } +} From 70d692c0cbb97bac6817e2ee7d2d2b0e5ecf9acd Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 2 Oct 2026 11:04:41 +0200 Subject: [PATCH 2/3] Update PR reference --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e8c81eed..6162d95df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Fix stale `turbo_module.*` tags on Android native crashes ([#6821](https://github.com/getsentry/sentry-react-native/issues/6821)) +- Fix stale `turbo_module.*` tags on Android native crashes ([#6823](https://github.com/getsentry/sentry-react-native/pull/6823)) - Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817)) ## 8.29.0 From dd4851f95d1b1b39339af95c99faacc204e7bb0c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 2 Oct 2026 13:46:36 +0200 Subject: [PATCH 3/3] fix(android): Guard FragmentActivity cast so the promise always settles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../java/io/sentry/react/RNSentryModuleImpl.java | 6 +++--- .../react/RNSentryInitFrameTrackingTest.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 0b5ec9f6f4..7d5c3c2731 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -176,10 +176,10 @@ private void initFragmentInitialFrameTracking() { final RNSentryReactFragmentLifecycleTracer fragmentLifecycleTracer = new RNSentryReactFragmentLifecycleTracer(buildInfo, emitNewFrameEvent, logger); - final @Nullable FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity(); - if (fragmentActivity != null) { + final @Nullable Activity currentActivity = getCurrentActivity(); + if (currentActivity instanceof FragmentActivity) { final @Nullable FragmentManager supportFragmentManager = - fragmentActivity.getSupportFragmentManager(); + ((FragmentActivity) currentActivity).getSupportFragmentManager(); if (supportFragmentManager != null) { supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleTracer, true); } diff --git a/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java b/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java index fd92c8752a..61552adf00 100644 --- a/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java +++ b/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Activity; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import androidx.fragment.app.FragmentActivity; @@ -67,4 +68,18 @@ public void resolvesPromiseAndStillRegistersFragmentCallbacks() { verify(promise).resolve(isNull()); verify(promise, never()).reject(anyString(), anyString()); } + + @Test + public void resolvesPromiseWhenActivityIsNotFragmentActivity() { + // Brownfield / hybrid host: a plain Activity can't do fragment tracking. The + // cast must not throw (ClassCastException would skip resolve and re-leak the + // tracker frame) — tracking is skipped and the promise still settles. + Activity plainActivity = mock(Activity.class); + when(reactContext.getCurrentActivity()).thenReturn(plainActivity); + + module.initNativeReactNavigationNewFrameTracking(promise); + + verify(promise).resolve(isNull()); + verify(promise, never()).reject(anyString(), anyString()); + } }