Skip to content

Commit

Permalink
feat(nextjs): Add method and url to route handler request data (#14084)
Browse files Browse the repository at this point in the history
Resolves: #13908

---------

Co-authored-by: Luca Forstner <[email protected]>
  • Loading branch information
onurtemizkan and lforst authored Oct 29, 2024
1 parent 57ba5a7 commit 06ef628
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn

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

expect(routehandlerError.request?.method).toBe('PUT');
expect(routehandlerError.request?.url).toContain('/route-handlers/baz/error');

expect(routehandlerError.transaction).toBe('PUT /route-handlers/[param]/error');
});

Expand Down
125 changes: 64 additions & 61 deletions packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
captureException,
getActiveSpan,
getCapturedScopesOnSpan,
getIsolationScope,
getRootSpan,
handleCallbackErrors,
setCapturedScopesOnSpan,
Expand All @@ -16,9 +17,8 @@ import {
import type { RouteHandlerContext } from './types';

import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';

import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { commonObjectToIsolationScope } from './utils/tracingUtils';

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

return new Proxy(routeHandler, {
apply: async (originalFunction, thisArg, args) => {
const isolationScope = commonObjectToIsolationScope(headers);

const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};

isolationScope.setSDKProcessingMetadata({
request: {
headers: completeHeadersDict,
},
});

const incomingPropagationContext = propagationContextFromHeaders(
completeHeadersDict['sentry-trace'],
completeHeadersDict['baggage'],
);

const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext);

const activeSpan = getActiveSpan();
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
if (rootSpan) {

let edgeRuntimeIsolationScopeOverride: Scope | undefined;
if (rootSpan && process.env.NEXT_RUNTIME === 'edge') {
const isolationScope = commonObjectToIsolationScope(headers);
const { scope } = getCapturedScopesOnSpan(rootSpan);
setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope);

if (process.env.NEXT_RUNTIME === 'edge') {
rootSpan.updateName(`${method} ${parameterizedRoute}`);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
}
edgeRuntimeIsolationScopeOverride = isolationScope;

rootSpan.updateName(`${method} ${parameterizedRoute}`);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
}

return withIsolationScope(isolationScope, () => {
return withScope(async scope => {
scope.setTransactionName(`${method} ${parameterizedRoute}`);
scope.setPropagationContext(propagationContext);
return withIsolationScope(
process.env.NEXT_RUNTIME === 'edge' ? edgeRuntimeIsolationScopeOverride : getIsolationScope(),
() => {
return withScope(async scope => {
scope.setTransactionName(`${method} ${parameterizedRoute}`);

const response: Response = await handleCallbackErrors(
() => originalFunction.apply(thisArg, args),
error => {
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
if (isRedirectNavigationError(error)) {
// Don't do anything
} else if (isNotFoundNavigationError(error)) {
if (process.env.NEXT_RUNTIME === 'edge') {
const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};
const incomingPropagationContext = propagationContextFromHeaders(
completeHeadersDict['sentry-trace'],
completeHeadersDict['baggage'],
);
scope.setPropagationContext(incomingPropagationContext);
scope.setSDKProcessingMetadata({
request: {
method,
headers: completeHeadersDict,
},
});
}

const response: Response = await handleCallbackErrors(
() => originalFunction.apply(thisArg, args),
error => {
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
if (isRedirectNavigationError(error)) {
// Don't do anything
} else if (isNotFoundNavigationError(error)) {
if (activeSpan) {
setHttpStatus(activeSpan, 404);
}
if (rootSpan) {
setHttpStatus(rootSpan, 404);
}
} else {
captureException(error, {
mechanism: {
handled: false,
},
});
}
},
);

try {
if (response.status) {
if (activeSpan) {
setHttpStatus(activeSpan, 404);
setHttpStatus(activeSpan, response.status);
}
if (rootSpan) {
setHttpStatus(rootSpan, 404);
setHttpStatus(rootSpan, response.status);
}
} else {
captureException(error, {
mechanism: {
handled: false,
},
});
}
},
);

try {
if (response.status) {
if (activeSpan) {
setHttpStatus(activeSpan, response.status);
}
if (rootSpan) {
setHttpStatus(rootSpan, response.status);
}
} catch {
// best effort - response may be undefined?
}
} catch {
// best effort - response may be undefined?
}

return response;
});
});
return response;
});
},
);
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,20 @@ function wrapHandler<T>(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | '

return new Proxy(handler, {
apply: (originalFunction, thisArg, args) => {
let sentryTraceHeader: string | undefined | null = undefined;
let baggageHeader: string | undefined | null = undefined;
let headers: WebFetchHeaders | undefined = undefined;

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

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Sentry.wrapRouteHandlerWithSentry(originalFunction as any, {
method,
parameterizedRoute: '__ROUTE__',
sentryTraceHeader,
baggageHeader,
headers,
}).apply(thisArg, args);
},
Expand Down

0 comments on commit 06ef628

Please sign in to comment.