Problem Statement
In a typical sentry-react-native app, the JS SDK owns the trace lifecycle: reactNavigationIntegration, manually started transactions, fetch / XHR instrumentation, etc. all create transactions and spans in JS. The native SDKs (sentry-android, sentry-cocoa) are initialized under the hood but are not informed about the active JS trace — there's no JS→native scope bridge.
Concretely, this means anything that emits spans on the native side cannot be linked to the JS trace:
SentryOkHttpInterceptor / SentryOkHttpEventListener for HTTP calls made from Kotlin or from third-party libraries that build their own OkHttpClient
- HTTP calls from React Native's internal
NetworkingModule (i.e. every fetch() from JS) when instrumented at the OkHttp layer via a custom OkHttpClientFactory
- iOS
URLSession auto-swizzled spans from sentry-cocoa
- Spans emitted from any custom native module
- Spans from third-party SDKs (e.g. Apollo, gRPC) that read the active Sentry scope on the native side
Today these all become orphaned standalone transactions in Sentry, sharing no trace_id with the JS transaction that triggered them.
A quick audit of the current native bridge confirms there is no API for this:
NativeRNSentry.ts exposes setActiveSpanId(spanId), but that is a red herring — it's only used by RNSentryTimeToDisplay for time-to-initial-display attribution and never touches Sentry's hub or scope.
- None of the
Sentry.configureScope { ... } call sites in RNSentryModuleImpl.java / RNSentry.mm set the propagation context or active transaction.
This is a real gap that customers hit anytime their app does meaningful native HTTP traffic.
Solution Brainstorm
Expose the scope propagation context across the bridge and have the JS SDK keep it in sync with the active JS root span automatically. The propagation context is already the canonical source of trace identity in modern Sentry SDKs (v8+), so this is a 1-1 mirror, not new semantics.
Rough shape
1. New TurboModule method (packages/core/src/js/NativeRNSentry.ts)
setCurrentScopePropagationContext(ctx: {
traceId: string;
parentSpanId?: string;
sampled?: boolean;
sampleRate?: number;
dsc?: UnsafeObject; // dynamic sampling context (baggage)
}): void;
2. Android impl (RNSentryModuleImpl.java)
Sentry.configureScope(scope -> {
scope.setPropagationContext(new PropagationContext(
new SentryId(traceId),
new SpanId(parentSpanId),
/* parent */ null,
Baggage.fromHeader(dscHeader),
sampled
));
});
3. iOS impl (RNSentry.mm)
[SentrySDK configureScope:^(SentryScope *scope) {
scope.propagationContext = [[SentryPropagationContext alloc]
initWithTraceId:traceId spanId:spanId sampled:sampled];
}];
4. JS auto-sync — hook into the JS client so this happens automatically without the user wiring anything:
client.on('spanStart', span => {
if (getRootSpan(span) !== span) return; // root spans only
const ctx = span.spanContext();
NATIVE.setCurrentScopePropagationContext({
traceId: ctx.traceId,
parentSpanId: ctx.spanId,
sampled: spanIsSampled(span),
dsc: getDynamicSamplingContextFromSpan(span),
});
});
Expected behavior after this lands
- Native HTTP spans (OkHttp on Android, URLSession on iOS) automatically share the same
trace_id as the JS navigation transaction that triggered them.
- In Sentry's Trace Explorer / waterfall, JS and native spans appear in the same trace — siblings under one trace, even if not nested under a single root span.
- Customers who want true nesting (native spans as children of a JS-started transaction) can additionally start a native transaction bound to scope, but the trace-level linkage works out of the box.
- iOS
URLSession instrumentation, which already reads the active propagation context, is fixed essentially for free.
Things to get right
- Bridge call frequency. Sync on root span start only — not every child span — to avoid bridge spam.
- DSC / baggage propagation. Without baggage, downstream services may resample inconsistently.
- Sampling decision propagation.
sampled must flow so head-based sampling stays consistent across layers.
autoInitializeNativeSdk: false. Buffer or no-op if native isn't ready yet.
- Tracing-disabled case. When
tracesSampleRate is 0, the propagation context still needs to flow so trace headers and standalone transactions get the right trace_id.
- Lifecycle. When the JS transaction ends, the propagation context can reasonably persist until the next root span starts — same as how scope-level propagation context already works in the JS SDK.
Context / repro
There is a minimal repro app demonstrating the current orphaning behavior here: https://github.com/thinkocapo/react-native-http-traces — NativeHttpRequest transactions show up in their own traces, not linked to the Route Change to /index JS trace.
Workarounds today require app developers to:
- Write a custom native module that wraps
Sentry.configureScope { it.propagationContext = ... } (Android) and the equivalent on iOS.
- Plumb the active JS
traceId / spanId over the bridge on every navigation.
This is boilerplate every non-trivial RN app ends up rewriting. It belongs in the SDK.
Problem Statement
In a typical
sentry-react-nativeapp, the JS SDK owns the trace lifecycle:reactNavigationIntegration, manually started transactions, fetch / XHR instrumentation, etc. all create transactions and spans in JS. The native SDKs (sentry-android, sentry-cocoa) are initialized under the hood but are not informed about the active JS trace — there's no JS→native scope bridge.Concretely, this means anything that emits spans on the native side cannot be linked to the JS trace:
SentryOkHttpInterceptor/SentryOkHttpEventListenerfor HTTP calls made from Kotlin or from third-party libraries that build their ownOkHttpClientNetworkingModule(i.e. everyfetch()from JS) when instrumented at the OkHttp layer via a customOkHttpClientFactoryURLSessionauto-swizzled spans from sentry-cocoaToday these all become orphaned standalone transactions in Sentry, sharing no
trace_idwith the JS transaction that triggered them.A quick audit of the current native bridge confirms there is no API for this:
NativeRNSentry.tsexposessetActiveSpanId(spanId), but that is a red herring — it's only used byRNSentryTimeToDisplayfor time-to-initial-display attribution and never touches Sentry's hub or scope.Sentry.configureScope { ... }call sites inRNSentryModuleImpl.java/RNSentry.mmset the propagation context or active transaction.This is a real gap that customers hit anytime their app does meaningful native HTTP traffic.
Solution Brainstorm
Expose the scope propagation context across the bridge and have the JS SDK keep it in sync with the active JS root span automatically. The propagation context is already the canonical source of trace identity in modern Sentry SDKs (v8+), so this is a 1-1 mirror, not new semantics.
Rough shape
1. New TurboModule method (
packages/core/src/js/NativeRNSentry.ts)2. Android impl (
RNSentryModuleImpl.java)3. iOS impl (
RNSentry.mm)4. JS auto-sync — hook into the JS client so this happens automatically without the user wiring anything:
Expected behavior after this lands
trace_idas the JS navigation transaction that triggered them.URLSessioninstrumentation, which already reads the active propagation context, is fixed essentially for free.Things to get right
sampledmust flow so head-based sampling stays consistent across layers.autoInitializeNativeSdk: false. Buffer or no-op if native isn't ready yet.tracesSampleRateis 0, the propagation context still needs to flow so trace headers and standalone transactions get the righttrace_id.Context / repro
There is a minimal repro app demonstrating the current orphaning behavior here: https://github.com/thinkocapo/react-native-http-traces —
NativeHttpRequesttransactions show up in their own traces, not linked to theRoute Change to /indexJS trace.Workarounds today require app developers to:
Sentry.configureScope { it.propagationContext = ... }(Android) and the equivalent on iOS.traceId/spanIdover the bridge on every navigation.This is boilerplate every non-trivial RN app ends up rewriting. It belongs in the SDK.