From c11cde7d52b99cdb0dd28b3360079dc4b836c067 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 14:24:46 +0200 Subject: [PATCH 01/13] attach headers in event processor --- packages/nextjs/src/server/index.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 5866f014ec69..e2dd658247e8 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -11,6 +11,7 @@ import { applySdkMetadata, debug, extractTraceparentData, + getActiveSpan, getCapturedScopesOnSpan, getClient, getCurrentScope, @@ -36,6 +37,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'; @@ -307,6 +309,26 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; + if (event.type === 'transaction' && event.sdkProcessingMetadata?.normalizedRequest && shouldSendDefaultPii) { + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; + if (rootSpan) { + addHeadersAsAttributes(event.sdkProcessingMetadata.normalizedRequest.headers, rootSpan); + } + + return event; + } + + return event; + }) satisfies EventProcessor, + { id: 'HttpRequestEnhancer' }, + ), + ); + // Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most // up-to-date value, but also so that the logic that detects changes to the transaction names to set the source to // "custom", doesn't trigger. From adc00a6f07896ebcd1df03757bdd2f4c5bd4b94b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 14:49:40 +0200 Subject: [PATCH 02/13] un-skip tests --- .../nextjs-15/tests/pageload-tracing.test.ts | 5 ----- 1 file changed, 5 deletions(-) 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'; }); From 16745d3be9b906c24204271cfaae5fd78338b712 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 15:44:45 +0200 Subject: [PATCH 03/13] use client hook instead --- packages/nextjs/src/server/index.ts | 30 ++++++++--------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index e2dd658247e8..f2a3b68214a5 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -165,13 +165,12 @@ export function init(options: NodeOptions): NodeClient | undefined { client?.on('spanStart', span => { const spanAttributes = spanToJSON(span).data; + const rootSpan = getRootSpan(span); // 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 @@ -192,6 +191,13 @@ export function init(options: NodeOptions): NodeClient | undefined { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); } + // Add headers to the root span if the client options allow it + const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; + if (shouldSendDefaultPii && rootSpan) { + 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)) { const scopes = getCapturedScopesOnSpan(span); @@ -309,26 +315,6 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; - if (event.type === 'transaction' && event.sdkProcessingMetadata?.normalizedRequest && shouldSendDefaultPii) { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; - if (rootSpan) { - addHeadersAsAttributes(event.sdkProcessingMetadata.normalizedRequest.headers, rootSpan); - } - - return event; - } - - return event; - }) satisfies EventProcessor, - { id: 'HttpRequestEnhancer' }, - ), - ); - // Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most // up-to-date value, but also so that the logic that detects changes to the transaction names to set the source to // "custom", doesn't trigger. From 1ec0da167ded9a5e299acc4eb92564ac9e40aeb5 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 15:46:47 +0200 Subject: [PATCH 04/13] . --- packages/nextjs/src/server/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index f2a3b68214a5..5b6eda3dc7e2 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -11,7 +11,6 @@ import { applySdkMetadata, debug, extractTraceparentData, - getActiveSpan, getCapturedScopesOnSpan, getClient, getCurrentScope, From 1a3f402105b94d1d4febc46bce7e8c5ba77cfbb7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 21:05:22 +0200 Subject: [PATCH 05/13] remove headers logic from wrappers --- .../wrapApiHandlerWithSentry.ts | 2 -- .../nextjs/src/common/wrapGenerationFunctionWithSentry.ts | 6 ------ packages/nextjs/src/common/wrapMiddlewareWithSentry.ts | 5 +---- packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts | 6 ------ packages/nextjs/src/common/wrapServerComponentWithSentry.ts | 6 ------ packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts | 5 +---- 6 files changed, 2 insertions(+), 28 deletions(-) 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..8a2e7661300d 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,7 @@ export function wrapMiddlewareWithSentry( let spanName: string; let spanSource: TransactionSource; - let headerAttributes: Record = {}; + const headerAttributes: Record = {}; if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ @@ -68,8 +67,6 @@ export function wrapMiddlewareWithSentry( }); spanName = `middleware ${req.method} ${new URL(req.url).pathname}`; spanSource = 'url'; - - headerAttributes = addHeadersAsAttributes(req.headers); } else { spanName = 'middleware'; spanSource = 'component'; 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/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 735caf8d7788..9f839dc8e662 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.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 './types'; @@ -32,15 +31,13 @@ export function wrapApiHandlerWithSentry( const req: unknown = args[0]; const currentScope = getCurrentScope(); - let headerAttributes: Record = {}; + const headerAttributes: Record = {}; if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(req), }); currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`); - - headerAttributes = addHeadersAsAttributes(req.headers); } else { currentScope.setTransactionName(`handler (${parameterizedRoute})`); } From be665a327cf385797969064195a52803c2d85a25 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 23:10:25 +0200 Subject: [PATCH 06/13] add route handler tests --- .../app/route-handler/[xoxo]/edge/route.ts | 8 ++++ .../app/route-handler/[xoxo]/node/route.ts | 7 ++++ .../nextjs-15/tests/route-handler.test.ts | 38 +++++++++++++++++++ packages/nextjs/src/server/index.ts | 7 ++-- 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts 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/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts new file mode 100644 index 000000000000..f7e4bc9b2c64 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts @@ -0,0 +1,38 @@ +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 }) => { + 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'); + // todo: check if we can set request headers for edge on sdkProcessingMetadata + // expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x-charly']).toBe('gomez'); +}); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 5b6eda3dc7e2..8dddcd720649 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -165,6 +165,7 @@ 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. @@ -192,13 +193,13 @@ export function init(options: NodeOptions): NodeClient | undefined { // Add headers to the root span if the client options allow it const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; - if (shouldSendDefaultPii && rootSpan) { - const headers = getIsolationScope().getScopeData().sdkProcessingMetadata.normalizedRequest?.headers; + if (shouldSendDefaultPii && 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(); From d566af57a9eb52e0aa7c1d7541a01417215a17bf Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 30 Sep 2025 23:44:11 +0200 Subject: [PATCH 07/13] edge hurts --- .../nextjs-15/tests/route-handler.test.ts | 2 +- packages/nextjs/src/edge/index.ts | 12 ++++++++++++ packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts | 4 +++- 3 files changed, 16 insertions(+), 2 deletions(-) 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 index f7e4bc9b2c64..1907c9976f5e 100644 --- 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 @@ -34,5 +34,5 @@ test('Should create a transaction for edge route handlers', async ({ request }) expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); // todo: check if we can set request headers for edge on sdkProcessingMetadata - // expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x-charly']).toBe('gomez'); + // expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez'); }); diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 7982667f0c3f..a14b4a9416d9 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,6 +1,8 @@ import { applySdkMetadata, + getClient, getGlobalScope, + getIsolationScope, getRootSpan, GLOBAL_OBJ, registerSpanErrorInstrumentation, @@ -13,6 +15,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 +62,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 +75,13 @@ export function init(options: VercelEdgeOptions = {}): void { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware'); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); } + + const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; + if (shouldSendDefaultPii && 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 9f839dc8e662..9d3d4e4427fa 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -13,6 +13,7 @@ import { winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; +import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import type { EdgeRouteHandler } from './types'; @@ -31,13 +32,14 @@ export function wrapApiHandlerWithSentry( const req: unknown = args[0]; const currentScope = getCurrentScope(); - const headerAttributes: Record = {}; + let headerAttributes: Record = {}; if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(req), }); currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`); + headerAttributes = addHeadersAsAttributes(req.headers); } else { currentScope.setTransactionName(`handler (${parameterizedRoute})`); } From 3a211de683234a1bc7a5e685a887764c3c521056 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 1 Oct 2025 00:03:00 +0200 Subject: [PATCH 08/13] . --- .../test-applications/nextjs-15/tests/route-handler.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index 1907c9976f5e..f9dedccb4923 100644 --- 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 @@ -22,6 +22,9 @@ test('Should create a transaction for node route handlers', async ({ request }) }); 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'; }); @@ -33,6 +36,5 @@ test('Should create a transaction for edge route handlers', async ({ request }) expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); - // todo: check if we can set request headers for edge on sdkProcessingMetadata - // expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez'); + expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez'); }); From ab16ac79cfce32120a6394a8718f91e573ef3cac Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 1 Oct 2025 13:35:20 +0200 Subject: [PATCH 09/13] remove double check for sendDefaultPii --- packages/nextjs/src/edge/index.ts | 3 +-- packages/nextjs/src/server/index.ts | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index a14b4a9416d9..cf36591a8cae 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -76,8 +76,7 @@ export function init(options: VercelEdgeOptions = {}): void { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); } - const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; - if (shouldSendDefaultPii && isRootSpan) { + if (isRootSpan) { // todo: check if we can set request headers for edge on sdkProcessingMetadata const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; addHeadersAsAttributes(headers, rootSpan); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 8dddcd720649..5ce23e6a9460 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -191,9 +191,7 @@ export function init(options: NodeOptions): NodeClient | undefined { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); } - // Add headers to the root span if the client options allow it - const shouldSendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; - if (shouldSendDefaultPii && isRootSpan) { + if (isRootSpan) { const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; addHeadersAsAttributes(headers, rootSpan); } From 00ac5f0443f2ebee92aa35cfa67faa93795adaa9 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 1 Oct 2025 13:36:25 +0200 Subject: [PATCH 10/13] early return --- packages/nextjs/src/common/utils/addHeadersAsAttributes.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts index 4e8cdb3fe7c9..8c16dc638d4d 100644 --- a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts +++ b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts @@ -15,6 +15,10 @@ export function addHeadersAsAttributes( const client = getClient(); const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false; + if (!sendDefaultPii) { + return {}; + } + const headersDict: Record = headers instanceof Headers || (typeof headers === 'object' && 'get' in headers) ? winterCGHeadersToDict(headers as Headers) From ee86bf50b207187c161919fad7725361b34d0b49 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 1 Oct 2025 15:14:14 +0200 Subject: [PATCH 11/13] lint --- packages/nextjs/src/edge/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index cf36591a8cae..6469e3c6a2c8 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,6 +1,5 @@ import { applySdkMetadata, - getClient, getGlobalScope, getIsolationScope, getRootSpan, From acdb953ecc5248433a7bf0ae27cb8db8b5b72c4a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 1 Oct 2025 16:42:53 +0200 Subject: [PATCH 12/13] . --- packages/nextjs/src/common/utils/addHeadersAsAttributes.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts index 8c16dc638d4d..4e8cdb3fe7c9 100644 --- a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts +++ b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts @@ -15,10 +15,6 @@ export function addHeadersAsAttributes( const client = getClient(); const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false; - if (!sendDefaultPii) { - return {}; - } - const headersDict: Record = headers instanceof Headers || (typeof headers === 'object' && 'get' in headers) ? winterCGHeadersToDict(headers as Headers) From d727ee9f46ada5908190d44c53d03b154fff6990 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 1 Oct 2025 17:03:54 +0200 Subject: [PATCH 13/13] removed unused header obj --- packages/nextjs/src/common/wrapMiddlewareWithSentry.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index 8a2e7661300d..07694d659e57 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -59,7 +59,6 @@ export function wrapMiddlewareWithSentry( let spanName: string; let spanSource: TransactionSource; - const headerAttributes: Record = {}; if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ @@ -85,7 +84,6 @@ export function wrapMiddlewareWithSentry( const rootSpan = getRootSpan(activeSpan); if (rootSpan) { setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope); - rootSpan.setAttributes(headerAttributes); } } @@ -96,7 +94,6 @@ export function wrapMiddlewareWithSentry( attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrap_middleware', - ...headerAttributes, }, }, () => {