Rewrite RN's app start end customization onto a span-returning API consistent with Flutter, Android and Cocoa. Today appLoaded() only moves the end timestamp and returns nothing, so users can't break down extended launch work.
// Simple: extend the app start window, then finish
Sentry.extendAppStart();
await initializeRemoteConfig();
Sentry.finishExtendedAppStart();
// With child spans: instrument the extended region
Sentry.extendAppStart();
const span = Sentry.getExtendedAppStartSpan();
const configSpan = span.startChild({ op: 'app.init', description: 'fetch remote config' });
await loadRemoteConfig();
configSpan.finish();
Sentry.finishExtendedAppStart();
Reference: getsentry/sentry-cocoa#6886 (Done, SentryExtendedAppLaunchManager). Siblings: getsentry/sentry-dart#3767, getsentry/sentry-java#5553 — shared contract, RN-specific sections below.
API
extendAppStart(): void — creates the extended app start span (op app.start.extended_app_start, description "Extended App Start").
finishExtendedAppStart — finishes it (returns void); triggers finalization.
getExtendedAppStartSpan(): Span — returns the span for attaching children; a no-op span when there's no active extension.
finishExtendedAppStart is equivalent to calling .finish() on the returned span.
Lifecycle
extendAppStart: creates the span (start = call time). No-ops if app start already finished, if none is in progress, or on repeat calls (first wins).
getExtendedAppStartSpan: returns the extended span, else a no-op span.
finishExtendedAppStart: no-ops if not extended or called twice. Doesn't finalize directly — the parent (auto-generated, waitForChildren) finalizes when its last child finishes and trims its end to that child. Open children of the extended span are finished cancelled at the finish-call time.
Duration & measurement
- Parent duration = end of the last child to finish (trim-to-last-child), floored at the default app start end — i.e.
end = max(last child end, default app start end). Extending can only push the end later; finishing before the default end keeps the default (never shorter than a non-extended app start). The extended span is just one participant.
- App start measurement = that final duration (process start → last child), set at finalization. The normal completion-time measurement must be suppressed in extended mode so the early (non-extended) value doesn't win.
Timeout
- Parent's auto-finish deadline is 30s. If
finishExtendedAppStart is never called, the transaction auto-finishes on the deadline (unfinished children → deadlineExceeded, snapped to transaction end), is still captured, but the app.vitals.start measurement is suppressed — never emit a ~30s app start. Same for standalone and ui.load.
Standalone & non-standalone
- Both supported, same mechanism: standalone uses the app-start tracer, non-standalone the
ui.load transaction (also waitForChildren + deadline). In non-standalone the duration is bounded by anything keeping ui.load open (TTID, other children), so it can include spans unrelated to the extension.
RN specifics
- Replace
appLoaded() directly (experimental — no deprecation cycle) and fold in the already-deprecated captureAppStart(). Document the migration to extendAppStart / children / finishExtendedAppStart.
- RN's
waitForChildren equivalent is the cancel-deferred path: auto-capture schedules captureStandaloneAppStart() via setTimeout(…, 0); extendAppStart() must cancel that deferred send and keep the transaction open until finishExtendedAppStart is called, producing the same trim-to-last-child end. Reuse the cached-response path that bypasses the native has_fetched: true guard.
Acceptance criteria
Rewrite RN's app start end customization onto a span-returning API consistent with Flutter, Android and Cocoa. Today
appLoaded()only moves the end timestamp and returns nothing, so users can't break down extended launch work.Reference: getsentry/sentry-cocoa#6886 (Done,
SentryExtendedAppLaunchManager). Siblings: getsentry/sentry-dart#3767, getsentry/sentry-java#5553 — shared contract, RN-specific sections below.API
extendAppStart(): void— creates the extended app start span (opapp.start.extended_app_start, description"Extended App Start").finishExtendedAppStart— finishes it (returns void); triggers finalization.getExtendedAppStartSpan(): Span— returns the span for attaching children; a no-op span when there's no active extension.Lifecycle
extendAppStart: creates the span (start = call time). No-ops if app start already finished, if none is in progress, or on repeat calls (first wins).getExtendedAppStartSpan: returns the extended span, else a no-op span.finishExtendedAppStart: no-ops if not extended or called twice. Doesn't finalize directly — the parent (auto-generated,waitForChildren) finalizes when its last child finishes and trims its end to that child. Open children of the extended span are finishedcancelledat the finish-call time.Duration & measurement
end = max(last child end, default app start end). Extending can only push the end later; finishing before the default end keeps the default (never shorter than a non-extended app start). The extended span is just one participant.Timeout
finishExtendedAppStartis never called, the transaction auto-finishes on the deadline (unfinished children →deadlineExceeded, snapped to transaction end), is still captured, but theapp.vitals.startmeasurement is suppressed — never emit a ~30s app start. Same for standalone andui.load.Standalone & non-standalone
ui.loadtransaction (alsowaitForChildren+ deadline). In non-standalone the duration is bounded by anything keepingui.loadopen (TTID, other children), so it can include spans unrelated to the extension.RN specifics
appLoaded()directly (experimental — no deprecation cycle) and fold in the already-deprecatedcaptureAppStart(). Document the migration toextendAppStart/ children /finishExtendedAppStart.waitForChildrenequivalent is the cancel-deferred path: auto-capture schedulescaptureStandaloneAppStart()viasetTimeout(…, 0);extendAppStart()must cancel that deferred send and keep the transaction open untilfinishExtendedAppStartis called, producing the same trim-to-last-child end. Reuse the cached-response path that bypasses the nativehas_fetched: trueguard.Acceptance criteria
extendAppStart()creates the span before finish; no-ops when too late, not started, or called repeatedly.getExtendedAppStartSpan()returns the span, else a no-op span (including after finish).Extended App Start.finishExtendedAppStartno-ops if not extended or called twice; finalizes via the deferred-cancel path with transaction end = last child.cancelled; on deadline →deadlineExceeded, snapped to end.app.vitals.startsuppressed.appLoaded()replaced,captureAppStart()folded in, migration documented.