Skip to content

Sentry: propagate RN trace_id to FGS so cold-start spans land on one trace #68

@gmaclennan

Description

@gmaclennan

Context

Cold-start currently produces three separate Sentry traces:

  • RN side: App Start transaction (app.start.cold / app.start.warm), trace owned by @sentry/react-native.
  • FGS side: comapeo.boot transaction, trace owned by the FGS-process sentry-android SDK (SentryFgsBridge).
  • Node side: boot.loader-init, boot.import-index, boot.listen-control, boot.manager-init — inherit the FGS trace via --sentryTrace argv → Sentry.continueTrace.

Result: in Sentry's Performance view you can find RN's App Start or the comapeo.boot trace, but not the cold-start end-to-end on a single timeline. Comparing "what was holding up first paint — RN or Node?" requires manually correlating timestamps across two traces.

Proposal

Stamp the activity's current sentry-trace + baggage headers on the start-service intent. The FGS picks them up in onStartCommand, calls Sentry.continueTrace(...) before SentryFgsBridge.startBootTransaction, and comapeo.boot becomes a child transaction of RN's App Start on the same trace.

Node still inherits via --sentryTrace argv (no change there). All three layers land on a single trace.

Implementation sketch

  1. ComapeoCoreReactActivityLifecycleListener.actionOnService — before the startForegroundService / startService call, read the current scope's propagation context and stamp the headers:

    val scopes = io.sentry.Sentry.getCurrentScopes()
    val sentryTrace = scopes.propagationContext.toSentryTrace().value
    val baggage = scopes.propagationContext.toBaggage()?.toHeaderString(null)
    intent.putExtra(EXTRA_SENTRY_TRACE, sentryTrace)
    baggage?.let { intent.putExtra(EXTRA_SENTRY_BAGGAGE, it) }
  2. ComapeoCoreService.onStartCommand — read both headers from the intent and forward to NodeJSService:

    nodeJSService.parentSentryTrace = intent?.getStringExtra(EXTRA_SENTRY_TRACE)
    nodeJSService.parentSentryBaggage = intent?.getStringExtra(EXTRA_SENTRY_BAGGAGE)
  3. NodeJSService.start — call Sentry.continueTrace immediately before startBootTransaction so the boot transaction inherits trace_id + parent_span_id:

    val parentTrace = parentSentryTrace
    if (parentTrace != null) {
      io.sentry.Sentry.continueTrace(parentTrace, parentSentryBaggage?.let { listOf(it) })
    }
    val tx = SentryFgsBridge.startBootTransaction(backdatedStart, bootKind)
  4. Constants for the intent extras in Actions.kt or alongside EXTRA_SERVICE_START_ELAPSED_MS.

  5. iOS is single-process so this is unnecessary there — @sentry/react-native's scope is already shared with SentryNativeBridge's captures. No iOS change.

Considerations

  • System-restart case. When Android restarts the FGS without an intent (already handled today — boot.kind: system-restart), no trace headers arrive; comapeo.boot starts a fresh trace. Same fallback as boot.fgs-launch being skipped. Already covered by existing tag.
  • Cold start before RN init. If the FGS launches before RN has called initSentry(), Sentry.getCurrentScopes() returns a no-op hub and headers will be empty/null. The intent stamping should null-guard so we don't write garbage headers; the FGS-side null-check then skips continueTrace. Falls through to current behaviour (own trace).
  • Sampling. RN's App Start transaction is force-sampled (sampled by AppStart integration). If RN inherits a non-sampled trace... actually irrelevant — RN is the root.
  • Trace continuity past App Start. RN's App Start transaction finishes within a couple of seconds. After it ends, the propagation context still holds the trace_id for subsequent activity. FGS-side comapeo.boot opens during this window so it inherits the right trace. Verified: even after App Start finishes, the propagation_context.trace_id persists on the scope.

Acceptance criteria

  • On Android cold start, the trace_id of App Start (RN-side) equals the trace_id of comapeo.boot (FGS-side) equals the trace_id of boot.import-index (Node-side).
  • Searching Sentry by either side's trace_id surfaces all three transactions in the Performance > Trace view.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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