diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts new file mode 100644 index 000000000000..7cd1fc7e332c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; + +export const runtime = 'edge'; +export const dynamic = 'force-dynamic'; + +export async function GET() { + return NextResponse.json({ message: 'Hello Edge Route Handler' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts new file mode 100644 index 000000000000..5bc418f077aa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + return NextResponse.json({ message: 'Hello Node Route Handler' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts index 3cad4a546508..c9a9fc44bdba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts @@ -24,11 +24,6 @@ test('App router transactions should be attached to the pageload request span', }); test('extracts HTTP request headers as span attributes', async ({ baseURL }) => { - test.skip( - process.env.TEST_ENV === 'prod-turbopack' || process.env.TEST_ENV === 'dev-turbopack', - 'Incoming fetch request headers are not added as span attributes when Turbopack is enabled (addHeadersAsAttributes)', - ); - const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent?.transaction === 'GET /pageload-tracing'; }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts new file mode 100644 index 000000000000..f9dedccb4923 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts @@ -0,0 +1,40 @@ +import test, { expect } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should create a transaction for node route handlers', async ({ request }) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + console.log(transactionEvent?.transaction); + return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/node'; + }); + + const response = await request.get('/route-handler/123/node', { headers: { 'x-charly': 'gomez' } }); + expect(await response.json()).toStrictEqual({ message: 'Hello Node Route Handler' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + + // This is flaking on dev mode + if (process.env.TEST_ENV !== 'development' && process.env.TEST_ENV !== 'dev-turbopack') { + expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez'); + } +}); + +test('Should create a transaction for edge route handlers', async ({ request }) => { + // This test only works for webpack builds on non-async param extraction + // todo: check if we can set request headers for edge on sdkProcessingMetadata + test.skip(); + const routehandlerTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/edge'; + }); + + const response = await request.get('/route-handler/123/edge', { headers: { 'x-charly': 'gomez' } }); + expect(await response.json()).toStrictEqual({ message: 'Hello Edge Route Handler' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez'); +}); diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 4cf8fde751fb..8f02df798f84 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -15,7 +15,6 @@ import { } from '@sentry/core'; import type { NextApiRequest } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; -import { addHeadersAsAttributes } from '../utils/addHeadersAsAttributes'; import { flushSafelyWithTimeout } from '../utils/responseEnd'; import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils'; @@ -88,7 +87,6 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', - ...addHeadersAsAttributes(normalizedRequest.headers || {}), }, }, async span => { diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 323d4d1f2e3b..c22910df43bf 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -22,7 +22,6 @@ import { import type { GenerationFunctionContext } from '../common/types'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; -import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils'; @@ -64,11 +63,6 @@ export function wrapGenerationFunctionWithSentry a const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - addHeadersAsAttributes(headers, rootSpan); - } - let data: Record | undefined = undefined; if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index ab05fbd5e944..07694d659e57 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -13,7 +13,6 @@ import { winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; -import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import type { EdgeRouteHandler } from '../edge/types'; @@ -60,7 +59,6 @@ export function wrapMiddlewareWithSentry( let spanName: string; let spanSource: TransactionSource; - let headerAttributes: Record = {}; if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ @@ -68,8 +66,6 @@ export function wrapMiddlewareWithSentry( }); spanName = `middleware ${req.method} ${new URL(req.url).pathname}`; spanSource = 'url'; - - headerAttributes = addHeadersAsAttributes(req.headers); } else { spanName = 'middleware'; spanSource = 'component'; @@ -88,7 +84,6 @@ export function wrapMiddlewareWithSentry( const rootSpan = getRootSpan(activeSpan); if (rootSpan) { setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope); - rootSpan.setAttributes(headerAttributes); } } @@ -99,7 +94,6 @@ export function wrapMiddlewareWithSentry( attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrap_middleware', - ...headerAttributes, }, }, () => { diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index 54858a9bdae2..068ab7960ae4 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -19,7 +19,6 @@ import { } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import type { RouteHandlerContext } from './types'; -import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope } from './utils/tracingUtils'; @@ -40,10 +39,6 @@ export function wrapRouteHandlerWithSentry any>( const activeSpan = getActiveSpan(); const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; - if (rootSpan && process.env.NEXT_RUNTIME !== 'edge') { - addHeadersAsAttributes(headers, rootSpan); - } - let edgeRuntimeIsolationScopeOverride: Scope | undefined; if (rootSpan && process.env.NEXT_RUNTIME === 'edge') { const isolationScope = commonObjectToIsolationScope(headers); @@ -55,7 +50,6 @@ export function wrapRouteHandlerWithSentry any>( rootSpan.updateName(`${method} ${parameterizedRoute}`); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); - addHeadersAsAttributes(headers, rootSpan); } return withIsolationScope( diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index d25225a149f9..1f522dbf212f 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -24,7 +24,6 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/ import type { ServerComponentContext } from '../common/types'; import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; -import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils'; @@ -62,11 +61,6 @@ export function wrapServerComponentWithSentry any> const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - addHeadersAsAttributes(context.headers, rootSpan); - } - let params: Record | undefined = undefined; if (getClient()?.getOptions().sendDefaultPii) { diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 7982667f0c3f..6469e3c6a2c8 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,6 +1,7 @@ import { applySdkMetadata, getGlobalScope, + getIsolationScope, getRootSpan, GLOBAL_OBJ, registerSpanErrorInstrumentation, @@ -13,6 +14,7 @@ import { } from '@sentry/core'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; +import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { isBuild } from '../common/utils/isBuild'; import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; @@ -59,6 +61,8 @@ export function init(options: VercelEdgeOptions = {}): void { client?.on('spanStart', span => { const spanAttributes = spanToJSON(span).data; + const rootSpan = getRootSpan(span); + const isRootSpan = span === rootSpan; // Mark all spans generated by Next.js as 'auto' if (spanAttributes?.['next.span_type'] !== undefined) { @@ -70,6 +74,12 @@ export function init(options: VercelEdgeOptions = {}): void { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware'); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); } + + if (isRootSpan) { + // todo: check if we can set request headers for edge on sdkProcessingMetadata + const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; + addHeadersAsAttributes(headers, rootSpan); + } }); // Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 735caf8d7788..9d3d4e4427fa 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -39,7 +39,6 @@ export function wrapApiHandlerWithSentry( normalizedRequest: winterCGRequestToRequestData(req), }); currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`); - headerAttributes = addHeadersAsAttributes(req.headers); } else { currentScope.setTransactionName(`handler (${parameterizedRoute})`); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 5866f014ec69..5ce23e6a9460 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -36,6 +36,7 @@ import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, } from '../common/span-attributes-with-logic-attached'; +import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { isBuild } from '../common/utils/isBuild'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; @@ -163,13 +164,13 @@ export function init(options: NodeOptions): NodeClient | undefined { client?.on('spanStart', span => { const spanAttributes = spanToJSON(span).data; + const rootSpan = getRootSpan(span); + const isRootSpan = span === rootSpan; // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. if (typeof spanAttributes?.['next.route'] === 'string') { - const rootSpan = getRootSpan(span); const rootSpanAttributes = spanToJSON(rootSpan).data; - // Only hoist the http.route attribute if the transaction doesn't already have it if ( // eslint-disable-next-line deprecation/deprecation @@ -190,8 +191,13 @@ export function init(options: NodeOptions): NodeClient | undefined { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); } + if (isRootSpan) { + const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; + addHeadersAsAttributes(headers, rootSpan); + } + // We want to fork the isolation scope for incoming requests - if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && span === getRootSpan(span)) { + if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && isRootSpan) { const scopes = getCapturedScopesOnSpan(span); const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();