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
-
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) }
-
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)
-
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)
-
Constants for the intent extras in Actions.kt or alongside EXTRA_SERVICE_START_ELAPSED_MS.
-
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
Context
Cold-start currently produces three separate Sentry traces:
App Starttransaction (app.start.cold/app.start.warm), trace owned by@sentry/react-native.comapeo.boottransaction, trace owned by the FGS-processsentry-androidSDK (SentryFgsBridge).boot.loader-init,boot.import-index,boot.listen-control,boot.manager-init— inherit the FGS trace via--sentryTraceargv →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, callsSentry.continueTrace(...)beforeSentryFgsBridge.startBootTransaction, andcomapeo.bootbecomes a child transaction of RN's App Start on the same trace.Node still inherits via
--sentryTraceargv (no change there). All three layers land on a single trace.Implementation sketch
ComapeoCoreReactActivityLifecycleListener.actionOnService— before thestartForegroundService/startServicecall, read the current scope's propagation context and stamp the headers:ComapeoCoreService.onStartCommand— read both headers from the intent and forward toNodeJSService:NodeJSService.start— callSentry.continueTraceimmediately beforestartBootTransactionso the boot transaction inherits trace_id + parent_span_id:Constants for the intent extras in
Actions.ktor alongsideEXTRA_SERVICE_START_ELAPSED_MS.iOS is single-process so this is unnecessary there —
@sentry/react-native's scope is already shared withSentryNativeBridge's captures. No iOS change.Considerations
boot.kind: system-restart), no trace headers arrive;comapeo.bootstarts a fresh trace. Same fallback asboot.fgs-launchbeing skipped. Already covered by existing tag.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 skipscontinueTrace. Falls through to current behaviour (own trace).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-sidecomapeo.bootopens 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
App Start(RN-side) equals the trace_id ofcomapeo.boot(FGS-side) equals the trace_id ofboot.import-index(Node-side).Related
docs/sentry-integration-plan.md§7.4 — native instrumentation taxonomy. Update §7.4.2 to note cross-process trace propagation when this lands.