Skip to content

Commit 70a154c

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 682f5e2 commit 70a154c

10 files changed

Lines changed: 264 additions & 325 deletions

packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts

Lines changed: 50 additions & 118 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, httpRequestToRequestData, objectify, withIsolationScope } 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,117 +17,66 @@ 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(() => {
44-
const [req, res] = args;
25+
const [req, res] = args;
26+
if (!req) {
27+
debug.log(
28+
`Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`,
29+
);
30+
return wrappingTarget.apply(thisArg, args);
31+
} else if (!res) {
32+
debug.log(
33+
`Wrapped API handler on route "${parameterizedRoute}" was not passed a response object. Will not instrument.`,
34+
);
35+
return wrappingTarget.apply(thisArg, args);
36+
}
4537

46-
if (!req) {
47-
debug.log(
48-
`Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`,
49-
);
50-
return wrappingTarget.apply(thisArg, args);
51-
} else if (!res) {
52-
debug.log(
53-
`Wrapped API handler on route "${parameterizedRoute}" was not passed a response object. Will not instrument.`,
54-
);
55-
return wrappingTarget.apply(thisArg, args);
56-
}
57-
58-
// Prevent double wrapping of the same request.
59-
if (req.__withSentry_applied__) {
60-
return wrappingTarget.apply(thisArg, args);
61-
}
62-
req.__withSentry_applied__ = true;
38+
// Prevent double wrapping of the same request.
39+
if (req.__withSentry_applied__) {
40+
return wrappingTarget.apply(thisArg, args);
41+
}
6342

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;
70-
71-
return continueTraceIfNoActiveSpan(
72-
{
73-
sentryTrace:
74-
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined,
75-
baggage: req.headers?.baggage,
76-
},
77-
() => {
78-
const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
79-
const normalizedRequest = httpRequestToRequestData(req);
43+
req.__withSentry_applied__ = true;
8044

81-
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
82-
isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`);
45+
return withIsolationScope(async isolationScope => {
46+
const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
8347

84-
const requestUrl = normalizedRequest.url || req.url;
85-
const urlObject = requestUrl ? parseStringToURLObject(requestUrl) : undefined;
48+
isolationScope.setSDKProcessingMetadata({ normalizedRequest: httpRequestToRequestData(req) });
49+
isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`);
8650

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);
51+
try {
52+
return await wrappingTarget.apply(thisArg, args);
53+
} catch (e) {
54+
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
55+
// store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
56+
// to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a
57+
// way to prevent it from actually being reported twice.)
58+
const objectifiedErr = objectify(e);
11959

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();
60+
captureException(objectifiedErr, {
61+
mechanism: {
62+
type: 'auto.http.nextjs.api_handler',
63+
handled: false,
64+
data: {
65+
wrapped_handler: wrappingTarget.name,
66+
function: 'withSentry',
67+
},
68+
},
69+
});
13370

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();
71+
// we need to await the flush here to ensure that the error is captured
72+
// as the runtime freezes as soon as the error is thrown below
73+
await flushSafelyWithTimeout();
13774

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-
});
75+
// We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
76+
// would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
77+
// the error as already having been captured.)
78+
throw objectifiedErr;
79+
}
14880
});
14981
},
15082
});

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
import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op';
3236

@@ -128,17 +132,27 @@ export function init(options: VercelEdgeOptions = {}): void {
128132
dropMiddlewareTunnelRequests(span, spanAttributes);
129133

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

136140
// Make sure middleware spans get the right op
137-
if (spanAttributes?.['next.span_type'] === 'Middleware.execute') {
141+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') {
138142
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, WEB_SERVER_MIDDLEWARE_SPAN_OP);
139143
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
140144
}
141145

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

@@ -157,16 +171,18 @@ export function init(options: VercelEdgeOptions = {}): void {
157171
// Span streaming bypasses event processors entirely - see the `processSegmentSpan` hook below for that path.
158172
client.on('preprocessEvent', event => {
159173
if (event.type === 'transaction' && event.contexts?.trace?.data) {
160-
enhanceMiddlewareRootSpan({
174+
const mutableRootSpan = {
161175
attributes: event.contexts.trace.data,
162176
getName: () => event.transaction,
163-
setName: name => {
177+
setName: (name: string) => {
164178
event.transaction = name;
165179
},
166-
setOp: op => {
180+
setOp: (op: string) => {
167181
event.contexts!.trace!.op = op;
168182
},
169-
});
183+
};
184+
enhanceMiddlewareRootSpan(mutableRootSpan);
185+
enhanceRunHandlerRootSpan(mutableRootSpan);
170186
}
171187

172188
setUrlProcessingMetadata(event);
@@ -176,16 +192,18 @@ export function init(options: VercelEdgeOptions = {}): void {
176192
// transaction events, so the same enhancement has to be applied here directly on the span JSON.
177193
client.on('processSegmentSpan', span => {
178194
const attributes = (span.attributes ??= {});
179-
enhanceMiddlewareRootSpan({
195+
const mutableRootSpan = {
180196
attributes,
181197
getName: () => span.name,
182-
setName: name => {
198+
setName: (name: string) => {
183199
span.name = name;
184200
},
185-
setOp: op => {
201+
setOp: (op: string) => {
186202
attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op;
187203
},
188-
});
204+
};
205+
enhanceMiddlewareRootSpan(mutableRootSpan);
206+
enhanceRunHandlerRootSpan(mutableRootSpan);
189207
});
190208

191209
client.on('spanEnd', span => {

0 commit comments

Comments
 (0)