@@ -2,8 +2,10 @@ import type { Context, SpanContext, TextMapGetter, TextMapPropagator, TextMapSet
22import { context , trace , TraceFlags } from '@opentelemetry/api' ;
33import type { continueTrace , DynamicSamplingContext } from '@sentry/core' ;
44import {
5+ _INTERNAL_safeMathRandom ,
56 baggageHeaderToDynamicSamplingContext ,
67 consoleSandbox ,
8+ generateTraceId ,
79 getClient ,
810 getCurrentScope ,
911 getIsolationScope ,
@@ -122,16 +124,36 @@ export function continueTraceAsRemoteSpan<T>(
122124 * so it can be used to implement an OpenTelemetry propagator's `extract`.
123125 */
124126function getContextWithRemoteActiveSpanAndScopes ( ctx : Context , options : Parameters < typeof continueTrace > [ 0 ] ) : Context {
125- return ensureScopesOnContext ( getContextWithRemoteActiveSpan ( ctx , options ) ) ;
127+ const ctxWithRemoteSpan = getContextWithRemoteActiveSpan ( ctx , options ) ;
128+ // If a remote active span was set, we are continuing an incoming trace, so the trace id is fixed.
129+ // Otherwise there was no (valid) incoming trace and we are the head of a new trace.
130+ const isContinuingTrace = trace . getSpanContext ( ctxWithRemoteSpan ) !== undefined ;
131+ return ensureScopesOnContext ( ctxWithRemoteSpan , isContinuingTrace ) ;
126132}
127133
128- function ensureScopesOnContext ( ctx : Context ) : Context {
134+ function ensureScopesOnContext ( ctx : Context , isContinuingTrace : boolean ) : Context {
129135 // If there are no scopes yet on the context, ensure we have them
130136 const scopes = getScopesFromContext ( ctx ) ;
137+
138+ // If we have no scope here, this is most likely either the root context or a context manually derived from it
139+ // In this case, we want to fork the current scope, to ensure we do not pollute the root scope
140+ const scope = scopes ? scopes . scope : getCurrentScope ( ) . clone ( ) ;
141+
142+ // When we forked a fresh scope and are not continuing an incoming trace, we give it its own trace.
143+ // Without this, concurrent header-less requests (e.g. edge middleware, whose root span is created by
144+ // upstream OTEL instrumentation rather than through `continueTrace`) would all inherit the forked
145+ // scope's trace id and collapse into a single trace. Mirrors `continueTrace` in the core HTTP server.
146+ if ( ! scopes && ! isContinuingTrace ) {
147+ const propagationContext = scope . getPropagationContext ( ) ;
148+ scope . setPropagationContext ( {
149+ ...propagationContext ,
150+ traceId : generateTraceId ( ) ,
151+ sampleRand : _INTERNAL_safeMathRandom ( ) ,
152+ } ) ;
153+ }
154+
131155 const newScopes = {
132- // If we have no scope here, this is most likely either the root context or a context manually derived from it
133- // In this case, we want to fork the current scope, to ensure we do not pollute the root scope
134- scope : scopes ? scopes . scope : getCurrentScope ( ) . clone ( ) ,
156+ scope,
135157 isolationScope : scopes ? scopes . isolationScope : getIsolationScope ( ) ,
136158 } ;
137159
0 commit comments