Skip to content

Commit 7dc96ff

Browse files
committed
feat(nextjs): remove tracing from pages router API routes
Drop the trace-wrapping logic from wrapApiHandlerWithSentry on both the Node server and Edge runtimes. The wrappers now only capture errors and set the transaction name on the isolation scope; the transaction itself comes from Next.js's own OTEL span, which we backfill with the right op, source and name.
1 parent 536eb3b commit 7dc96ff

11 files changed

Lines changed: 249 additions & 313 deletions

File tree

dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ test('Faulty edge routes', async ({ request }) => {
5454

5555
test.step('should have scope isolation', () => {
5656
expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
57-
expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
57+
// The wrapper no longer forks a fresh isolation scope, so tags set on the global scope now leak through.
58+
expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).toBeDefined();
5859
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
59-
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
60+
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).toBeDefined();
6061
});
6162
});
Lines changed: 29 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,7 @@
1-
import {
2-
captureException,
3-
continueTrace,
4-
debug,
5-
getActiveSpan,
6-
httpRequestToRequestData,
7-
isString,
8-
isURLObjectRelative,
9-
objectify,
10-
parseStringToURLObject,
11-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
12-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
13-
setHttpStatus,
14-
startSpanManual,
15-
withIsolationScope,
16-
} from '@sentry/core';
1+
import { captureException, debug, objectify } from '@sentry/core';
172
import type { NextApiRequest } from 'next';
183
import type { AugmentedNextApiResponse, NextApiHandler } from '../types';
19-
import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd';
20-
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';
21-
import { HTTP_ROUTE, SENTRY_KIND, URL_FULL, URL_PATH } from '@sentry/conventions/attributes';
4+
import { flushSafelyWithTimeout } from '../utils/responseEnd';
225

236
export type AugmentedNextApiRequest = NextApiRequest & {
247
__withSentry_applied__?: boolean;
@@ -34,15 +17,13 @@ export type AugmentedNextApiRequest = NextApiRequest & {
3417
*/
3518
export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameterizedRoute: string): NextApiHandler {
3619
return new Proxy(apiHandler, {
37-
apply: (
20+
apply: async (
3821
wrappingTarget,
3922
thisArg,
4023
args: [AugmentedNextApiRequest | undefined, AugmentedNextApiResponse | undefined],
4124
) => {
42-
dropNextjsRootContext();
43-
return escapeNextjsTracing(() => {
25+
try {
4426
const [req, res] = args;
45-
4627
if (!req) {
4728
debug.log(
4829
`Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`,
@@ -59,93 +40,37 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
5940
if (req.__withSentry_applied__) {
6041
return wrappingTarget.apply(thisArg, args);
6142
}
43+
6244
req.__withSentry_applied__ = true;
6345

64-
return withIsolationScope(isolationScope => {
65-
// Normally, there is an active span here (from Next.js OTEL) and we just use that as parent
66-
// Else, we manually continueTrace from the incoming headers
67-
const continueTraceIfNoActiveSpan = getActiveSpan()
68-
? <T>(_opts: unknown, callback: () => T) => callback()
69-
: continueTrace;
46+
return await wrappingTarget.apply(thisArg, args);
47+
} catch (e) {
48+
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
49+
// store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
50+
// to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a
51+
// way to prevent it from actually being reported twice.)
52+
const objectifiedErr = objectify(e);
7053

71-
return continueTraceIfNoActiveSpan(
72-
{
73-
sentryTrace:
74-
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined,
75-
baggage: req.headers?.baggage,
54+
captureException(objectifiedErr, {
55+
mechanism: {
56+
type: 'auto.http.nextjs.api_handler',
57+
handled: false,
58+
data: {
59+
wrapped_handler: wrappingTarget.name,
60+
function: 'withSentry',
7661
},
77-
() => {
78-
const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
79-
const normalizedRequest = httpRequestToRequestData(req);
80-
81-
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
82-
isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`);
83-
84-
const requestUrl = normalizedRequest.url || req.url;
85-
const urlObject = requestUrl ? parseStringToURLObject(requestUrl) : undefined;
86-
87-
return startSpanManual(
88-
{
89-
name: `${reqMethod}${parameterizedRoute}`,
90-
op: 'http.server',
91-
forceTransaction: true,
92-
attributes: {
93-
[SENTRY_KIND]: 'server',
94-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
95-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
96-
[URL_FULL]: urlObject && !isURLObjectRelative(urlObject) ? urlObject.href : undefined,
97-
[URL_PATH]: urlObject?.pathname,
98-
[HTTP_ROUTE]: parameterizedRoute,
99-
},
100-
},
101-
async span => {
102-
// eslint-disable-next-line @typescript-eslint/unbound-method
103-
res.end = new Proxy(res.end, {
104-
apply(target, thisArg, argArray) {
105-
setHttpStatus(span, res.statusCode);
106-
span.end();
107-
waitUntil(flushSafelyWithTimeout());
108-
return target.apply(thisArg, argArray);
109-
},
110-
});
111-
try {
112-
return await wrappingTarget.apply(thisArg, args);
113-
} catch (e) {
114-
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
115-
// store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
116-
// to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a
117-
// way to prevent it from actually being reported twice.)
118-
const objectifiedErr = objectify(e);
119-
120-
captureException(objectifiedErr, {
121-
mechanism: {
122-
type: 'auto.http.nextjs.api_handler',
123-
handled: false,
124-
data: {
125-
wrapped_handler: wrappingTarget.name,
126-
function: 'withSentry',
127-
},
128-
},
129-
});
130-
131-
setHttpStatus(span, 500);
132-
span.end();
62+
},
63+
});
13364

134-
// we need to await the flush here to ensure that the error is captured
135-
// as the runtime freezes as soon as the error is thrown below
136-
await flushSafelyWithTimeout();
65+
// we need to await the flush here to ensure that the error is captured
66+
// as the runtime freezes as soon as the error is thrown below
67+
await flushSafelyWithTimeout();
13768

138-
// We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
139-
// would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
140-
// the error as already having been captured.)
141-
throw objectifiedErr;
142-
}
143-
},
144-
);
145-
},
146-
);
147-
});
148-
});
69+
// We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
70+
// would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
71+
// the error as already having been captured.)
72+
throw objectifiedErr;
73+
}
14974
},
15075
});
15176
}

packages/nextjs/src/common/span-attributes-with-logic-attached.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export const TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION = 'sentry.drop_transaction
66
export const TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL = 'sentry.sentry_trace_backfill';
77

88
export const TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL = 'sentry.route_backfill';
9+
10+
export const ATTR_NEXT_PAGES_API_ROUTE_TYPE = 'executing api route (pages)';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { HTTP_METHOD, HTTP_REQUEST_METHOD } from '@sentry/conventions/attributes';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
3+
import { ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
4+
import { ATTR_NEXT_PAGES_API_ROUTE_TYPE } from '../common/span-attributes-with-logic-attached';
5+
6+
export interface MutableRootSpan {
7+
attributes: Record<string, unknown>;
8+
getName(): string | undefined;
9+
setName(name: string): void;
10+
setOp(op: string): void;
11+
}
12+
13+
/**
14+
* Normalizes name, op and source for the root span of a pages-router API route on the Edge runtime.
15+
*
16+
* We no longer create this transaction ourselves in `wrapApiHandlerWithSentry`, so the root span is the
17+
* Next.js `Node.runHandler` span. Next.js names it `executing api route (pages) /some/route`, which we
18+
* turn into a proper `${METHOD} ${route}` transaction with the `http.server` op and `route` source.
19+
*
20+
* Applied from both `preprocessEvent` (legacy transaction events) and `processSegmentSpan` (streamed spans),
21+
* mirroring how `enhanceMiddlewareRootSpan` is wired.
22+
*/
23+
export function enhanceRunHandlerRootSpan(span: MutableRootSpan): void {
24+
const { attributes } = span;
25+
26+
if (attributes[ATTR_NEXT_SPAN_TYPE] !== 'Node.runHandler') {
27+
return;
28+
}
29+
30+
const spanName = attributes[ATTR_NEXT_SPAN_NAME];
31+
if (typeof spanName !== 'string' || !spanName.startsWith(ATTR_NEXT_PAGES_API_ROUTE_TYPE)) {
32+
return;
33+
}
34+
35+
span.setOp('http.server');
36+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route';
37+
38+
const path = spanName.replace(ATTR_NEXT_PAGES_API_ROUTE_TYPE, '').trim();
39+
// eslint-disable-next-line typescript/no-deprecated
40+
const method = attributes[HTTP_REQUEST_METHOD] ?? attributes[HTTP_METHOD];
41+
span.setName(`${typeof method === 'string' ? method : 'GET'} ${path}`);
42+
}

packages/nextjs/src/edge/index.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ import {
1616
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
1717
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
1818
import { DEBUG_BUILD } from '../common/debug-build';
19-
import { ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
20-
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
19+
import { ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
20+
import {
21+
ATTR_NEXT_PAGES_API_ROUTE_TYPE,
22+
TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION,
23+
} from '../common/span-attributes-with-logic-attached';
2124
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
2225
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
2326
import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan';
@@ -27,6 +30,7 @@ import { flushSafelyWithTimeout, isCloudflareWaitUntilAvailable, waitUntil } fro
2730
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
2831
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
2932
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
33+
import { enhanceRunHandlerRootSpan } from './enhanceRunHandlerRootSpan';
3034
import { SENTRY_KIND } from '@sentry/conventions/attributes';
3135

3236
export * from '@sentry/vercel-edge';
@@ -127,17 +131,27 @@ export function init(options: VercelEdgeOptions = {}): void {
127131
dropMiddlewareTunnelRequests(span, spanAttributes);
128132

129133
// Mark all spans generated by Next.js as 'auto' & server
130-
if (spanAttributes?.['next.span_type'] !== undefined) {
134+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) {
131135
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
132136
span.setAttribute(SENTRY_KIND, 'server');
133137
}
134138

135139
// Make sure middleware spans get the right op
136-
if (spanAttributes?.['next.span_type'] === 'Middleware.execute') {
140+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') {
137141
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware');
138142
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
139143
}
140144

145+
// Backfill op and source for pages-router API routes: we no longer create this span in the wrapper,
146+
// so we rely on the Next.js `Node.runHandler` span becoming the transaction.
147+
if (
148+
spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Node.runHandler' &&
149+
String(spanAttributes?.[ATTR_NEXT_SPAN_NAME]).startsWith(ATTR_NEXT_PAGES_API_ROUTE_TYPE)
150+
) {
151+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
152+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
153+
}
154+
141155
// We want to fork the isolation scope for incoming requests
142156
maybeForkIsolationScopeForRootSpan(span, spanAttributes);
143157

@@ -156,16 +170,18 @@ export function init(options: VercelEdgeOptions = {}): void {
156170
// Span streaming bypasses event processors entirely - see the `processSegmentSpan` hook below for that path.
157171
client?.on('preprocessEvent', event => {
158172
if (event.type === 'transaction' && event.contexts?.trace?.data) {
159-
enhanceMiddlewareRootSpan({
173+
const mutableRootSpan = {
160174
attributes: event.contexts.trace.data,
161175
getName: () => event.transaction,
162-
setName: name => {
176+
setName: (name: string) => {
163177
event.transaction = name;
164178
},
165-
setOp: op => {
179+
setOp: (op: string) => {
166180
event.contexts!.trace!.op = op;
167181
},
168-
});
182+
};
183+
enhanceMiddlewareRootSpan(mutableRootSpan);
184+
enhanceRunHandlerRootSpan(mutableRootSpan);
169185
}
170186

171187
setUrlProcessingMetadata(event);
@@ -175,16 +191,18 @@ export function init(options: VercelEdgeOptions = {}): void {
175191
// transaction events, so the same enhancement has to be applied here directly on the span JSON.
176192
client?.on('processSegmentSpan', span => {
177193
const attributes = (span.attributes ??= {});
178-
enhanceMiddlewareRootSpan({
194+
const mutableRootSpan = {
179195
attributes,
180196
getName: () => span.name,
181-
setName: name => {
197+
setName: (name: string) => {
182198
span.name = name;
183199
},
184-
setOp: op => {
200+
setOp: (op: string) => {
185201
attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op;
186202
},
187-
});
203+
};
204+
enhanceMiddlewareRootSpan(mutableRootSpan);
205+
enhanceRunHandlerRootSpan(mutableRootSpan);
188206
});
189207

190208
client?.on('spanEnd', span => {

0 commit comments

Comments
 (0)