Skip to content

feat(tracing): sync JS scope propagation context to native scope - #6686

Open
alwx wants to merge 8 commits into
mainfrom
alwx/feature/scope-propagation
Open

feat(tracing): sync JS scope propagation context to native scope#6686
alwx wants to merge 8 commits into
mainfrom
alwx/feature/scope-propagation

Conversation

@alwx

@alwx alwx commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Adds a new setCurrentScopePropagationContext bridge method that pushes the active JS root span's traceId, spanId, sampled, and sampleRand to the native SDK scope whenever a new root span starts. Wired up automatically via a spanStart hook in reactNativeTracingIntegration.

💡 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_id as the JS navigation transaction that triggered them.

  • JS: syncPropagationContextToNative(client) registers a spanStart hook (root spans only) and calls NATIVE.setCurrentScopePropagationContext({ traceId, spanId, sampled, sampleRand }).
  • Android: PropagationContext.fromExistingTrace(traceId, spanId, sampled, sampleRand) applied via Sentry.configureScope.
  • iOS: SentrySDK.internal.setTrace(_:spanId:) called through RNSentryInternal.swift using the existing @_spi(Private) bridge.

💚 How did you test it?

  • 5 unit tests in test/tracing/nativeScopeSync.test.ts: root span sync, child span skip, SentryNonRecordingSpan skip, sampled/sampleRand propagation, per-span firing.
  • Full test suite: 1718 tests pass.

📝 Checklist

  • I added tests to verify changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.
  • No breaking changes.

🔮 Next steps

DSC/baggage propagation and iOS sampled flag — SentrySDK.internal.setTrace sets traceId+spanId only; follow up with sentry-cocoa once the API is extended.

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>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(tracing): sync JS scope propagation context to native scope by alwx in #6686
  • test(e2e): Evaluate replay assertion on Android by antonis in #6684
  • test(e2e): Restore iOS replay assertion in captureReplay test by antonis in #6683
  • feat(visionos): Add visionOS sample app by antonis in #6676
  • fix(core): Align beforeBreadcrumb and tracesSampler error fallbacks with spec by antonis in #6675
  • chore(replay): Mark mobileReplayIntegration as stable by antonis in #6679
  • feat(sample): Add tvOS sample app by antonis in #6677
  • fix(e2e): Pin json gem < 3.0 for RN < 0.72 iOS builds by antonis in #6678
  • feat(ios): Expose enableMemoryIntrospection option by antonis in #6674
  • feat(android): Add anrProfilingSampleRate option by antonis in #6673
  • ci: Unpin Android E2E emulator build by antonis in #6672
  • ref(ios): Remove deprecated private SDK API usage by philprime in #6647
  • chore(deps): update Cocoa SDK to v9.27.0 by github-actions in #6670
  • chore(deps): update Sentry Android Gradle Plugin to v6.21.0 by github-actions in #6671
  • chore(deps): Bump fast-uri to ^3.1.6 to resolve security alerts by antonis in #6662
  • chore(deps): Bump qs to ^6.16.0 to resolve security alerts by antonis in #6663
  • chore(deps): Bump @xmldom/xmldom pins to patched versions to resolve security alerts by antonis in #6664
  • docs: Revamp root and nested AGENTS.md by antonis in #6666

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor
Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 5590a26

alwx and others added 2 commits September 8, 2026 14:42
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alwx
alwx marked this pull request as ready for review September 8, 2026 12:45
Comment thread packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/js/tracing/span.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/core/ios/RNSentryInternal.swift
Comment thread packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java Outdated
Comment thread packages/core/src/js/tracing/reactnativetracing.ts
Comment thread packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java Outdated
…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>
Comment on lines +252 to +256
@_spi(Private) @objc public static func setCurrentScopePropagationContext(traceId: String, spanId: String) {
let sentryTraceId = SentryId(uuidString: traceId)
let sentrySpanId = SpanId(value: spanId)
SentrySDK.internal.setTrace(sentryTraceId, spanId: sentrySpanId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The iOS setCurrentScopePropagationContext implementation ignores sampled and sampleRand values, causing native spans to have different sampling decisions than the JavaScript trace.
Severity: MEDIUM

Suggested Fix

Update the setCurrentScopePropagationContext method in RNSentry.mm to extract sampled and sampleRand from the dictionary. Modify the setCurrentScopePropagationContext function in RNSentryInternal.swift to accept these new parameters and use them to create a PropagationContext, similar to the Android implementation, ensuring the sampling decision is correctly propagated.

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/ios/RNSentryInternal.swift#L252-L256

Potential issue: The iOS implementation of `setCurrentScopePropagationContext` only
accepts `traceId` and `spanId` parameters, silently ignoring the `sampled` and
`sampleRand` values passed from the JavaScript layer. This causes native iOS HTTP spans
to be sampled independently of the JavaScript trace's sampling decision. The
corresponding Swift function `setCurrentScopePropagationContext` only accepts `traceId`
and `spanId`, calling `SentrySDK.internal.setTrace` without any sampling information.
This undermines the feature's goal of unified trace sampling across platforms.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/core/ios/RNSentryInternal.swift
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alwx
alwx requested a review from a team as a code owner September 9, 2026 08:59
Comment thread packages/core/src/js/tracing/span.ts
Comment thread packages/core/src/js/tracing/span.ts

// Note: sampled and sampleRand from the JS propagation context are not applied here.
// SentrySDK.internal.setTrace only accepts traceId/spanId; wiring sampling fields
// through would require a sentry-cocoa API change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's open an issue/pr for the Cocoa API change to keep track of this case an fix it.

…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>
Comment on lines +217 to +230
export function syncPropagationContextToNative(client: Client): void {
client.on('spanStart', (span: Span) => {
if (!isRootSpan(span)) return;
if (getActiveSpan() !== span) return;
const ctx = span.spanContext();
const propagationCtx = getCurrentScope().getPropagationContext();
NATIVE.setCurrentScopePropagationContext({
traceId: ctx.traceId,
spanId: ctx.spanId,
sampled: spanIsSampled(span),
sampleRand: propagationCtx.sampleRand ?? Math.random(),
});
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spanStart active-span guard prevents native propagation sync

Drop the getActiveSpan() !== span check on spanStart (or sync after activation in startIdleSpan); spanStart fires from startInactiveSpan before the span is active, so this guard skips every real navigation/idle root and the feature never syncs.

Evidence
  • syncPropagationContextToNative() registers a spanStart listener that returns early unless getActiveSpan() === span (span.ts).
  • Production roots go through startIdleSpan()coreStartIdleSpan() / startInactiveSpan(), which emit spanStart during creation before the span is set active (idle navigation tests only assert getActiveSpan() after startIdleNavigationSpan returns).
  • nativeScopeSync.test.ts never exercises that path: positive cases manually withActiveSpan(span, () => client.emit('spanStart', span)), while plain startInactiveSpan({ forceTransaction: true }) is only used to assert the native call is skipped.
  • Result: navigation/user-interaction roots that should fix orphaned native HTTP spans never call NATIVE.setCurrentScopePropagationContext.
Also found at 1 additional location
  • packages/core/test/tracing/nativeScopeSync.test.ts:41-45

Identified by Warden · find-bugs · XSA-86X

Comment on lines +690 to +695
// sampled is a boolean on the JS side; the Java SDK expects the actual sample rate (0.0–1.0),
// which we don't have here, so we pass null and let sampleRand carry the sampling context.
Double sampleRand = ctx.hasKey("sampleRand") ? ctx.getDouble("sampleRand") : null;

PropagationContext propagationContext =
PropagationContext.fromExistingTrace(traceId, spanId, null, sampleRand);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android PropagationContext drops JS sampled decision

sampled is sent from JS but never applied; fromExistingTrace(..., null, sampleRand) leaves native isSampled unset, so OkHttp trace headers/spans can miss the JS sampling decision—sampleRand alone is not that decision. Read sampled and build a PropagationContext that sets isSampled (and keep sampleRand on baggage).

Evidence
  • JS syncPropagationContextToNative() calls NATIVE.setCurrentScopePropagationContext({ traceId, spanId, sampled: spanIsSampled(span), sampleRand }) in span.ts.
  • This Android method only validates/reads traceId, spanId, and optional sampleRand; it never reads ctx key sampled.
  • The call is PropagationContext.fromExistingTrace(traceId, spanId, null, sampleRand) with an explicit null third argument (sample rate per the in-code comment), so the created context does not receive the JS boolean sampling decision.
  • Native HTTP instrumentation uses scope propagation context for sentry-trace sampled state; with isSampled left unset, Android cannot reliably mirror the active JS root span’s sampled/unsampled decision.

Identified by Warden · find-bugs · 4BQ-AGU

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sync JS scope propagation context to native scope (link native spans to JS trace)

2 participants