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.
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_timestampto process-init time.timeToDisplayIntegrationthen computestime_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:
ui.loaddurationapp_start_coldnavigation.processing(real render)ui.loadself_time (dead air)In every trace: cold start completes ≤2.4s → ~18–21s uninstrumented gap → navigation + real work in a thin sliver at the end. The
ui.loadspan's ownself_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 startapp_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:727event.start_timestamp = appStartTimestampSeconds;(process init):736-738re-anchors theui.load.initial_displayspan to app-start and recomputestime_to_initial_display:684-685age guardMAX_APP_START_AGE_MS = 60_000— permits attachment when the first navigation is up to 60s after process-initpackages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts:51transactionStartTimestampSeconds = event.start_timestampaddAutomaticTimeToInitialDisplay:durationMs = (ttidTimestampSeconds − transactionStartTimestampSeconds) * 1000appStartIntegrationis registered beforetimeToDisplayIntegration(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:
The ~23s plateau
#6210 (8.13.0) added a
>30sgate intimeToDisplayIntegration.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
event.start_timestamp— e.g. snapshot the nav start before app-start attachment, or compute TTID beforeappStart's event processor runs.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.Reproduction
App with a delayed first React-Navigation navigation (splash / auth / loading taking more than ~5s before the first
navigationtransaction). Observe launch-screentime_to_initial_display≈ process-init → first frame, withui.loadself_time ≈ the pre-navigation gap andnavigation.processing≈ the true render.Amplifier: a high
finalTimeoutMsonreactNativeTracingIntegration(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.