Skip to content

Commit 121bcee

Browse files
authored
feat(nextjs): Use OTEL instrumentation for route handlers (#13887)
1 parent af2f5cb commit 121bcee

File tree

3 files changed

+74
-83
lines changed

3 files changed

+74
-83
lines changed

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn
5454
expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true);
5555
expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
5656

57-
expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error');
57+
expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error');
5858
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
59-
expect(routehandlerTransaction.contexts?.trace?.origin).toBe('auto.function.nextjs');
59+
expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto.http.otel.http');
6060

6161
expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error');
6262

packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts

Lines changed: 50 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import {
2-
SEMANTIC_ATTRIBUTE_SENTRY_OP,
3-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
4-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
5-
SPAN_STATUS_ERROR,
2+
Scope,
63
captureException,
4+
getActiveSpan,
5+
getCapturedScopesOnSpan,
6+
getRootSpan,
77
handleCallbackErrors,
8-
setHttpStatus,
9-
startSpan,
8+
setCapturedScopesOnSpan,
109
withIsolationScope,
1110
withScope,
1211
} from '@sentry/core';
13-
import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
14-
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
12+
1513
import type { RouteHandlerContext } from './types';
16-
import { flushSafelyWithTimeout } from './utils/responseEnd';
17-
import {
18-
commonObjectToIsolationScope,
19-
commonObjectToPropagationContext,
20-
escapeNextjsTracing,
21-
} from './utils/tracingUtils';
22-
import { vercelWaitUntil } from './utils/vercelWaitUntil';
14+
15+
import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
16+
17+
import { isRedirectNavigationError } from './nextNavigationErrorUtils';
18+
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
2319

2420
/**
2521
* Wraps a Next.js App Router Route handler with Sentry error and performance instrumentation.
@@ -34,74 +30,51 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
3430
const { method, parameterizedRoute, headers } = context;
3531

3632
return new Proxy(routeHandler, {
37-
apply: (originalFunction, thisArg, args) => {
38-
return escapeNextjsTracing(() => {
39-
const isolationScope = commonObjectToIsolationScope(headers);
33+
apply: async (originalFunction, thisArg, args) => {
34+
const isolationScope = commonObjectToIsolationScope(headers);
4035

41-
const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};
36+
const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};
4237

43-
isolationScope.setSDKProcessingMetadata({
44-
request: {
45-
headers: completeHeadersDict,
46-
},
47-
});
38+
isolationScope.setSDKProcessingMetadata({
39+
request: {
40+
headers: completeHeadersDict,
41+
},
42+
});
4843

49-
const incomingPropagationContext = propagationContextFromHeaders(
50-
completeHeadersDict['sentry-trace'],
51-
completeHeadersDict['baggage'],
52-
);
44+
const incomingPropagationContext = propagationContextFromHeaders(
45+
completeHeadersDict['sentry-trace'],
46+
completeHeadersDict['baggage'],
47+
);
5348

54-
const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext);
49+
const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext);
5550

56-
return withIsolationScope(isolationScope, () => {
57-
return withScope(async scope => {
58-
scope.setTransactionName(`${method} ${parameterizedRoute}`);
59-
scope.setPropagationContext(propagationContext);
60-
try {
61-
return startSpan(
62-
{
63-
name: `${method} ${parameterizedRoute}`,
64-
attributes: {
65-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
66-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
67-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
68-
},
69-
forceTransaction: true,
70-
},
71-
async span => {
72-
const response: Response = await handleCallbackErrors(
73-
() => originalFunction.apply(thisArg, args),
74-
error => {
75-
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
76-
if (isRedirectNavigationError(error)) {
77-
// Don't do anything
78-
} else if (isNotFoundNavigationError(error) && span) {
79-
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
80-
} else {
81-
captureException(error, {
82-
mechanism: {
83-
handled: false,
84-
},
85-
});
86-
}
87-
},
88-
);
51+
const activeSpan = getActiveSpan();
52+
if (activeSpan) {
53+
const rootSpan = getRootSpan(activeSpan);
54+
rootSpan.setAttribute('sentry.route_handler', true);
55+
const { scope } = getCapturedScopesOnSpan(rootSpan);
56+
setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope);
57+
}
8958

90-
try {
91-
if (span && response.status) {
92-
setHttpStatus(span, response.status);
93-
}
94-
} catch {
95-
// best effort - response may be undefined?
96-
}
97-
98-
return response;
99-
},
100-
);
101-
} finally {
102-
vercelWaitUntil(flushSafelyWithTimeout());
103-
}
104-
});
59+
return withIsolationScope(isolationScope, () => {
60+
return withScope(scope => {
61+
scope.setTransactionName(`${method} ${parameterizedRoute}`);
62+
scope.setPropagationContext(propagationContext);
63+
return handleCallbackErrors(
64+
() => originalFunction.apply(thisArg, args),
65+
error => {
66+
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
67+
if (isRedirectNavigationError(error)) {
68+
// Don't do anything
69+
} else {
70+
captureException(error, {
71+
mechanism: {
72+
handled: false,
73+
},
74+
});
75+
}
76+
},
77+
);
10578
});
10679
});
10780
},

packages/nextjs/src/server/index.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,10 @@ export function init(options: NodeOptions): NodeClient | undefined {
196196
// We want to rename these spans because they look like "GET /path/to/route" and we already emit spans that look
197197
// like this with our own http instrumentation.
198198
if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest') {
199-
span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this.
199+
const rootSpan = getRootSpan(span);
200+
if (span !== rootSpan) {
201+
span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this.
202+
}
200203
}
201204
});
202205

@@ -211,11 +214,13 @@ export function init(options: NodeOptions): NodeClient | undefined {
211214
return null;
212215
}
213216

214-
// We only want to use our HTTP integration/instrumentation for app router requests, which are marked with the `sentry.rsc` attribute.
217+
// We only want to use our HTTP integration/instrumentation for app router requests,
218+
// which are marked with the `sentry.rsc` or `sentry.route_handler` attribute.
215219
if (
216220
(event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.http' ||
217221
event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest') &&
218-
event.contexts?.trace?.data?.['sentry.rsc'] !== true
222+
event.contexts?.trace?.data?.['sentry.rsc'] !== true &&
223+
event.contexts?.trace?.data?.['sentry.route_handler'] !== true
219224
) {
220225
return null;
221226
}
@@ -297,13 +302,26 @@ export function init(options: NodeOptions): NodeClient | undefined {
297302
event.type === 'transaction' &&
298303
event.transaction?.match(/^(RSC )?GET /) &&
299304
event.contexts?.trace?.data?.['sentry.rsc'] === true &&
300-
!event.contexts.trace.op
305+
!event.contexts?.trace?.op
301306
) {
302307
event.contexts.trace.data = event.contexts.trace.data || {};
303308
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server';
304309
event.contexts.trace.op = 'http.server';
305310
}
306311

312+
// Enhance route handler transactions
313+
if (event.type === 'transaction' && event.contexts?.trace?.data?.['sentry.route_handler'] === true) {
314+
event.contexts.trace.data = event.contexts.trace.data || {};
315+
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server';
316+
event.contexts.trace.op = 'http.server';
317+
if (typeof event.contexts.trace.data[ATTR_HTTP_ROUTE] === 'string') {
318+
// eslint-disable-next-line deprecation/deprecation
319+
event.transaction = `${event.contexts.trace.data[SEMATTRS_HTTP_METHOD]} ${event.contexts.trace.data[
320+
ATTR_HTTP_ROUTE
321+
].replace(/\/route$/, '')}`;
322+
}
323+
}
324+
307325
return event;
308326
}) satisfies EventProcessor,
309327
{ id: 'NextjsTransactionEnhancer' },

0 commit comments

Comments
 (0)