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 8f474ed50046..3e8006ae21f8 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 @@ -54,9 +54,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true); expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error'); + expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); - expect(routehandlerTransaction.contexts?.trace?.origin).toBe('auto.function.nextjs'); + expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto.http.otel.http'); expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error'); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index bf0d475603f2..ad70c865dedf 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -1,25 +1,21 @@ import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SPAN_STATUS_ERROR, + Scope, captureException, + getActiveSpan, + getCapturedScopesOnSpan, + getRootSpan, handleCallbackErrors, - setHttpStatus, - startSpan, + setCapturedScopesOnSpan, withIsolationScope, withScope, } from '@sentry/core'; -import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; -import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; + import type { RouteHandlerContext } from './types'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; -import { - commonObjectToIsolationScope, - commonObjectToPropagationContext, - escapeNextjsTracing, -} from './utils/tracingUtils'; -import { vercelWaitUntil } from './utils/vercelWaitUntil'; + +import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; + +import { isRedirectNavigationError } from './nextNavigationErrorUtils'; +import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; /** * Wraps a Next.js App Router Route handler with Sentry error and performance instrumentation. @@ -34,74 +30,51 @@ export function wrapRouteHandlerWithSentry any>( const { method, parameterizedRoute, headers } = context; return new Proxy(routeHandler, { - apply: (originalFunction, thisArg, args) => { - return escapeNextjsTracing(() => { - const isolationScope = commonObjectToIsolationScope(headers); + apply: async (originalFunction, thisArg, args) => { + const isolationScope = commonObjectToIsolationScope(headers); - const completeHeadersDict: Record = headers ? winterCGHeadersToDict(headers) : {}; + const completeHeadersDict: Record = headers ? winterCGHeadersToDict(headers) : {}; - isolationScope.setSDKProcessingMetadata({ - request: { - headers: completeHeadersDict, - }, - }); + isolationScope.setSDKProcessingMetadata({ + request: { + headers: completeHeadersDict, + }, + }); - const incomingPropagationContext = propagationContextFromHeaders( - completeHeadersDict['sentry-trace'], - completeHeadersDict['baggage'], - ); + const incomingPropagationContext = propagationContextFromHeaders( + completeHeadersDict['sentry-trace'], + completeHeadersDict['baggage'], + ); - const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); + const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); - return withIsolationScope(isolationScope, () => { - return withScope(async scope => { - scope.setTransactionName(`${method} ${parameterizedRoute}`); - scope.setPropagationContext(propagationContext); - try { - return startSpan( - { - name: `${method} ${parameterizedRoute}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - }, - forceTransaction: true, - }, - async span => { - 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) && span) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else { - captureException(error, { - mechanism: { - handled: false, - }, - }); - } - }, - ); + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + rootSpan.setAttribute('sentry.route_handler', true); + const { scope } = getCapturedScopesOnSpan(rootSpan); + setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); + } - try { - if (span && response.status) { - setHttpStatus(span, response.status); - } - } catch { - // best effort - response may be undefined? - } - - return response; - }, - ); - } finally { - vercelWaitUntil(flushSafelyWithTimeout()); - } - }); + return withIsolationScope(isolationScope, () => { + return withScope(scope => { + scope.setTransactionName(`${method} ${parameterizedRoute}`); + scope.setPropagationContext(propagationContext); + return 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 { + captureException(error, { + mechanism: { + handled: false, + }, + }); + } + }, + ); }); }); }, diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 1bfc57b44418..96b5f798d56d 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -196,7 +196,10 @@ export function init(options: NodeOptions): NodeClient | undefined { // We want to rename these spans because they look like "GET /path/to/route" and we already emit spans that look // like this with our own http instrumentation. if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest') { - span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this. + const rootSpan = getRootSpan(span); + if (span !== rootSpan) { + span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this. + } } }); @@ -211,11 +214,13 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } - // We only want to use our HTTP integration/instrumentation for app router requests, which are marked with the `sentry.rsc` attribute. + // We only want to use our HTTP integration/instrumentation for app router requests, + // which are marked with the `sentry.rsc` or `sentry.route_handler` attribute. if ( (event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.http' || event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest') && - event.contexts?.trace?.data?.['sentry.rsc'] !== true + event.contexts?.trace?.data?.['sentry.rsc'] !== true && + event.contexts?.trace?.data?.['sentry.route_handler'] !== true ) { return null; } @@ -297,13 +302,26 @@ export function init(options: NodeOptions): NodeClient | undefined { event.type === 'transaction' && event.transaction?.match(/^(RSC )?GET /) && event.contexts?.trace?.data?.['sentry.rsc'] === true && - !event.contexts.trace.op + !event.contexts?.trace?.op ) { event.contexts.trace.data = event.contexts.trace.data || {}; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; event.contexts.trace.op = 'http.server'; } + // Enhance route handler transactions + if (event.type === 'transaction' && event.contexts?.trace?.data?.['sentry.route_handler'] === true) { + event.contexts.trace.data = event.contexts.trace.data || {}; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; + event.contexts.trace.op = 'http.server'; + if (typeof event.contexts.trace.data[ATTR_HTTP_ROUTE] === 'string') { + // eslint-disable-next-line deprecation/deprecation + event.transaction = `${event.contexts.trace.data[SEMATTRS_HTTP_METHOD]} ${event.contexts.trace.data[ + ATTR_HTTP_ROUTE + ].replace(/\/route$/, '')}`; + } + } + return event; }) satisfies EventProcessor, { id: 'NextjsTransactionEnhancer' },