From 77a2d2d9ee886ec1bdce6071faf1afa16acefee9 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 9 Sep 2026 20:57:00 +0300 Subject: [PATCH 1/2] fix(android): identify the screen appear event by name instead of class --- CHANGELOG.md | 1 + .../rnscreens/events/ScreenAppearEvent.kt | 14 ++++ ...NSentryReactFragmentLifecycleTracerTest.kt | 67 +++++++++++++++++++ .../RNSentryReactFragmentLifecycleTracer.java | 4 +- 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/com/swmansion/rnscreens/events/ScreenAppearEvent.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8436b20dec..f7874b5c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes +- Time to initial display is no longer understated on Android in optimized release builds ([#6692](https://github.com/getsentry/sentry-react-native/pull/6692)) - Fix visionOS compilation ([#6676](https://github.com/getsentry/sentry-react-native/pull/6676)) - A throwing `beforeBreadcrumb` now drops the breadcrumb, and a throwing `tracesSampler` now falls back to the configured `tracesSampleRate` ([#6675](https://github.com/getsentry/sentry-react-native/pull/6675)) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/com/swmansion/rnscreens/events/ScreenAppearEvent.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/com/swmansion/rnscreens/events/ScreenAppearEvent.kt new file mode 100644 index 0000000000..94cccc4e16 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/com/swmansion/rnscreens/events/ScreenAppearEvent.kt @@ -0,0 +1,14 @@ +package com.swmansion.rnscreens.events + +import com.facebook.react.uimanager.events.Event + +/** + * Stands in for the react-native-screens event, so a test can dispatch an event that carries this + * class name while reporting a different event name. That is what R8 produces when it merges the + * event classes of a release build into one. + */ +class ScreenAppearEvent( + private val name: String, +) : Event() { + override fun getEventName(): String = name +} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt index c636a60a54..499aae0475 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReactFragmentLifecycleTracerTest.kt @@ -5,10 +5,14 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.EventDispatcherListener import com.swmansion.rnscreens.ScreenStackFragment +import com.swmansion.rnscreens.events.ScreenAppearEvent import io.sentry.ILogger import io.sentry.android.core.BuildInfoProvider +import io.sentry.android.core.internal.util.FirstDrawDoneListener import io.sentry.react.RNSentryReactFragmentLifecycleTracer import org.junit.After import org.junit.Test @@ -18,7 +22,9 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.MockedStatic import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -41,6 +47,48 @@ class RNSentryReactFragmentLifecycleTracerTest { verify(mockEventDispatcher, times(1)).addListener(any()) } + @Test + fun tracerListensForFirstDrawOnScreenAppearEvent() { + val mockEventDispatcher = mock() + mockUIManager(mockEventDispatcher) + + mockStatic(FirstDrawDoneListener::class.java).use { firstDrawDoneListener -> + callOnFragmentViewCreated(mock(), mockScreenViewWithReactContext()) + + dispatchEventNamed("topAppear", mockEventDispatcher) + + firstDrawDoneListener.verify { + FirstDrawDoneListener.registerForNextDraw( + any(View::class.java), + any(Runnable::class.java), + any(BuildInfoProvider::class.java), + ) + } + verify(mockEventDispatcher, times(1)).removeListener(any()) + } + } + + @Test + fun tracerDoesNotListenForFirstDrawOnOtherEvents() { + val mockEventDispatcher = mock() + mockUIManager(mockEventDispatcher) + + mockStatic(FirstDrawDoneListener::class.java).use { firstDrawDoneListener -> + callOnFragmentViewCreated(mock(), mockScreenViewWithReactContext()) + + dispatchEvent(ScreenAppearEvent("topWillAppear"), mockEventDispatcher) + + firstDrawDoneListener.verify({ + FirstDrawDoneListener.registerForNextDraw( + any(View::class.java), + any(Runnable::class.java), + any(BuildInfoProvider::class.java), + ) + }, never()) + verify(mockEventDispatcher, never()).removeListener(any()) + } + } + @Test fun tracerRemovesListenerWhenFragmentViewDestroyed() { val mockEventDispatcher = mock() @@ -119,6 +167,25 @@ class RNSentryReactFragmentLifecycleTracerTest { tracer.onFragmentViewDestroyed(mock(), mockFragment) } + private fun dispatchEventNamed( + eventName: String, + mockEventDispatcher: EventDispatcher, + ) = dispatchEvent( + mock> { + whenever(it.eventName).thenReturn(eventName) + }, + mockEventDispatcher, + ) + + private fun dispatchEvent( + event: Event<*>, + mockEventDispatcher: EventDispatcher, + ) { + val listener = argumentCaptor() + verify(mockEventDispatcher, times(1)).addListener(listener.capture()) + listener.firstValue.onEventDispatch(event) + } + private fun createSutWith(): RNSentryReactFragmentLifecycleTracer { val logger: ILogger = mock() val buildInfo = BuildInfoProvider(logger) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java index c8cbc4bc88..b0c134cc4c 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java @@ -22,6 +22,7 @@ import org.jetbrains.annotations.Nullable; public class RNSentryReactFragmentLifecycleTracer extends FragmentLifecycleCallbacks { + private static final String SCREEN_APPEAR_EVENT_NAME = "topAppear"; private @NotNull final BuildInfoProvider buildInfoProvider; private @NotNull final Runnable emitNewFrameEvent; @@ -90,8 +91,7 @@ public void onFragmentViewCreated( new EventDispatcherListenerWrapper(eventDispatcher) { @Override public void onEventDispatch(Event event) { - if ("com.swmansion.rnscreens.events.ScreenAppearEvent" - .equals(event.getClass().getCanonicalName())) { + if (SCREEN_APPEAR_EVENT_NAME.equals(event.getEventName())) { this.dispatcher.removeListener(this); listenerWrapperMap.remove(f); FirstDrawDoneListener.registerForNextDraw(v, emitNewFrameEvent, buildInfoProvider); From 95843cce6c37df6e77ba3d7e51dcd9d4d8980e5d Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 10 Sep 2026 12:14:32 +0300 Subject: [PATCH 2/2] docs(android): explain the screen appear event name constant --- .../io/sentry/react/RNSentryReactFragmentLifecycleTracer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java index b0c134cc4c..7d9560935e 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryReactFragmentLifecycleTracer.java @@ -22,6 +22,10 @@ import org.jetbrains.annotations.Nullable; public class RNSentryReactFragmentLifecycleTracer extends FragmentLifecycleCallbacks { + /** + * The name react-native-screens uses for its screen appear event. Matching on the class name + * instead would break, because R8 can merge several event classes into one. + */ private static final String SCREEN_APPEAR_EVENT_NAME = "topAppear"; private @NotNull final BuildInfoProvider buildInfoProvider;