Skip to content

Commit 06ef628

Browse files
onurtemizkanlforst
andauthored
feat(nextjs): Add method and url to route handler request data (#14084)
Resolves: #13908 --------- Co-authored-by: Luca Forstner <luca.forstner@sentry.io>
1 parent 57ba5a7 commit 06ef628

File tree

3 files changed

+68
-69
lines changed

3 files changed

+68
-69
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn
6060

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

63+
expect(routehandlerError.request?.method).toBe('PUT');
64+
expect(routehandlerError.request?.url).toContain('/route-handlers/baz/error');
65+
6366
expect(routehandlerError.transaction).toBe('PUT /route-handlers/[param]/error');
6467
});
6568

packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
captureException,
66
getActiveSpan,
77
getCapturedScopesOnSpan,
8+
getIsolationScope,
89
getRootSpan,
910
handleCallbackErrors,
1011
setCapturedScopesOnSpan,
@@ -16,9 +17,8 @@ import {
1617
import type { RouteHandlerContext } from './types';
1718

1819
import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
19-
2020
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
21-
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
21+
import { commonObjectToIsolationScope } from './utils/tracingUtils';
2222

2323
/**
2424
* Wraps a Next.js App Router Route handler with Sentry error and performance instrumentation.
@@ -34,80 +34,83 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
3434

3535
return new Proxy(routeHandler, {
3636
apply: async (originalFunction, thisArg, args) => {
37-
const isolationScope = commonObjectToIsolationScope(headers);
38-
39-
const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};
40-
41-
isolationScope.setSDKProcessingMetadata({
42-
request: {
43-
headers: completeHeadersDict,
44-
},
45-
});
46-
47-
const incomingPropagationContext = propagationContextFromHeaders(
48-
completeHeadersDict['sentry-trace'],
49-
completeHeadersDict['baggage'],
50-
);
51-
52-
const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext);
53-
5437
const activeSpan = getActiveSpan();
5538
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
56-
if (rootSpan) {
39+
40+
let edgeRuntimeIsolationScopeOverride: Scope | undefined;
41+
if (rootSpan && process.env.NEXT_RUNTIME === 'edge') {
42+
const isolationScope = commonObjectToIsolationScope(headers);
5743
const { scope } = getCapturedScopesOnSpan(rootSpan);
5844
setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope);
5945

60-
if (process.env.NEXT_RUNTIME === 'edge') {
61-
rootSpan.updateName(`${method} ${parameterizedRoute}`);
62-
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
63-
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
64-
}
46+
edgeRuntimeIsolationScopeOverride = isolationScope;
47+
48+
rootSpan.updateName(`${method} ${parameterizedRoute}`);
49+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
50+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
6551
}
6652

67-
return withIsolationScope(isolationScope, () => {
68-
return withScope(async scope => {
69-
scope.setTransactionName(`${method} ${parameterizedRoute}`);
70-
scope.setPropagationContext(propagationContext);
53+
return withIsolationScope(
54+
process.env.NEXT_RUNTIME === 'edge' ? edgeRuntimeIsolationScopeOverride : getIsolationScope(),
55+
() => {
56+
return withScope(async scope => {
57+
scope.setTransactionName(`${method} ${parameterizedRoute}`);
7158

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)) {
59+
if (process.env.NEXT_RUNTIME === 'edge') {
60+
const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};
61+
const incomingPropagationContext = propagationContextFromHeaders(
62+
completeHeadersDict['sentry-trace'],
63+
completeHeadersDict['baggage'],
64+
);
65+
scope.setPropagationContext(incomingPropagationContext);
66+
scope.setSDKProcessingMetadata({
67+
request: {
68+
method,
69+
headers: completeHeadersDict,
70+
},
71+
});
72+
}
73+
74+
const response: Response = await handleCallbackErrors(
75+
() => originalFunction.apply(thisArg, args),
76+
error => {
77+
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
78+
if (isRedirectNavigationError(error)) {
79+
// Don't do anything
80+
} else if (isNotFoundNavigationError(error)) {
81+
if (activeSpan) {
82+
setHttpStatus(activeSpan, 404);
83+
}
84+
if (rootSpan) {
85+
setHttpStatus(rootSpan, 404);
86+
}
87+
} else {
88+
captureException(error, {
89+
mechanism: {
90+
handled: false,
91+
},
92+
});
93+
}
94+
},
95+
);
96+
97+
try {
98+
if (response.status) {
7999
if (activeSpan) {
80-
setHttpStatus(activeSpan, 404);
100+
setHttpStatus(activeSpan, response.status);
81101
}
82102
if (rootSpan) {
83-
setHttpStatus(rootSpan, 404);
103+
setHttpStatus(rootSpan, response.status);
84104
}
85-
} else {
86-
captureException(error, {
87-
mechanism: {
88-
handled: false,
89-
},
90-
});
91-
}
92-
},
93-
);
94-
95-
try {
96-
if (response.status) {
97-
if (activeSpan) {
98-
setHttpStatus(activeSpan, response.status);
99-
}
100-
if (rootSpan) {
101-
setHttpStatus(rootSpan, response.status);
102105
}
106+
} catch {
107+
// best effort - response may be undefined?
103108
}
104-
} catch {
105-
// best effort - response may be undefined?
106-
}
107109

108-
return response;
109-
});
110-
});
110+
return response;
111+
});
112+
},
113+
);
111114
},
112115
});
113116
}

packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,20 @@ function wrapHandler<T>(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | '
3838

3939
return new Proxy(handler, {
4040
apply: (originalFunction, thisArg, args) => {
41-
let sentryTraceHeader: string | undefined | null = undefined;
42-
let baggageHeader: string | undefined | null = undefined;
4341
let headers: WebFetchHeaders | undefined = undefined;
4442

4543
// We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API
4644
try {
47-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
4845
const requestAsyncStore = requestAsyncStorage?.getStore() as ReturnType<RequestAsyncStorage['getStore']>;
49-
sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined;
50-
baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined;
5146
headers = requestAsyncStore?.headers;
5247
} catch (e) {
5348
/** empty */
5449
}
5550

56-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
51+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5752
return Sentry.wrapRouteHandlerWithSentry(originalFunction as any, {
5853
method,
5954
parameterizedRoute: '__ROUTE__',
60-
sentryTraceHeader,
61-
baggageHeader,
6255
headers,
6356
}).apply(thisArg, args);
6457
},

0 commit comments

Comments
 (0)