digidem / digidem/comapeo-core-react-native

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

Open
#68 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

priority: P3 sentry
Dominant language
Kotlin
Stars
1
Forks
0
Avg merge
8h 24m
Merged PRs (30d)
9

Description

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

  • PR #63 — Phase 3 / 10 (this is the follow-up after offline transport + nanos fixes landed)
  • docs/sentry-integration-plan.md §7.4 — native instrumentation taxonomy. Update §7.4.2 to note cross-process trace propagation when this lands.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ComapeoCoreReactActivityLifecycleListener.actionOnService, ComapeoCoreService.onStartCommand, and NodeJSService.start, then inspect Actions.kt and the existing SentryFgsBridge boot flow. Verify the Android cold-start path and existing system-restart handling before making the change. Done means App Start, comapeo.boot, and boot.import-index share one trace_id in Sentry, with the documented §7.4.2 update.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin
Domain
mobile, observability
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.