Skip to content

Launch-screen TTID/TTFD inflated by pre-navigation time #6618

Description

@antonis

SDK versions

Regression can appear from ~8.3.0–8.5.0 onward, as app-start attachment to the first navigation transaction became reliable (#5756 in 8.3.0, #5833 in 8.5.0); symptom shaped/censored by the 30s cap added in 8.13.0. Baseline behavior last correct in 8.1.0.

Platform

iOS and Android (JS-layer mechanism, platform-agnostic; reproductions captured on Android).

Summary

When an app's first React-Navigation transaction fires well after process start (a delayed first navigation — splash / auth / loading / deferred nav), the app-start integration still attaches app-start data to it and rewrites the transaction's start_timestamp to process-init time. timeToDisplayIntegration then computes time_to_initial_display = ttid_end − event.start_timestamp, so TTID becomes process_init → first_frame and absorbs the entire pre-navigation gap. The launch screen's actual render is a few hundred ms, but the reported TTID is 20–30s.

This is not a change to the TTID formula — the rewrite code is byte-identical to 8.1.0. What changed is that app-start data now reliably reaches the first navigation transaction, which it did not in 8.1.0.

Evidence (launch screen, Android)

Three slow events, identical shape:

Root ui.load duration app_start_cold navigation.processing (real render) ui.load self_time (dead air)
20.04s 1.57s 186ms 18.15s
25.86s 2.37s 308ms 21.00s
29.75s 1.03s 30ms (same shape, 38 spans)

In every trace: cold start completes ≤2.4s → ~18–21s uninstrumented gap → navigation + real work in a thin sliver at the end. The ui.load span's own self_time (18–21s) is that gap.

Aggregate P95 by sdk.version, same screen/traffic:

  • time_to_initial_display: 906ms (8.1.0) → 23.40s (8.13.0)
  • app_start_cold: (not present on 8.1.0)2.13s (8.13.0) — app-start itself is healthy; the inflation is the pre-nav gap, not a large app start
  • app_start_warm: (not present on 8.1.0)1.07s (8.13.0)

The absence of app-start measurements on 8.1.0 is the direct confirmation that app-start attachment to this transaction is new.

Root cause

packages/core/src/js/tracing/integrations/appStart.ts

  • :727 event.start_timestamp = appStartTimestampSeconds; (process init)
  • :736-738 re-anchors the ui.load.initial_display span to app-start and recomputes time_to_initial_display
  • :684-685 age guard MAX_APP_START_AGE_MS = 60_000 — permits attachment when the first navigation is up to 60s after process-init

packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts

  • :51 transactionStartTimestampSeconds = event.start_timestamp
  • addAutomaticTimeToInitialDisplay: durationMs = (ttidTimestampSeconds − transactionStartTimestampSeconds) * 1000
  • appStartIntegration is registered before timeToDisplayIntegration (integrations/default.ts), so the start is already rewritten before TTID is computed.

Why it correlates with SDK version (enabler chain)

The rewrite existed in 8.1.0 but never fired on the launch screen (a premature route-less idle span absorbed the app-start lock, then was discarded). These fixes progressively routed app-start onto the first sampled, non-discarded navigation transaction:

PR Version Change
#5756 8.3.0 App start data not attached to sampled transactions when preceded by unsampled transactions
#5789 8.4.0 Defer initial navigation span creation until navigation container is registered
#5833 8.5.0 Fix app start data lost when first navigation transaction is discarded
#6051 8.10.0 Discard invalid navigation/interaction transactions via an event processor

The ~23s plateau

#6210 (8.13.0) added a >30s gate in timeToDisplayIntegration.ts (:18-19, :80-87) that drops inflated samples entirely. A distribution right-censored at 30s pushes high percentiles against the ceiling → observed ~23s P95. Before 8.13 the same inflation was reported uncapped.

Impact

Silently changes the meaning of a core mobile vital (launch-screen TTID/TTFD) for any app whose first navigation is delayed after process start. Corrupts P95 dashboards/alerting and makes auto-TTID incomparable across the 8.1→8.13 range, with no user-facing signal that the baseline moved.

Proposed fix direction

  • Compute/attach auto-TTID relative to the navigation transaction's own (dispatch) start, not the app-start-rewritten event.start_timestamp — e.g. snapshot the nav start before app-start attachment, or compute TTID before appStart's event processor runs.
  • Tighten MAX_APP_START_AGE_MS (and/or only attach app-start when the first navigation occurs within a small window of process-init) so a long-delayed first navigation doesn't inherit app start.
  • Consider not conflating "app cold start" with "first screen initial display" when they're separated by a large uninstrumented gap.

Reproduction

App with a delayed first React-Navigation navigation (splash / auth / loading taking more than ~5s before the first navigation transaction). Observe launch-screen time_to_initial_display ≈ process-init → first frame, with ui.load self_time ≈ the pre-navigation gap and navigation.processing ≈ the true render.

Amplifier: a high finalTimeoutMs on reactNativeTracingIntegration (e.g. finalTimeoutMs: 45000, idleTimeoutMs: 5000) lets the idle navigation transaction stay open long enough to encompass a large pre-navigation gap, making the inflation more pronounced.

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions