Skip to content

Add API to extend app start (React Native) #6303

Description

@buenaflor

Rewrite RN's app start end customization onto a span-returning API consistent with Flutter 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.finishAppStart();
// 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.finishAppStart();

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").
  • finishAppStart(): void — finishes it; triggers finalization.
  • getExtendedAppStartSpan(): Span — returns the span for attaching children; a no-op span when there's no active extension.

finishAppStart().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.
  • finishAppStart: 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); 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 finishAppStart() 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.

Metrics hygiene (decide before the app.vitals.start migration)

  • Extended app starts skew the duration distribution. Tag them (an app.vitals.start.reason value or an extended: true flag) so they're separable from the p75/p90 baseline — land before the migration or it's a backfill.

RN specifics

  • Replace appLoaded() directly (experimental — no deprecation cycle) and fold in the already-deprecated captureAppStart(). Document the migration to extendAppStart / children / finishAppStart.
  • 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 finishAppStart(), producing the same trim-to-last-child end. Reuse the cached-response path that bypasses the native has_fetched: true guard.
  • Depends on app.start.cold / app.start.warm → allow nesting; or make it be controlled somehow #5936 (child nesting under app.start.cold / warm; currently Blocked).

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).
  • Child spans render under Extended App Start.
  • finishAppStart() no-ops if not extended or called twice; finalizes via the deferred-cancel path with transaction end = last child.
  • Exactly one transaction across all paths; deferred send cancelled, no duplicates.
  • Measurement = process start → last child, set at finalization.
  • Open children at finish → cancelled; on deadline → deadlineExceeded, snapped to end.
  • Deadline path: transaction captured, app.vitals.start suppressed.
  • Extended app starts carry a segmentation attribute.
  • appLoaded() replaced, captureAppStart() folded in, migration documented.
  • Docs updated.

Metadata

Metadata

Assignees

No one assigned
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions