From cd9cb833f6ff7d588e0618c3ddee8e818b4f40b2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Feb 2026 10:46:45 +0100 Subject: [PATCH 1/6] add tests --- .../app/route-handler/[xoxo]/error/route.ts | 5 +++ .../nextjs-16/tests/route-handler.test.ts | 44 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/error/route.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/error/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/error/route.ts new file mode 100644 index 000000000000..064b9df86854 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/error/route.ts @@ -0,0 +1,5 @@ +export const dynamic = 'force-dynamic'; + +export async function GET() { + throw new Error('route-handler-error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts index 31d6696f39ec..5c0f76c13105 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts @@ -1,5 +1,5 @@ import test, { expect } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Should create a transaction for node route handlers', async ({ request }) => { const routehandlerTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => { @@ -37,3 +37,45 @@ test('Should create a transaction for edge route handlers', async ({ request }) expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez'); }); + +test('Should report an error with a parameterized transaction name for a throwing route handler', async ({ + request, +}) => { + const errorEventPromise = waitForError('nextjs-16', errorEvent => { + return errorEvent?.exception?.values?.some(value => value.value === 'route-handler-error') ?? false; + }); + + const transactionEventPromise = waitForTransaction('nextjs-16', transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /route-handler/[xoxo]/error' && + transactionEvent?.contexts?.trace?.op === 'http.server' + ); + }); + + request.get('/route-handler/456/error').catch(() => {}); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; + + // Error event should be part of the same trace as the transaction + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); + + // Error should carry the parameterized transaction name + expect(errorEvent.transaction).toBe('/route-handler/[xoxo]/error'); + + expect(errorEvent.contexts?.nextjs).toEqual({ + route_type: 'route', + router_kind: 'App Router', + router_path: '/route-handler/[xoxo]/error', + request_path: '/route-handler/456/error', + }); + + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ + handled: false, + type: 'auto.function.nextjs.on_request_error', + }); + + // Transaction should have parameterized name and internal_error status + expect(transactionEvent.transaction).toBe('GET /route-handler/[xoxo]/error'); + expect(transactionEvent.contexts?.trace?.status).toBe('internal_error'); +}); From aa10473650d8d8f47c1b5b6ec8673a88dfbb1b99 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Feb 2026 11:24:35 +0100 Subject: [PATCH 2/6] add capture tests --- .../[xoxo]/capture-exception/route.ts | 9 +++ .../[xoxo]/capture-message/route.ts | 9 +++ .../nextjs-16/tests/route-handler.test.ts | 62 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-exception/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-message/route.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-exception/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-exception/route.ts new file mode 100644 index 000000000000..2f8a8b84d9e6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-exception/route.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + Sentry.captureException(new Error('route-handler-capture-exception')); + return NextResponse.json({ message: 'Exception captured' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-message/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-message/route.ts new file mode 100644 index 000000000000..67015ec11b2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/route-handler/[xoxo]/capture-message/route.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + Sentry.captureMessage('route-handler-message'); + return NextResponse.json({ message: 'Message captured' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts index 5c0f76c13105..32d592ad89f2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts @@ -79,3 +79,65 @@ test('Should report an error with a parameterized transaction name for a throwin expect(transactionEvent.transaction).toBe('GET /route-handler/[xoxo]/error'); expect(transactionEvent.contexts?.trace?.status).toBe('internal_error'); }); + +test('Should set a parameterized transaction name on a captureMessage event in a route handler', async ({ + request, +}) => { + const messageEventPromise = waitForError('nextjs-16', event => { + return event?.message === 'route-handler-message'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-16', transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /route-handler/[xoxo]/capture-message' && + transactionEvent?.contexts?.trace?.op === 'http.server' + ); + }); + + const response = await request.get('/route-handler/789/capture-message'); + expect(await response.json()).toStrictEqual({ message: 'Message captured' }); + + const messageEvent = await messageEventPromise; + const transactionEvent = await transactionEventPromise; + + // Message event should be part of the same trace as the transaction + expect(messageEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); + + // Message should carry the parameterized transaction name + expect(messageEvent.transaction).toBe('GET /route-handler/[xoxo]/capture-message'); + + // Transaction should have parameterized name and ok status + expect(transactionEvent.transaction).toBe('GET /route-handler/[xoxo]/capture-message'); + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); +}); + +test('Should set a parameterized transaction name on a captureException event in a route handler', async ({ + request, +}) => { + const errorEventPromise = waitForError('nextjs-16', errorEvent => { + return errorEvent?.exception?.values?.some(value => value.value === 'route-handler-capture-exception') ?? false; + }); + + const transactionEventPromise = waitForTransaction('nextjs-16', transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /route-handler/[xoxo]/capture-exception' && + transactionEvent?.contexts?.trace?.op === 'http.server' + ); + }); + + const response = await request.get('/route-handler/321/capture-exception'); + expect(await response.json()).toStrictEqual({ message: 'Exception captured' }); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; + + // Error event should be part of the same trace as the transaction + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); + + // Manually captured exception should carry the parameterized transaction name + expect(errorEvent.transaction).toBe('GET /route-handler/[xoxo]/capture-exception'); + + // Transaction should have parameterized name and ok status (error was caught, not thrown) + expect(transactionEvent.transaction).toBe('GET /route-handler/[xoxo]/capture-exception'); + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); +}); From f08eb0228eb8c0fece9e4fd7eb30e23cdf483b2f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Feb 2026 11:24:56 +0100 Subject: [PATCH 3/6] set tx name in onspanstart --- packages/nextjs/src/server/handleOnSpanStart.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/handleOnSpanStart.ts index 37070a9f1701..c8e2215b2aaf 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/handleOnSpanStart.ts @@ -46,6 +46,14 @@ export function handleOnSpanStart(span: Span): void { // Preserving the original attribute despite internally not depending on it rootSpan.setAttribute(ATTR_NEXT_ROUTE, route); + // Update the isolation scope's transaction name so that non-transaction events + // (e.g. captureMessage, captureException) also get the parameterized route. + // eslint-disable-next-line deprecation/deprecation + const method = rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]; + if (typeof method === 'string') { + getIsolationScope().setTransactionName(`${method} ${route}`); + } + // Check if this is a Vercel cron request and start a check-in maybeStartCronCheckIn(rootSpan, route); } From 88eeda335dc0ec57b63ee632ccfff0a7f35e424f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Feb 2026 11:46:02 +0100 Subject: [PATCH 4/6] unify error tx naming --- .../test-applications/nextjs-16/tests/route-handler.test.ts | 4 ++-- packages/nextjs/src/common/captureRequestError.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts index 32d592ad89f2..c04cae5b4ad4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts @@ -60,8 +60,8 @@ test('Should report an error with a parameterized transaction name for a throwin // Error event should be part of the same trace as the transaction expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); - // Error should carry the parameterized transaction name - expect(errorEvent.transaction).toBe('/route-handler/[xoxo]/error'); + // Error should carry the parameterized transaction name (with HTTP method) + expect(errorEvent.transaction).toBe('GET /route-handler/[xoxo]/error'); expect(errorEvent.contexts?.nextjs).toEqual({ route_type: 'route', diff --git a/packages/nextjs/src/common/captureRequestError.ts b/packages/nextjs/src/common/captureRequestError.ts index 41fd5d15bea6..a34a868859ac 100644 --- a/packages/nextjs/src/common/captureRequestError.ts +++ b/packages/nextjs/src/common/captureRequestError.ts @@ -33,7 +33,7 @@ export function captureRequestError(error: unknown, request: RequestInfo, errorC route_type: errorContext.routeType, }); - scope.setTransactionName(errorContext.routePath); + scope.setTransactionName(`${request.method} ${errorContext.routePath}`); captureException(error, { mechanism: { From b66a1b9e46851ab4d86b9f32ce6ed410cd09fe1f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Feb 2026 12:15:28 +0100 Subject: [PATCH 5/6] . --- .../test-applications/nextjs-16/tests/route-handler.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts index c04cae5b4ad4..50776cad00b6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts @@ -42,7 +42,10 @@ test('Should report an error with a parameterized transaction name for a throwin request, }) => { const errorEventPromise = waitForError('nextjs-16', errorEvent => { - return errorEvent?.exception?.values?.some(value => value.value === 'route-handler-error') ?? false; + return ( + (errorEvent?.exception?.values?.some(value => value.value === 'route-handler-error') ?? false) && + errorEvent?.contexts?.nextjs?.route_type === 'route' + ); }); const transactionEventPromise = waitForTransaction('nextjs-16', transactionEvent => { From c2582844bde2dcafd7e11dc4331b63b56ec2ab80 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 13 Feb 2026 13:04:56 +0100 Subject: [PATCH 6/6] wat --- .../nextjs-16/tests/route-handler.test.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts index 50776cad00b6..e37c39eb4dba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/route-handler.test.ts @@ -42,10 +42,7 @@ test('Should report an error with a parameterized transaction name for a throwin request, }) => { const errorEventPromise = waitForError('nextjs-16', errorEvent => { - return ( - (errorEvent?.exception?.values?.some(value => value.value === 'route-handler-error') ?? false) && - errorEvent?.contexts?.nextjs?.route_type === 'route' - ); + return errorEvent?.exception?.values?.some(value => value.value === 'route-handler-error') ?? false; }); const transactionEventPromise = waitForTransaction('nextjs-16', transactionEvent => { @@ -63,20 +60,25 @@ test('Should report an error with a parameterized transaction name for a throwin // Error event should be part of the same trace as the transaction expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); - // Error should carry the parameterized transaction name (with HTTP method) + // Error should carry the parameterized transaction name expect(errorEvent.transaction).toBe('GET /route-handler/[xoxo]/error'); - expect(errorEvent.contexts?.nextjs).toEqual({ - route_type: 'route', - router_kind: 'App Router', - router_path: '/route-handler/[xoxo]/error', - request_path: '/route-handler/456/error', - }); - - expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ - handled: false, - type: 'auto.function.nextjs.on_request_error', - }); + // On turbopack (no wrapping loader), the error goes through onRequestError which sets nextjs context. + // On webpack, the wrapping loader's error handler fires first and captures without nextjs context. + // The SDK deduplicates by error identity, so only the first capture survives. + if (process.env.TEST_ENV === 'development') { + expect(errorEvent.contexts?.nextjs).toEqual({ + route_type: 'route', + router_kind: 'App Router', + router_path: '/route-handler/[xoxo]/error', + request_path: '/route-handler/456/error', + }); + + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ + handled: false, + type: 'auto.function.nextjs.on_request_error', + }); + } // Transaction should have parameterized name and internal_error status expect(transactionEvent.transaction).toBe('GET /route-handler/[xoxo]/error');