feat(tracing): sync JS scope propagation context to native scope - #6686
Conversation
When a new JS root span starts, push its traceId, spanId, sampled, and sampleRand to the native SDK scope via a new bridge method. Native HTTP instrumentation (OkHttp on Android, URLSession on iOS) then attaches the correct sentry-trace header, linking native spans to the JS trace. Fixes #6237 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ridge modules - Add missing @ReactMethod wrapper in oldarch RNSentryModule - Add missing @OverRide wrapper in newarch RNSentryModule - Guard against null ctx/traceId/spanId in RNSentryModuleImpl - Pass null instead of sampled boolean as sample rate; the Java SDK expects the actual configured rate (0.0–1.0), not a boolean flag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update native context for all root spans including non-recording (unsampled) ones to prevent stale traceId lingering from a previous navigation - Make setCurrentScopePropagationContext synchronous on iOS (RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD) and Android (boolean return) to eliminate race with native URLSession/OkHttp - Update NativeRNSentry spec return type void→boolean to match synchronous codegen path - Document that iOS setTrace does not accept sampled/sampleRand (Cocoa SDK limitation) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ae93384. Configure here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ive scope sync - RNSentryInternal: reformat 32-char hex traceId to hyphenated UUID before passing to SentryId(uuidString:); without hyphens it silently produces an empty SentryId and trace linking breaks on iOS. - syncPropagationContextToNative: skip root spans that are not the active span so inactive forceTransaction roots (app start, expo-updates) do not clobber the native propagation context of an in-flight navigation span. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sync - span.ts: revert getActiveSpan() guard — spanStart fires before idle spans are made active, so the guard was skipping every navigation span and the feature never synced. All root spans now trigger a sync; the JSDoc explains the timing constraint. - RNSentryModuleImpl: read the JS `sampled` boolean and apply it via PropagationContext.setSampled() so OkHttp spans carry the correct isSampled decision, not just sampleRand. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| const ctx = span.spanContext(); | ||
| const propagationCtx = getCurrentScope().getPropagationContext(); | ||
| NATIVE.setCurrentScopePropagationContext({ | ||
| traceId: ctx.traceId, | ||
| spanId: ctx.spanId, | ||
| sampled: spanIsSampled(span), | ||
| sampleRand: propagationCtx.sampleRand ?? Math.random(), |
There was a problem hiding this comment.
Bug: Root spans created without startIdleSpan can cause a mismatched propagation context, combining a new traceId with a stale sampleRand, leading to incorrect native sampling.
Severity: MEDIUM
Suggested Fix
Ensure that code paths creating root spans, like startInactiveSpan({ forceTransaction: true }), explicitly update the propagation context with a new traceId and sampleRand before the span starts, similar to how startIdleSpan does. This will prevent syncPropagationContextToNative from using stale data.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/core/src/js/tracing/span.ts#L223-L229
Potential issue: For root spans not created via `startIdleSpan`, such as those for
app-start using `startInactiveSpan({ forceTransaction: true })`, the native scope
synchronization logic can create a mismatched propagation context. When
`syncPropagationContextToNative` is triggered, it uses the `traceId` from the new span
but may use a stale `sampleRand` value from the previous trace's scope. This sends a
mismatched context to the native layer, potentially causing incorrect sampling decisions
for downstream native HTTP calls.
There was a problem hiding this comment.
This looks valid 👍
Also these background roots come from startInactiveSpan({ forceTransaction: true }) (appStart.ts:556/1019, expoupdateslistener.ts:207/224) and can start mid navigation, overwriting the native trace that in-flight native HTTP spans link to, with no restore when they end. Refreshing their propagation context (the suggested fix) keeps the pushed values consistent but still links native calls to the wrong trace.
There was a problem hiding this comment.
The right fix would be to skip background/inactive roots (those created via startInactiveSpan that never become the active span). But distinguishing those from navigation roots at spanStart time is tricky so I will rather create a follow-up issue.
| } | ||
|
|
||
| @ReactMethod | ||
| @ReactMethod(isBlockingSynchronousMethod = true) |
There was a problem hiding this comment.
q: Why do we change this? I think it might degrade the performance
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ae37560+dirty | 470.40 ms | 564.12 ms | 93.72 ms |
| 40c9884+dirty | 430.40 ms | 497.76 ms | 67.36 ms |
| 7ff4d0f+dirty | 413.81 ms | 450.64 ms | 36.83 ms |
| 580fb5c+dirty | 436.34 ms | 471.63 ms | 35.28 ms |
| a5d243c+dirty | 424.52 ms | 485.18 ms | 60.66 ms |
| 4b87b12+dirty | 421.82 ms | 413.60 ms | -8.22 ms |
| bc8f61e+dirty | 442.98 ms | 506.56 ms | 63.58 ms |
| 774257e+dirty | 407.09 ms | 447.46 ms | 40.37 ms |
| 5257d80+dirty | 423.37 ms | 467.54 ms | 44.17 ms |
| 3817909+dirty | 406.67 ms | 416.58 ms | 9.91 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ae37560+dirty | 48.30 MiB | 53.60 MiB | 5.29 MiB |
| 40c9884+dirty | 49.74 MiB | 55.09 MiB | 5.34 MiB |
| 7ff4d0f+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 580fb5c+dirty | 49.74 MiB | 54.79 MiB | 5.05 MiB |
| a5d243c+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| 4b87b12+dirty | 43.75 MiB | 48.14 MiB | 4.39 MiB |
| bc8f61e+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 774257e+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 5257d80+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 3817909+dirty | 43.75 MiB | 48.08 MiB | 4.33 MiB |
📲 Install BuildsAndroid
|
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d200634+dirty | 3829.38 ms | 1224.08 ms | -2605.30 ms |
| 3b6e9f9+dirty | 3851.90 ms | 1233.33 ms | -2618.57 ms |
| 4e0b819+dirty | 3839.05 ms | 1210.75 ms | -2628.30 ms |
| 1e5d96d+dirty | 3851.45 ms | 1212.05 ms | -2639.41 ms |
| 0a9e622+dirty | 3835.87 ms | 1221.07 ms | -2614.80 ms |
| 7ac3378+dirty | 1213.37 ms | 1218.15 ms | 4.78 ms |
| 9ad4522+dirty | 3850.40 ms | 1222.40 ms | -2628.00 ms |
| 20fbd51+dirty | 3827.47 ms | 1213.32 ms | -2614.15 ms |
| d0e3b3e+dirty | 3854.77 ms | 1218.58 ms | -2636.19 ms |
| 822d35b+dirty | 3830.64 ms | 1209.20 ms | -2621.44 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d200634+dirty | 5.15 MiB | 6.89 MiB | 1.74 MiB |
| 3b6e9f9+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 4e0b819+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 1e5d96d+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 0a9e622+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 7ac3378+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 9ad4522+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| 20fbd51+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| d0e3b3e+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 822d35b+dirty | 4.98 MiB | 6.50 MiB | 1.53 MiB |
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 68ae91b+dirty | 3836.36 ms | 1222.04 ms | -2614.32 ms |
| 0b5a379+dirty | 3857.69 ms | 1230.34 ms | -2627.35 ms |
| bf168a4+dirty | 3846.83 ms | 1221.33 ms | -2625.49 ms |
| f170ec3+dirty | 3844.74 ms | 1222.67 ms | -2622.07 ms |
| 68672fc+dirty | 3832.22 ms | 1228.29 ms | -2603.93 ms |
| 84cba31+dirty | 3830.40 ms | 1204.44 ms | -2625.96 ms |
| 2c735cc+dirty | 1223.33 ms | 1224.38 ms | 1.04 ms |
| b9bebee+dirty | 3858.02 ms | 1231.92 ms | -2626.11 ms |
| a50b33d+dirty | 1207.11 ms | 1212.10 ms | 5.00 ms |
| 5569641+dirty | 3824.35 ms | 1210.78 ms | -2613.57 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 68ae91b+dirty | 4.98 MiB | 6.46 MiB | 1.48 MiB |
| 0b5a379+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| bf168a4+dirty | 4.98 MiB | 6.47 MiB | 1.49 MiB |
| f170ec3+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 68672fc+dirty | 5.15 MiB | 6.71 MiB | 1.55 MiB |
| 84cba31+dirty | 4.98 MiB | 6.55 MiB | 1.57 MiB |
| 2c735cc+dirty | 3.38 MiB | 4.74 MiB | 1.35 MiB |
| b9bebee+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| a50b33d+dirty | 3.38 MiB | 4.73 MiB | 1.35 MiB |
| 5569641+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
antonis
left a comment
There was a problem hiding this comment.
Left a couple of comments that should be checked but nothing blocking on my side 👍
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| f3215d3+dirty | 396.53 ms | 436.66 ms | 40.13 ms |
| d0e3b3e+dirty | 443.19 ms | 480.00 ms | 36.81 ms |
| a5d243c+dirty | 499.20 ms | 525.62 ms | 26.42 ms |
| 7fd0012+dirty | 422.41 ms | 445.57 ms | 23.17 ms |
| 0a147b2+dirty | 442.80 ms | 522.24 ms | 79.44 ms |
| d7d54c6+dirty | 423.54 ms | 498.56 ms | 75.02 ms |
| c823bb5+dirty | 468.26 ms | 516.16 ms | 47.90 ms |
| 5789645+dirty | 502.66 ms | 594.19 ms | 91.53 ms |
| 40c9884+dirty | 453.76 ms | 478.98 ms | 25.22 ms |
| d771cd5+dirty | 432.29 ms | 464.38 ms | 32.09 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| f3215d3+dirty | 48.30 MiB | 53.49 MiB | 5.19 MiB |
| d0e3b3e+dirty | 49.74 MiB | 55.09 MiB | 5.34 MiB |
| a5d243c+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| 7fd0012+dirty | 50.56 MiB | 56.46 MiB | 5.90 MiB |
| 0a147b2+dirty | 49.74 MiB | 55.08 MiB | 5.34 MiB |
| d7d54c6+dirty | 49.74 MiB | 55.09 MiB | 5.34 MiB |
| c823bb5+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 5789645+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
| 40c9884+dirty | 49.74 MiB | 55.09 MiB | 5.34 MiB |
| d771cd5+dirty | 49.74 MiB | 55.36 MiB | 5.62 MiB |
JS callers declare it void and never use the return value, so blocking the JS thread was unnecessary overhead.

📢 Type of change
📜 Description
Adds a new
setCurrentScopePropagationContextbridge method that pushes the active JS root span'straceId,spanId,sampled, andsampleRandto the native SDK scope whenever a new root span starts. Wired up automatically via aspanStarthook inreactNativeTracingIntegration.💡 Motivation and Context
Fixes #6237.
Native HTTP spans (OkHttp on Android, URLSession on iOS) were always orphaned — they had no knowledge of the active JS trace and ended up as standalone transactions in Sentry. After this change, native spans automatically share the same
trace_idas the JS navigation transaction that triggered them.syncPropagationContextToNative(client)registers aspanStarthook (root spans only) and callsNATIVE.setCurrentScopePropagationContext({ traceId, spanId, sampled, sampleRand }).PropagationContext.fromExistingTrace(traceId, spanId, sampled, sampleRand)applied viaSentry.configureScope.SentrySDK.internal.setTrace(_:spanId:)called throughRNSentryInternal.swiftusing the existing@_spi(Private)bridge.💚 How did you test it?
test/tracing/nativeScopeSync.test.ts: root span sync, child span skip,SentryNonRecordingSpanskip, sampled/sampleRand propagation, per-span firing.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
DSC/baggage propagation and iOS
sampledflag —SentrySDK.internal.setTracesets traceId+spanId only; follow up with sentry-cocoa once the API is extended.