Skip to content

[Android] Unbounded ui.load.initial_display end timestamp inflates navigation transaction duration to days when ScreenStackFragment draws late #6208

@InterstellarStella

Description

@InterstellarStella

Filed by Sentry Support on behalf of a customer. Internal reference: Intercom conversation.

What React Native libraries do you use?
React Navigation, Hermes, RN New Architecture, Expo (mobile only), Expo Application Services (EAS)

Are you using sentry.io or on-premise?
sentry.io (SaaS)

Are you using any other error monitoring solution alongside Sentry?
No

@sentry/react-native SDK Version
8.2.0. Also reproduces against the current develop branch.

How does your development environment look like?

React Native: 0.83.6
Hermes: 0.14.1 (enabled)
New Architecture: Fabric + Turbo Modules (enabled)
Expo: yes
Target: Android (observed in production)

Sentry.init()

Sentry.init({
  // ...
  integrations: [
    Sentry.reactNativeTracingIntegration(),
    Sentry.reactNavigationIntegration({
      enableTimeToInitialDisplay: true,
      useDispatchedActionData: true,
    }),
  ],
});

Steps to Reproduce

Navigation hierarchy:

NavigationContainer
└── Stack
    └── Tab
        └── Stack
            └── ScreenA (initial route)
  1. Cold start the app on Android with the navigation hierarchy above and enableTimeToInitialDisplay: true on reactNavigationIntegration.
  2. Navigate into a sibling screen inside the inner stack so ScreenA's ScreenStackFragment view is destroyed or freezeOnBlur'd but the fragment remains cached behind the tab.
  3. Background the app. With Hermes on Android the JS thread is suspended.
  4. Foreground the app some time later (minutes to days).
  5. POP back to ScreenA. The fragment view is recreated, RNSentryReactFragmentLifecycleTracer.onFragmentViewCreated re-attaches its ScreenAppearEvent listener, the event fires, and FirstDrawDoneListener.registerForNextDraw captures the current draw timestamp into RNSentryTimeToDisplay against the currently-active span ID.
  6. The JS-side idle navigation span finishes with sentry.idle_span_finish_reason: idleTimeout.
  7. timeToDisplayIntegration.processEvent reads the stored TTID via popTimeToDisplayFor and unconditionally extends event.timestamp = max(ttid.timestamp, ttfd.timestamp, event.timestamp).

Expected Result

  • The ui.load.initial_display end timestamp is bounded. If the captured TTID is past a sensible deadline relative to start_timestamp, the SDK either drops the TTID, marks it deadline_exceeded, or refuses to extend event.timestamp with it.
  • The root navigation transaction duration reflects the actual screen render time, not the wall-clock elapsed since navigation started.

Actual Result

The root navigation transaction is sent with a duration of hours or days. The captured TTID has the same start/end as the root, and event.timestamp is extended to match the TTID end. Trace excerpt:

{
  "op": "navigation",
  "start_timestamp": 1778936730.088075,
  "timestamp": 1779346256.41,
  "data": {
    "sentry.idle_span_finish_reason": "idleTimeout",
    "navigation.action_type": "POP",
    "route.name": "ScreenA"
  }
}
{
  "op": "ui.load.initial_display",
  "description": "com.swmansion.rnscreens.ScreenStackFragment initial display",
  "origin": "auto.ui.time_to_display",
  "start_timestamp": 1778936730.088075,
  "timestamp": 1779346256.41
}

The navigation.processing child closed at start + 29 ms. All http.client children closed within roughly 1 s. Only the TTID is pathological. Observed at production scale on Sentry SaaS as a small but non-trivial fraction (well under 1%) of navigation transactions exceeding 1 hour duration, 100% on Android, concentrated on screens behind a Stack > Tab > Stack shape.

Workaround

Sentry.init({
  // ...
  beforeSendTransaction(event) {
    const start = event.start_timestamp ?? 0;
    const end = event.timestamp ?? 0;
    if (event.contexts?.trace?.op === 'navigation' && end - start > 60) {
      return null;
    }
    return event;
  },
});

Safe on 8.2.0 and on develop, no effect on healthy traces, immediately stops inflated transactions from polluting Sentry aggregates and alerts.

Metadata

Metadata

Assignees

No fields configured for issues without a type.

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions