From e2fef71a633a2c4588de0c21317e7d80b80dfbe7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 11:03:12 +0100 Subject: [PATCH 01/11] feat(nextjs): remove tracing from pages router API routes --- .../nextjs-13/tests/client/sessions.test.ts | 1 + .../tests/server/cjs-api-endpoints.test.ts | 12 +- .../server/pages-router-api-endpoints.test.ts | 12 +- .../server/wrapApiHandlerWithSentry.test.ts | 4 +- .../wrapApiHandlerWithSentry.ts | 124 ++++++------------ .../nextjs/test/config/withSentry.test.ts | 54 -------- 6 files changed, 52 insertions(+), 155 deletions(-) delete mode 100644 packages/nextjs/test/config/withSentry.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts index 8fbe8ac8b7b5..5ed4500928e7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts @@ -5,6 +5,7 @@ test('should report healthy sessions', async ({ page }) => { test.skip(process.env.TEST_ENV === 'development', 'test is flakey in dev mode'); const sessionPromise = waitForSession('nextjs-13', session => { + console.log('session', session); return session.init === true && session.status === 'ok' && session.errors === 0; }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts index 28cc91e9b879..9f07e32648a1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts @@ -39,12 +39,12 @@ test('should create a transaction for a CJS pages router API endpoint', async ({ data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.nextjs', + 'sentry.origin': 'auto', 'sentry.sample_rate': 1, 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.http.nextjs', + origin: 'auto', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -57,7 +57,7 @@ test('should create a transaction for a CJS pages router API endpoint', async ({ cookies: expect.any(Object), headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint$/), + url: expect.stringMatching(/\/api\/cjs-api-endpoint$/), }, spans: expect.arrayContaining([]), start_timestamp: expect.any(Number), @@ -102,12 +102,12 @@ test('should not mess up require statements in CJS API endpoints', async ({ requ data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.nextjs', + 'sentry.origin': 'auto', 'sentry.sample_rate': 1, 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.http.nextjs', + origin: 'auto', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -120,7 +120,7 @@ test('should not mess up require statements in CJS API endpoints', async ({ requ cookies: expect.any(Object), headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint-with-require$/), + url: expect.stringMatching(/\/api\/cjs-api-endpoint-with-require$/), }, spans: expect.arrayContaining([]), start_timestamp: expect.any(Number), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts index 9f5ff5db8434..bea87cdd8992 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -55,11 +55,11 @@ test('Should report an error event for errors thrown in pages router api routes' data: { 'http.response.status_code': 500, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.nextjs', + 'sentry.origin': 'auto', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.http.nextjs', + origin: 'auto', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'internal_error', trace_id: (await errorEventPromise).contexts?.trace?.trace_id, @@ -69,7 +69,7 @@ test('Should report an error event for errors thrown in pages router api routes' request: { headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/^http.*\/api\/foo\/failure-api-route$/), + url: expect.stringMatching(/^\/api\/foo\/failure-api-route$/), }, start_timestamp: expect.any(Number), timestamp: expect.any(Number), @@ -98,11 +98,11 @@ test('Should report a transaction event for a successful pages router api route' data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.nextjs', + 'sentry.origin': 'auto', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.http.nextjs', + origin: 'auto', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -112,7 +112,7 @@ test('Should report a transaction event for a successful pages router api route' request: { headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/^http.*\/api\/foo\/success-api-route$/), + url: expect.stringMatching(/^\/api\/foo\/success-api-route$/), }, start_timestamp: expect.any(Number), timestamp: expect.any(Number), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts index 798ea3409089..1f0e788fc8a4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts @@ -39,11 +39,11 @@ cases.forEach(({ name, url, transactionName }) => { data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.nextjs', + 'sentry.origin': 'auto', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.http.nextjs', + origin: 'auto', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 60a9b0d617f7..dbe763d408eb 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -1,21 +1,14 @@ import { captureException, - continueTrace, debug, - getActiveSpan, + getCurrentScope, + getIsolationScope, httpRequestToRequestData, - isString, objectify, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - setHttpStatus, - startSpanManual, - withIsolationScope, } from '@sentry/core'; import type { NextApiRequest } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; -import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd'; -import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils'; +import { flushSafelyWithTimeout } from '../utils/responseEnd'; export type AugmentedNextApiRequest = NextApiRequest & { __withSentry_applied__?: boolean; @@ -31,15 +24,13 @@ export type AugmentedNextApiRequest = NextApiRequest & { */ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameterizedRoute: string): NextApiHandler { return new Proxy(apiHandler, { - apply: ( + apply: async ( wrappingTarget, thisArg, args: [AugmentedNextApiRequest | undefined, AugmentedNextApiResponse | undefined], ) => { - dropNextjsRootContext(); - return escapeNextjsTracing(() => { + try { const [req, res] = args; - if (!req) { debug.log( `Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`, @@ -56,86 +47,45 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz if (req.__withSentry_applied__) { return wrappingTarget.apply(thisArg, args); } - req.__withSentry_applied__ = true; - - return withIsolationScope(isolationScope => { - // Normally, there is an active span here (from Next.js OTEL) and we just use that as parent - // Else, we manually continueTrace from the incoming headers - const continueTraceIfNoActiveSpan = getActiveSpan() - ? (_opts: unknown, callback: () => T) => callback() - : continueTrace; - - return continueTraceIfNoActiveSpan( - { - sentryTrace: - req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined, - baggage: req.headers?.baggage, - }, - () => { - const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - const normalizedRequest = httpRequestToRequestData(req); - isolationScope.setSDKProcessingMetadata({ normalizedRequest }); - isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`); - - return startSpanManual( - { - name: `${reqMethod}${parameterizedRoute}`, - op: 'http.server', - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', - }, - }, - async span => { - // eslint-disable-next-line @typescript-eslint/unbound-method - res.end = new Proxy(res.end, { - apply(target, thisArg, argArray) { - setHttpStatus(span, res.statusCode); - span.end(); - waitUntil(flushSafelyWithTimeout()); - return target.apply(thisArg, argArray); - }, - }); - try { - return await wrappingTarget.apply(thisArg, args); - } catch (e) { - // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can - // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced - // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a - // way to prevent it from actually being reported twice.) - const objectifiedErr = objectify(e); + req.__withSentry_applied__ = true; - captureException(objectifiedErr, { - mechanism: { - type: 'auto.http.nextjs.api_handler', - handled: false, - data: { - wrapped_handler: wrappingTarget.name, - function: 'withSentry', - }, - }, - }); + // Set transaction name even without tracing to ensure parameterized routes are used + const method = req.method || 'GET'; + getCurrentScope().setTransactionName(`${method} ${parameterizedRoute}`); - setHttpStatus(span, 500); - span.end(); + // Set SDK processing metadata for session tracking (needed even without tracing) + const normalizedRequest = httpRequestToRequestData(req); + getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); - // we need to await the flush here to ensure that the error is captured - // as the runtime freezes as soon as the error is thrown below - await flushSafelyWithTimeout(); + return await wrappingTarget.apply(thisArg, args); + } catch (e) { + // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can + // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced + // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a + // way to prevent it from actually being reported twice.) + const objectifiedErr = objectify(e); - // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it - // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark - // the error as already having been captured.) - throw objectifiedErr; - } - }, - ); + captureException(objectifiedErr, { + mechanism: { + type: 'auto.http.nextjs.api_handler', + handled: false, + data: { + wrapped_handler: wrappingTarget.name, + function: 'withSentry', }, - ); + }, }); - }); + + // we need to await the flush here to ensure that the error is captured + // as the runtime freezes as soon as the error is thrown below + await flushSafelyWithTimeout(); + + // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it + // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark + // the error as already having been captured.) + throw objectifiedErr; + } }, }); } diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts deleted file mode 100644 index 3ed6672393ea..000000000000 --- a/packages/nextjs/test/config/withSentry.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types'; -import { wrapApiHandlerWithSentry } from '../../src/server'; - -const startSpanManualSpy = vi.spyOn(SentryCore, 'startSpanManual'); - -describe('withSentry', () => { - let req: NextApiRequest, res: NextApiResponse; - - const origHandlerNoError: NextApiHandler = async (_req, res) => { - res.send('Good dog, Maisey!'); - }; - - const wrappedHandlerNoError = wrapApiHandlerWithSentry(origHandlerNoError, '/my-parameterized-route'); - - beforeEach(() => { - req = { url: 'http://dogs.are.great' } as NextApiRequest; - res = { - send: function (this: AugmentedNextApiResponse) { - this.end(); - }, - end: function (this: AugmentedNextApiResponse) { - // eslint-disable-next-line deprecation/deprecation - this.finished = true; - // @ts-expect-error This is a mock - this.writableEnded = true; - }, - } as unknown as AugmentedNextApiResponse; - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('tracing', () => { - it('starts a transaction when tracing is enabled', async () => { - await wrappedHandlerNoError(req, res); - expect(startSpanManualSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'GET /my-parameterized-route', - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', - }, - }), - expect.any(Function), - ); - }); - }); -}); From 6fce173b32c93dc073109c355caecb8e7da6c5b1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 11:11:00 +0100 Subject: [PATCH 02/11] fix: metadata processing --- .../nextjs-13/tests/client/sessions.test.ts | 1 - .../tests/server/pages-router-api-endpoints.test.ts | 4 ++-- .../wrapApiHandlerWithSentry.ts | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts index 5ed4500928e7..8fbe8ac8b7b5 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts @@ -5,7 +5,6 @@ test('should report healthy sessions', async ({ page }) => { test.skip(process.env.TEST_ENV === 'development', 'test is flakey in dev mode'); const sessionPromise = waitForSession('nextjs-13', session => { - console.log('session', session); return session.init === true && session.status === 'ok' && session.errors === 0; }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts index bea87cdd8992..de50ceee1076 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -69,7 +69,7 @@ test('Should report an error event for errors thrown in pages router api routes' request: { headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/^\/api\/foo\/failure-api-route$/), + url: expect.stringMatching(/^http.*\/api\/foo\/failure-api-route$/), }, start_timestamp: expect.any(Number), timestamp: expect.any(Number), @@ -112,7 +112,7 @@ test('Should report a transaction event for a successful pages router api route' request: { headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/^\/api\/foo\/success-api-route$/), + url: expect.stringMatching(/^http.*\/api\/foo\/success-api-route$/), }, start_timestamp: expect.any(Number), timestamp: expect.any(Number), diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index dbe763d408eb..96ca814eb0b2 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -54,9 +54,10 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz const method = req.method || 'GET'; getCurrentScope().setTransactionName(`${method} ${parameterizedRoute}`); - // Set SDK processing metadata for session tracking (needed even without tracing) - const normalizedRequest = httpRequestToRequestData(req); - getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); + // Set SDK processing metadata + getIsolationScope().setSDKProcessingMetadata({ + normalizedRequest: httpRequestToRequestData(req), + }); return await wrappingTarget.apply(thisArg, args); } catch (e) { From ec986a8c06490912b15537ac4b5f0ee988c63d4f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 11:47:37 +0100 Subject: [PATCH 03/11] fix: set the transaction name on the isolation scope level --- .../wrapApiHandlerWithSentry.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 96ca814eb0b2..67ae32213418 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -1,11 +1,4 @@ -import { - captureException, - debug, - getCurrentScope, - getIsolationScope, - httpRequestToRequestData, - objectify, -} from '@sentry/core'; +import { captureException, debug, getIsolationScope, httpRequestToRequestData, objectify } from '@sentry/core'; import type { NextApiRequest } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; import { flushSafelyWithTimeout } from '../utils/responseEnd'; @@ -50,12 +43,13 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz req.__withSentry_applied__ = true; - // Set transaction name even without tracing to ensure parameterized routes are used + // Set transaction name on isolation scope to ensure parameterized routes are used + // The HTTP server integration sets it on isolation scope, so we need to match that const method = req.method || 'GET'; - getCurrentScope().setTransactionName(`${method} ${parameterizedRoute}`); - + const isolationScope = getIsolationScope(); + isolationScope.setTransactionName(`${method} ${parameterizedRoute}`); // Set SDK processing metadata - getIsolationScope().setSDKProcessingMetadata({ + isolationScope.setSDKProcessingMetadata({ normalizedRequest: httpRequestToRequestData(req), }); From 31fac7a4ef0fdb6d2e105ae6b9ab459e3c8c24af Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 12:57:38 +0100 Subject: [PATCH 04/11] tests: try without paramaterization --- .../nextjs-13/tests/server/pages-router-api-endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts index de50ceee1076..b32ab00bb021 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -8,7 +8,7 @@ test('Should report an error event for errors thrown in pages router api routes' const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { return ( - transactionEvent.transaction === 'GET /api/[param]/failure-api-route' && + transactionEvent.transaction === 'GET /api/foo/failure-api-route' && transactionEvent.contexts?.trace?.op === 'http.server' ); }); From eb4c84d3472cc680e4d67e4006f6b8123e4e9c5c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 15:54:06 +0100 Subject: [PATCH 05/11] fix: parameterization backfill --- .../server/pages-router-api-endpoints.test.ts | 2 +- .../wrapApiHandlerWithSentry.ts | 19 ++++++++++++++++++- packages/nextjs/src/server/index.ts | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts index b32ab00bb021..de50ceee1076 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -8,7 +8,7 @@ test('Should report an error event for errors thrown in pages router api routes' const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { return ( - transactionEvent.transaction === 'GET /api/foo/failure-api-route' && + transactionEvent.transaction === 'GET /api/[param]/failure-api-route' && transactionEvent.contexts?.trace?.op === 'http.server' ); }); diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 67ae32213418..2016c79922cd 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -1,5 +1,14 @@ -import { captureException, debug, getIsolationScope, httpRequestToRequestData, objectify } from '@sentry/core'; +import { + captureException, + debug, + getActiveSpan, + getIsolationScope, + getRootSpan, + httpRequestToRequestData, + objectify, +} from '@sentry/core'; import type { NextApiRequest } from 'next'; +import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; import { flushSafelyWithTimeout } from '../utils/responseEnd'; @@ -53,6 +62,14 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz normalizedRequest: httpRequestToRequestData(req), }); + // Set the route backfill attribute on the root span so that the transaction name + // gets updated to use the parameterized route during event processing + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL, parameterizedRoute); + } + return await wrappingTarget.apply(thisArg, args); } catch (e) { // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index bc5372274ad6..9670a8713a4f 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -368,6 +368,7 @@ export function init(options: NodeOptions): NodeClient | undefined { // backfill transaction name for pages that would otherwise contain unparameterized routes if (event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL] && event.transaction !== 'GET /_app') { event.transaction = `${method} ${event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL]}`; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; } const middlewareMatch = From 596150fe045c36d0f5299ba80a4dad9850d27113 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 16:29:11 +0100 Subject: [PATCH 06/11] test: update expectations and fix lint issues --- .../create-next-app/tests/server-transactions.test.ts | 4 ++-- packages/nextjs/src/server/index.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts index 731d1820ee61..dc300e4f0cb2 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts @@ -22,11 +22,11 @@ test('Sends server-side transactions to Sentry', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), op: 'http.server', - origin: 'auto.http.nextjs', + origin: 'auto', data: expect.objectContaining({ 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.nextjs', + 'sentry.origin': 'auto', 'sentry.sample_rate': 1, 'sentry.source': 'route', }), diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 9670a8713a4f..1b9315cdf706 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -431,4 +431,5 @@ function sdkAlreadyInitialized(): boolean { export * from '../common'; +// eslint-disable-next-line max-lines export { wrapApiHandlerWithSentry } from '../common/pages-router-instrumentation/wrapApiHandlerWithSentry'; From ea4d841fb1b3c7b40ccd2066272832b3bba419ae Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 3 Dec 2025 16:56:30 +0100 Subject: [PATCH 07/11] test: update expectations --- .../create-next-app/tests/server-errors.test.ts | 2 +- .../tests/server-transactions.test.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts index 08a47ace671f..1c825e52947a 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts @@ -24,7 +24,7 @@ test('Sends a server-side exception to Sentry', async ({ baseURL }) => { expect(errorEvent.transaction).toEqual('GET /api/error'); - expect(errorEvent.contexts?.trace).toEqual({ + expect(errorEvent.contexts?.trace).toMatchObject({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts index dc300e4f0cb2..09939c738e0b 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts @@ -33,21 +33,23 @@ test('Sends server-side transactions to Sentry', async ({ baseURL }) => { status: 'ok', }, }), - spans: [ - { + spans: expect.arrayContaining([ + expect.objectContaining({ data: { 'sentry.origin': 'manual', }, description: 'test-span', origin: 'manual', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + // Note: parent_span_id may be the root span or an intermediate "executing api route" span + // depending on Next.js instrumentation, so we just check it exists + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), status: 'ok', timestamp: expect.any(Number), trace_id: transactionEvent.contexts?.trace?.trace_id, - }, - ], + }), + ]), request: { headers: expect.any(Object), method: 'GET', From bdb4be3ce7d26fadb5232c7afd474a3496ecd97f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 9 Dec 2025 17:21:57 +0200 Subject: [PATCH 08/11] feat: drop tracing from edge pages runtime wrapping --- .../src/edge/wrapApiHandlerWithSentry.ts | 104 +++++------------- 1 file changed, 25 insertions(+), 79 deletions(-) diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 528c174e45fa..45418d310bb8 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,23 +1,9 @@ -import { - captureException, - getActiveSpan, - getCurrentScope, - getRootSpan, - handleCallbackErrors, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - setCapturedScopesOnSpan, - startSpan, - winterCGRequestToRequestData, - withIsolationScope, -} from '@sentry/core'; -import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; -import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; +import { captureException, getIsolationScope, winterCGRequestToRequestData } from '@sentry/core'; +import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import type { EdgeRouteHandler } from './types'; /** - * Wraps a Next.js edge route handler with Sentry error and performance instrumentation. + * Wraps a Next.js edge route handler with Sentry error monitoring. */ export function wrapApiHandlerWithSentry( handler: H, @@ -25,80 +11,40 @@ export function wrapApiHandlerWithSentry( ): (...params: Parameters) => Promise> { return new Proxy(handler, { apply: async (wrappingTarget, thisArg, args: Parameters) => { - // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack. - - return withIsolationScope(isolationScope => { + try { const req: unknown = args[0]; - const currentScope = getCurrentScope(); - let headerAttributes: Record = {}; + // Set transaction name on isolation scope to ensure parameterized routes are used + // The HTTP server integration sets it on isolation scope, so we need to match that + const isolationScope = getIsolationScope(); if (req instanceof Request) { + const method = req.method || 'GET'; + isolationScope.setTransactionName(`${method} ${parameterizedRoute}`); + // Set SDK processing metadata isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(req), }); - currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`); - headerAttributes = addHeadersAsAttributes(req.headers); } else { - currentScope.setTransactionName(`handler (${parameterizedRoute})`); + isolationScope.setTransactionName(`handler (${parameterizedRoute})`); } - let spanName: string; - let op: string | undefined = 'http.server'; + return await wrappingTarget.apply(thisArg, args); + } catch (error) { + captureException(error, { + mechanism: { + type: 'auto.function.nextjs.wrap_api_handler', + handled: false, + }, + }); - // If there is an active span, it likely means that the automatic Next.js OTEL instrumentation worked and we can - // rely on that for parameterization. - const activeSpan = getActiveSpan(); - if (activeSpan) { - spanName = `handler (${parameterizedRoute})`; - op = undefined; + // we need to await the flush here to ensure that the error is captured + // as the runtime freezes as soon as the error is thrown below + await flushSafelyWithTimeout(); - const rootSpan = getRootSpan(activeSpan); - if (rootSpan) { - rootSpan.updateName( - req instanceof Request ? `${req.method} ${parameterizedRoute}` : `handler ${parameterizedRoute}`, - ); - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - ...headerAttributes, - }); - setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope); - } - } else if (req instanceof Request) { - spanName = `${req.method} ${parameterizedRoute}`; - } else { - spanName = `handler ${parameterizedRoute}`; - } - - return startSpan( - { - name: spanName, - op: op, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrap_api_handler', - ...headerAttributes, - }, - }, - () => { - return handleCallbackErrors( - () => wrappingTarget.apply(thisArg, args), - error => { - captureException(error, { - mechanism: { - type: 'auto.function.nextjs.wrap_api_handler', - handled: false, - }, - }); - }, - () => { - waitUntil(flushSafelyWithTimeout()); - }, - ); - }, - ); - }); + // We rethrow here so that nextjs can do with the error whatever it would normally do. + throw error; + } }, }); } From ceaa1440927c258a89e8a913f8b29d0840dd853b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 16 Dec 2025 13:31:22 +0200 Subject: [PATCH 09/11] test: fix assertion expectation to expect parent id span --- .../create-next-app/tests/server-errors.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts index 1c825e52947a..c8cedea05193 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-errors.test.ts @@ -24,8 +24,10 @@ test('Sends a server-side exception to Sentry', async ({ baseURL }) => { expect(errorEvent.transaction).toEqual('GET /api/error'); - expect(errorEvent.contexts?.trace).toMatchObject({ + expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + // Will be present since we no longer drop Next.js spans in the wrapper + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); From 03e762741a809bc83c0624b46b72e63868ed7f54 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 16 Dec 2025 14:21:59 +0200 Subject: [PATCH 10/11] tests: adjust assertions to be consistent --- .../tests/server-transactions.test.ts | 3 +-- .../nextjs-13/tests/server/cjs-api-endpoints.test.ts | 12 ++++++------ .../tests/server/pages-router-api-endpoints.test.ts | 8 ++++---- .../nextjs-pages-dir/tests/edge-route.test.ts | 8 ++++++-- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts index 09939c738e0b..db902c46ada8 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts @@ -40,8 +40,7 @@ test('Sends server-side transactions to Sentry', async ({ baseURL }) => { }, description: 'test-span', origin: 'manual', - // Note: parent_span_id may be the root span or an intermediate "executing api route" span - // depending on Next.js instrumentation, so we just check it exists + // Won't be the trace span id because we don't wrap the Next.js span in the wrapper parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts index 9f07e32648a1..28cc91e9b879 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts @@ -39,12 +39,12 @@ test('should create a transaction for a CJS pages router API endpoint', async ({ data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto', + 'sentry.origin': 'auto.http.nextjs', 'sentry.sample_rate': 1, 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto', + origin: 'auto.http.nextjs', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -57,7 +57,7 @@ test('should create a transaction for a CJS pages router API endpoint', async ({ cookies: expect.any(Object), headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/\/api\/cjs-api-endpoint$/), + url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint$/), }, spans: expect.arrayContaining([]), start_timestamp: expect.any(Number), @@ -102,12 +102,12 @@ test('should not mess up require statements in CJS API endpoints', async ({ requ data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto', + 'sentry.origin': 'auto.http.nextjs', 'sentry.sample_rate': 1, 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto', + origin: 'auto.http.nextjs', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -120,7 +120,7 @@ test('should not mess up require statements in CJS API endpoints', async ({ requ cookies: expect.any(Object), headers: expect.any(Object), method: 'GET', - url: expect.stringMatching(/\/api\/cjs-api-endpoint-with-require$/), + url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint-with-require$/), }, spans: expect.arrayContaining([]), start_timestamp: expect.any(Number), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts index de50ceee1076..9f5ff5db8434 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -55,11 +55,11 @@ test('Should report an error event for errors thrown in pages router api routes' data: { 'http.response.status_code': 500, 'sentry.op': 'http.server', - 'sentry.origin': 'auto', + 'sentry.origin': 'auto.http.nextjs', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto', + origin: 'auto.http.nextjs', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'internal_error', trace_id: (await errorEventPromise).contexts?.trace?.trace_id, @@ -98,11 +98,11 @@ test('Should report a transaction event for a successful pages router api route' data: { 'http.response.status_code': 200, 'sentry.op': 'http.server', - 'sentry.origin': 'auto', + 'sentry.origin': 'auto.http.nextjs', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto', + origin: 'auto.http.nextjs', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts index 8401c6a5f5d2..ed23530e0ab8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts @@ -54,8 +54,12 @@ test('Faulty edge routes', async ({ request }) => { test.step('should have scope isolation', () => { expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true); - expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + // No longer valid since we removed the global scope isolation from the wrapper + // expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).toBeDefined(); expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); - expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + // No longer valid since we removed the global scope isolation from the wrapper + // expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).toBeDefined(); }); }); From 9c04b4730f6786261913f5c4db62ed70d1688539 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 16 Dec 2025 14:22:42 +0200 Subject: [PATCH 11/11] feat: remove transaction and SDK metadata setting from the wrapper --- .../wrapApiHandlerWithSentry.ts | 29 +-------------- .../span-attributes-with-logic-attached.ts | 2 + packages/nextjs/src/edge/index.ts | 37 ++++++++++++++++--- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 2016c79922cd..62ba689b3b85 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -1,14 +1,5 @@ -import { - captureException, - debug, - getActiveSpan, - getIsolationScope, - getRootSpan, - httpRequestToRequestData, - objectify, -} from '@sentry/core'; +import { captureException, debug, objectify } from '@sentry/core'; import type { NextApiRequest } from 'next'; -import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; import { flushSafelyWithTimeout } from '../utils/responseEnd'; @@ -52,24 +43,6 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz req.__withSentry_applied__ = true; - // Set transaction name on isolation scope to ensure parameterized routes are used - // The HTTP server integration sets it on isolation scope, so we need to match that - const method = req.method || 'GET'; - const isolationScope = getIsolationScope(); - isolationScope.setTransactionName(`${method} ${parameterizedRoute}`); - // Set SDK processing metadata - isolationScope.setSDKProcessingMetadata({ - normalizedRequest: httpRequestToRequestData(req), - }); - - // Set the route backfill attribute on the root span so that the transaction name - // gets updated to use the parameterized route during event processing - const activeSpan = getActiveSpan(); - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL, parameterizedRoute); - } - return await wrappingTarget.apply(thisArg, args); } catch (e) { // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can diff --git a/packages/nextjs/src/common/span-attributes-with-logic-attached.ts b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts index a272ef525dff..cdd1c5dd9ddc 100644 --- a/packages/nextjs/src/common/span-attributes-with-logic-attached.ts +++ b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts @@ -6,3 +6,5 @@ export const TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION = 'sentry.drop_transaction export const TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL = 'sentry.sentry_trace_backfill'; export const TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL = 'sentry.route_backfill'; + +export const ATTR_NEXT_PAGES_API_ROUTE_TYPE = 'executing api route (pages)'; diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 27d42f616727..62dd3eb7c1d7 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -22,7 +22,11 @@ import { import { getScopesFromContext } from '@sentry/opentelemetry'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; -import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; +import { ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; +import { + ATTR_NEXT_PAGES_API_ROUTE_TYPE, + TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, +} from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { isBuild } from '../common/utils/isBuild'; @@ -82,12 +86,21 @@ export function init(options: VercelEdgeOptions = {}): void { dropMiddlewareTunnelRequests(span, spanAttributes); // Mark all spans generated by Next.js as 'auto' - if (spanAttributes?.['next.span_type'] !== undefined) { + if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); } + // Backfill span attributes for api route pages because we removed it from the wrapper + if ( + spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Node.runHandler' && + String(spanAttributes?.['next.span_name']).startsWith(ATTR_NEXT_PAGES_API_ROUTE_TYPE) + ) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } + // Make sure middleware spans get the right op - if (spanAttributes?.['next.span_type'] === 'Middleware.execute') { + if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware'); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); @@ -119,8 +132,8 @@ export function init(options: VercelEdgeOptions = {}): void { // The otel auto inference will clobber the transaction name because the span has an http.target if ( event.type === 'transaction' && - event.contexts?.trace?.data?.['next.span_type'] === 'Middleware.execute' && - event.contexts?.trace?.data?.['next.span_name'] + event.contexts?.trace?.data?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute' && + event.contexts?.trace?.data?.[ATTR_NEXT_SPAN_NAME] !== undefined ) { if (event.transaction) { // Older nextjs versions pass the full url appended to the middleware name, which results in high cardinality transaction names. @@ -139,6 +152,20 @@ export function init(options: VercelEdgeOptions = {}): void { } } + // Backfill the transaction name for api route pages because we removed it from the wrapper + if ( + event.type === 'transaction' && + event.contexts?.trace?.data?.[ATTR_NEXT_SPAN_TYPE] === 'Node.runHandler' && + String(event.contexts.trace.data['next.span_name']).startsWith(ATTR_NEXT_PAGES_API_ROUTE_TYPE) + ) { + let path = String(event.contexts.trace.data['next.span_name']).replace(ATTR_NEXT_PAGES_API_ROUTE_TYPE, '').trim(); + // Set transaction name on isolation scope to ensure parameterized routes are used + // The HTTP server integration sets it on isolation scope, so we need to match that + const method = event.request?.method || 'GET'; + path = path ?? event.request?.url ?? '/'; + event.transaction = `${method} ${path}`; + } + setUrlProcessingMetadata(event); });