Skip to content

Commit 44bc6cf

Browse files
authored
fix(nextjs): Escape Next.js' OpenTelemetry instrumentation (#11625)
1 parent 1a22856 commit 44bc6cf

21 files changed

+704
-916
lines changed

packages/nextjs/src/common/utils/commonObjectTracing.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

packages/nextjs/src/common/utils/edgeWrapperUtils.ts

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import {
44
SPAN_STATUS_OK,
55
captureException,
66
continueTrace,
7-
getIsolationScope,
87
handleCallbackErrors,
98
setHttpStatus,
109
startSpan,
10+
withIsolationScope,
1111
} from '@sentry/core';
1212
import { winterCGRequestToRequestData } from '@sentry/utils';
1313

1414
import type { EdgeRouteHandler } from '../../edge/types';
1515
import { flushQueue } from './responseEnd';
16+
import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils';
1617

1718
/**
1819
* Wraps a function on the edge runtime with error and performance monitoring.
@@ -22,61 +23,65 @@ export function withEdgeWrapping<H extends EdgeRouteHandler>(
2223
options: { spanDescription: string; spanOp: string; mechanismFunctionName: string },
2324
): (...params: Parameters<H>) => Promise<ReturnType<H>> {
2425
return async function (this: unknown, ...args) {
25-
const req: unknown = args[0];
26+
return escapeNextjsTracing(() => {
27+
const req: unknown = args[0];
28+
return withIsolationScope(commonObjectToIsolationScope(req), isolationScope => {
29+
let sentryTrace;
30+
let baggage;
2631

27-
let sentryTrace;
28-
let baggage;
32+
if (req instanceof Request) {
33+
sentryTrace = req.headers.get('sentry-trace') || '';
34+
baggage = req.headers.get('baggage');
2935

30-
if (req instanceof Request) {
31-
sentryTrace = req.headers.get('sentry-trace') || '';
32-
baggage = req.headers.get('baggage');
33-
}
36+
isolationScope.setSDKProcessingMetadata({
37+
request: winterCGRequestToRequestData(req),
38+
});
39+
}
3440

35-
return continueTrace(
36-
{
37-
sentryTrace,
38-
baggage,
39-
},
40-
() => {
41-
getIsolationScope().setSDKProcessingMetadata({
42-
request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined,
43-
});
44-
return startSpan(
41+
return continueTrace(
4542
{
46-
name: options.spanDescription,
47-
op: options.spanOp,
48-
forceTransaction: true,
49-
attributes: {
50-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
51-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
52-
},
43+
sentryTrace,
44+
baggage,
5345
},
54-
async span => {
55-
const handlerResult = await handleCallbackErrors(
56-
() => handler.apply(this, args),
57-
error => {
58-
captureException(error, {
59-
mechanism: {
60-
type: 'instrument',
61-
handled: false,
62-
data: {
63-
function: options.mechanismFunctionName,
64-
},
65-
},
66-
});
46+
() => {
47+
return startSpan(
48+
{
49+
name: options.spanDescription,
50+
op: options.spanOp,
51+
forceTransaction: true,
52+
attributes: {
53+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
54+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
55+
},
6756
},
68-
);
57+
async span => {
58+
const handlerResult = await handleCallbackErrors(
59+
() => handler.apply(this, args),
60+
error => {
61+
captureException(error, {
62+
mechanism: {
63+
type: 'instrument',
64+
handled: false,
65+
data: {
66+
function: options.mechanismFunctionName,
67+
},
68+
},
69+
});
70+
},
71+
);
6972

70-
if (handlerResult instanceof Response) {
71-
setHttpStatus(span, handlerResult.status);
72-
} else {
73-
span.setStatus({ code: SPAN_STATUS_OK });
74-
}
73+
if (handlerResult instanceof Response) {
74+
setHttpStatus(span, handlerResult.status);
75+
} else {
76+
span.setStatus({ code: SPAN_STATUS_OK });
77+
}
7578

76-
return handlerResult;
79+
return handlerResult;
80+
},
81+
).finally(() => flushQueue());
7782
},
78-
).finally(() => flushQueue());
79-
},
80-
);
83+
);
84+
});
85+
});
8186
};
8287
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Scope, getCurrentScope, withActiveSpan } from '@sentry/core';
2+
import type { PropagationContext } from '@sentry/types';
3+
import { GLOBAL_OBJ, logger, uuid4 } from '@sentry/utils';
4+
import { DEBUG_BUILD } from '../debug-build';
5+
6+
const commonPropagationContextMap = new WeakMap<object, PropagationContext>();
7+
8+
/**
9+
* Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context.
10+
*
11+
* @param commonObject The shared object.
12+
* @param propagationContext The propagation context that should be shared between all the resources if no propagation context was registered yet.
13+
* @returns the shared propagation context.
14+
*/
15+
export function commonObjectToPropagationContext(
16+
commonObject: unknown,
17+
propagationContext: PropagationContext,
18+
): PropagationContext {
19+
if (typeof commonObject === 'object' && commonObject) {
20+
const memoPropagationContext = commonPropagationContextMap.get(commonObject);
21+
if (memoPropagationContext) {
22+
return memoPropagationContext;
23+
} else {
24+
commonPropagationContextMap.set(commonObject, propagationContext);
25+
return propagationContext;
26+
}
27+
} else {
28+
return propagationContext;
29+
}
30+
}
31+
32+
const commonIsolationScopeMap = new WeakMap<object, Scope>();
33+
34+
/**
35+
* Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context.
36+
*
37+
* @param commonObject The shared object.
38+
* @param isolationScope The isolationScope that should be shared between all the resources if no isolation scope was created yet.
39+
* @returns the shared isolation scope.
40+
*/
41+
export function commonObjectToIsolationScope(commonObject: unknown): Scope {
42+
if (typeof commonObject === 'object' && commonObject) {
43+
const memoIsolationScope = commonIsolationScopeMap.get(commonObject);
44+
if (memoIsolationScope) {
45+
return memoIsolationScope;
46+
} else {
47+
const newIsolationScope = new Scope();
48+
commonIsolationScopeMap.set(commonObject, newIsolationScope);
49+
return newIsolationScope;
50+
}
51+
} else {
52+
return new Scope();
53+
}
54+
}
55+
56+
interface AsyncLocalStorage<T> {
57+
getStore(): T | undefined;
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
run<R, TArgs extends any[]>(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R;
60+
}
61+
62+
let nextjsEscapedAsyncStorage: AsyncLocalStorage<true>;
63+
64+
/**
65+
* Will mark the execution context of the callback as "escaped" from Next.js internal tracing by unsetting the active
66+
* span and propagation context. When an execution passes through this function multiple times, it is a noop after the
67+
* first time.
68+
*/
69+
export function escapeNextjsTracing<T>(cb: () => T): T {
70+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
71+
const MaybeGlobalAsyncLocalStorage = (GLOBAL_OBJ as any).AsyncLocalStorage;
72+
73+
if (!MaybeGlobalAsyncLocalStorage) {
74+
DEBUG_BUILD &&
75+
logger.warn(
76+
"Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.",
77+
);
78+
return cb();
79+
}
80+
81+
if (!nextjsEscapedAsyncStorage) {
82+
nextjsEscapedAsyncStorage = new MaybeGlobalAsyncLocalStorage();
83+
}
84+
85+
if (nextjsEscapedAsyncStorage.getStore()) {
86+
return cb();
87+
} else {
88+
return withActiveSpan(null, () => {
89+
getCurrentScope().setPropagationContext({
90+
traceId: uuid4(),
91+
spanId: uuid4().substring(16),
92+
});
93+
return nextjsEscapedAsyncStorage.run(true, () => {
94+
return cb();
95+
});
96+
});
97+
}
98+
}

packages/nextjs/src/common/utils/withIsolationScopeOrReuseFromRootSpan.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

packages/nextjs/src/common/utils/wrapperUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import {
1010
startSpan,
1111
startSpanManual,
1212
withActiveSpan,
13+
withIsolationScope,
1314
} from '@sentry/core';
1415
import type { Span } from '@sentry/types';
1516
import { isString } from '@sentry/utils';
1617

1718
import { platformSupportsStreaming } from './platformSupportsStreaming';
1819
import { autoEndSpanOnResponseEnd, flushQueue } from './responseEnd';
19-
import { withIsolationScopeOrReuseFromRootSpan } from './withIsolationScopeOrReuseFromRootSpan';
20+
import { commonObjectToIsolationScope } from './tracingUtils';
2021

2122
declare module 'http' {
2223
interface IncomingMessage {
@@ -89,7 +90,8 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
8990
},
9091
): (...params: Parameters<F>) => Promise<ReturnType<F>> {
9192
return async function (this: unknown, ...args: Parameters<F>): Promise<ReturnType<F>> {
92-
return withIsolationScopeOrReuseFromRootSpan(async isolationScope => {
93+
const isolationScope = commonObjectToIsolationScope(req);
94+
return withIsolationScope(isolationScope, () => {
9395
isolationScope.setSDKProcessingMetadata({
9496
request: req,
9597
});
@@ -100,7 +102,6 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
100102

101103
return continueTrace({ sentryTrace, baggage }, () => {
102104
const requestSpan = getOrStartRequestSpan(req, res, options.requestedRouteName);
103-
104105
return withActiveSpan(requestSpan, () => {
105106
return startSpanManual(
106107
{

0 commit comments

Comments
 (0)