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, 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

  • 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).
  • Custom child spans render under Extended App Start.
  • finishExtendedAppStart no-ops if not extended or called twice; finalizes via the deferred-cancel path with transaction end = last child.
  • Measurement = process start → last child, floored at the default app start end (never shorter than a non-extended app start), 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