diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a68490167..496a2f1cd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Fix stale `turbo_module.*` tags on Android native crashes ([#6823](https://github.com/getsentry/sentry-react-native/pull/6823)) - Forward image attachments to Spotlight instead of stripping them from the envelope ([#6818](https://github.com/getsentry/sentry-react-native/pull/6818)) - Don't crash the iOS app when Sentry is initialized with an invalid DSN ([#6825](https://github.com/getsentry/sentry-react-native/pull/6825)) - Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817)) 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..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); } @@ -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..61552adf00 --- /dev/null +++ b/packages/core/android/src/test/java/io/sentry/react/RNSentryInitFrameTrackingTest.java @@ -0,0 +1,85 @@ +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.app.Activity; +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()); + } + + @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()); + } +}