diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 7e6dc5fbe300..648ee81839ac 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -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'); }); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index 6ff4e314b17b..215bb35ce9a5 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -5,6 +5,7 @@ import { captureException, getActiveSpan, getCapturedScopesOnSpan, + getIsolationScope, getRootSpan, handleCallbackErrors, setCapturedScopesOnSpan, @@ -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. @@ -34,80 +34,83 @@ export function wrapRouteHandlerWithSentry any>( return new Proxy(routeHandler, { apply: async (originalFunction, thisArg, args) => { - const isolationScope = commonObjectToIsolationScope(headers); - - const completeHeadersDict: Record = 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 = 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; + }); + }, + ); }, }); } diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index 9f955f9f4109..d0b9fba5ce3b 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -38,27 +38,20 @@ function wrapHandler(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; - 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); },