Skip to content

Commit 302be74

Browse files
committed
fixes for edge nextjs
1 parent 5d24bc4 commit 302be74

2 files changed

Lines changed: 34 additions & 10 deletions

File tree

packages/opentelemetry/src/propagator.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import type { Context, SpanContext, TextMapGetter, TextMapPropagator, TextMapSet
22
import { context, trace, TraceFlags } from '@opentelemetry/api';
33
import type { continueTrace, DynamicSamplingContext } from '@sentry/core';
44
import {
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
*/
124126
function 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

packages/vercel-edge/test/middlewareTraceIsolation.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ async function runMiddlewareRequest(
6666
delay = 0,
6767
): Promise<{ traceId: string; childTraceId: string }> {
6868
const request = new Request(url, { method: 'GET', headers });
69-
// Mirror `wrapMiddlewareWithSentry`: fork an isolation scope per request, then run Next's
70-
// `withPropagatedContext` + `Middleware.execute` trace inside it.
71-
return withIsolationScope(() =>
72-
withPropagatedContext(request.headers, () =>
73-
nextMiddlewareTrace(new URL(url).pathname, async () => {
69+
// Faithfully mirror production ordering: Next's native OTEL creates the `Middleware.execute` root span
70+
// (via `withPropagatedContext` -> `trace`) BEFORE `wrapMiddlewareWithSentry` forks its isolation scope.
71+
// So the root span's trace id is fixed by `extract` (SentryPropagator), not by the later fork - the fork
72+
// is nested *inside* the span callback, exactly like `wrapMiddlewareWithSentry`.
73+
return withPropagatedContext(request.headers, () =>
74+
nextMiddlewareTrace(new URL(url).pathname, () =>
75+
withIsolationScope(async () => {
7476
const rootSpan = trace.getActiveSpan()!;
7577
const traceId = spanToJSON(rootSpan).trace_id!;
7678

0 commit comments

Comments
 (0)