Problem
App start data (cold/warm start measurements) is currently attached to the first sampled navigation transaction as a carrier. This coupling creates several structural problems.
1. Fragile lifecycle dependency
App start data depends on the first navigation transaction surviving its entire lifecycle. That transaction has its own complex lifecycle:
- Idle timeout / heartbeat expiry
- Discard logic (
ignoreEmptyRouteChangeTransactions, ignoreEmptyBackNavigation)
- Timing-dependent navigation container registration (
registerNavigationContainer)
Any of these can cause the transaction to be dropped, silently discarding app start data with it. This is the root cause of #5831 (fixed in getsentry/sentry-react-native#5833 as a targeted workaround).
2. Inconsistent sampling behavior
App start data is only sent when a sampled transaction exists to carry it:
tracesSampleRate: 1.0 → works reliably
tracesSampleRate: 0.1 → app start attaches to the first sampled transaction, which may not be the first navigation event
tracesSampleRate: 0 → app start is never sent, even when enableAppStartTracking: true
A user who explicitly enables enableAppStartTracking likely expects to receive this data regardless of general tracing sample rate. The current design silently drops it.
3. Semantic mismatch between carrier and data
App start measurements can end up attached to any root span that happens to be first and sampled — not necessarily a navigation transaction. This makes the data harder to query consistently in Sentry.
Current workaround
getsentry/sentry-react-native#5833 added a lazy-check mechanism: if the locked navigation transaction is later found to be unsampled, the integration falls through to the next transaction. This fixes the specific regression in #5831 but doesn't address the structural coupling.
Proposed direction
Decouple app start data from the navigation transaction lifecycle. Possible approaches:
- Dedicated app start transaction — Send app start as its own transaction (similar to the existing
captureStandaloneAppStart path), always when enableAppStartTracking: true, respecting a separate or elevated sample rate.
- Guaranteed attachment — Attach app start to the first root span regardless of
_sampled status, so sampling decisions don't silently discard it.
- Session-level metric — Explore whether app start fits better as a session-attached metric rather than a transaction measurement.
Considerations
- Any change to sampling behavior for app start may be a breaking change for users who set
tracesSampleRate: 0 to opt out of performance data entirely.
- The
captureStandaloneAppStart path already exists and may be a useful foundation.
- Coordination with other mobile SDKs (Flutter, MAUI, Unity) if the approach is standardized.
Target span spec (Span V2 / EAP)
Whichever approach above is chosen, the standalone app start transaction should use the V2 spec:
- Transaction / span op:
app.start
- Span name / description:
App Start
- Attributes:
app.vitals.start.value — app start duration
app.vitals.start.type — cold / warm
This is the V2/EAP attribute-based encoding. Relay backfills app.vitals.start.value / app.vitals.start.type (and app.vitals.start.screen) for V1 transactions from the legacy app_start_cold / app_start_warm measurements and transaction name, so the SDK should emit the V2 attributes directly. Do not use the legacy per-type op (app.start.cold / app.start.warm); RN's current standalone mode emits op: ui.load, which should migrate to app.start.
Open question: emit app.vitals.start.screen directly, or rely on Relay's backfill from the transaction name?
This matches the Flutter spec (getsentry/sentry-dart#3634). Android (getsentry/sentry-java#5046) and Cocoa (getsentry/sentry-cocoa#6883) shipped the standalone-transaction behavior but using the legacy per-type op encoding.
Problem
App start data (cold/warm start measurements) is currently attached to the first sampled navigation transaction as a carrier. This coupling creates several structural problems.
1. Fragile lifecycle dependency
App start data depends on the first navigation transaction surviving its entire lifecycle. That transaction has its own complex lifecycle:
ignoreEmptyRouteChangeTransactions,ignoreEmptyBackNavigation)registerNavigationContainer)Any of these can cause the transaction to be dropped, silently discarding app start data with it. This is the root cause of #5831 (fixed in getsentry/sentry-react-native#5833 as a targeted workaround).
2. Inconsistent sampling behavior
App start data is only sent when a sampled transaction exists to carry it:
tracesSampleRate: 1.0→ works reliablytracesSampleRate: 0.1→ app start attaches to the first sampled transaction, which may not be the first navigation eventtracesSampleRate: 0→ app start is never sent, even whenenableAppStartTracking: trueA user who explicitly enables
enableAppStartTrackinglikely expects to receive this data regardless of general tracing sample rate. The current design silently drops it.3. Semantic mismatch between carrier and data
App start measurements can end up attached to any root span that happens to be first and sampled — not necessarily a navigation transaction. This makes the data harder to query consistently in Sentry.
Current workaround
getsentry/sentry-react-native#5833 added a lazy-check mechanism: if the locked navigation transaction is later found to be unsampled, the integration falls through to the next transaction. This fixes the specific regression in #5831 but doesn't address the structural coupling.
Proposed direction
Decouple app start data from the navigation transaction lifecycle. Possible approaches:
captureStandaloneAppStartpath), always whenenableAppStartTracking: true, respecting a separate or elevated sample rate._sampledstatus, so sampling decisions don't silently discard it.Considerations
tracesSampleRate: 0to opt out of performance data entirely.captureStandaloneAppStartpath already exists and may be a useful foundation.Target span spec (Span V2 / EAP)
Whichever approach above is chosen, the standalone app start transaction should use the V2 spec:
app.startApp Startapp.vitals.start.value— app start durationapp.vitals.start.type—cold/warmThis is the V2/EAP attribute-based encoding. Relay backfills
app.vitals.start.value/app.vitals.start.type(andapp.vitals.start.screen) for V1 transactions from the legacyapp_start_cold/app_start_warmmeasurements and transaction name, so the SDK should emit the V2 attributes directly. Do not use the legacy per-type op (app.start.cold/app.start.warm); RN's current standalone mode emitsop: ui.load, which should migrate toapp.start.Open question: emit
app.vitals.start.screendirectly, or rely on Relay's backfill from the transaction name?This matches the Flutter spec (getsentry/sentry-dart#3634). Android (getsentry/sentry-java#5046) and Cocoa (getsentry/sentry-cocoa#6883) shipped the standalone-transaction behavior but using the legacy per-type op encoding.