From bd5f5cd22d90431163f76e16b9d02777181da403 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 28 Jul 2026 13:12:50 -0400 Subject: [PATCH 1/2] test(nextjs): Assert single transaction for pages-router API routes Guards against duplicate root transactions for pages-router API routes: Next.js's own BaseServer.handleRequest transaction and the one created by wrapApiHandlerWithSentry must never both be sent for a single request. --- .../tests/api-route-transaction.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts new file mode 100644 index 000000000000..43e4c90c592d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// A pages-router API route sees both Next.js's own `BaseServer.handleRequest` OTEL transaction and the +// transaction created by `wrapApiHandlerWithSentry`. Exactly one of them must be sent for a request, never +// both. This guards against regressing back to duplicate root transactions for the same API route. +test('Sends exactly one transaction for a pages-router API route', async ({ request }) => { + const apiRouteTransactions: string[] = []; + + // Never resolves; we accumulate every matching transaction and assert on the total after a grace period. + const collectorPromise = waitForTransaction('nextjs-pages-dir', transactionEvent => { + if (transactionEvent?.transaction === 'GET /api/endpoint') { + apiRouteTransactions.push(transactionEvent.contexts?.trace?.trace_id ?? ''); + } + return false; + }); + + const response = await request.get('/api/endpoint'); + expect(await response.json()).toStrictEqual({ name: 'John Doe' }); + + await Promise.race([collectorPromise, new Promise(resolve => setTimeout(resolve, 6000))]); + + expect(apiRouteTransactions).toHaveLength(1); +}); From b4932be21815023d4714df19033ad88bacb3340f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 29 Jul 2026 12:27:02 -0400 Subject: [PATCH 2/2] test(nextjs): Simplify grace-period wait in pages-router API route test --- .../nextjs-pages-dir/tests/api-route-transaction.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts index 43e4c90c592d..bae301e5a5c3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/api-route-transaction.test.ts @@ -7,8 +7,9 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends exactly one transaction for a pages-router API route', async ({ request }) => { const apiRouteTransactions: string[] = []; - // Never resolves; we accumulate every matching transaction and assert on the total after a grace period. - const collectorPromise = waitForTransaction('nextjs-pages-dir', transactionEvent => { + // Accumulate every matching transaction and assert on the total after a grace period. This predicate never + // returns true, so the promise never resolves; we just let it collect while we wait out the grace period. + void waitForTransaction('nextjs-pages-dir', transactionEvent => { if (transactionEvent?.transaction === 'GET /api/endpoint') { apiRouteTransactions.push(transactionEvent.contexts?.trace?.trace_id ?? ''); } @@ -18,7 +19,7 @@ test('Sends exactly one transaction for a pages-router API route', async ({ requ const response = await request.get('/api/endpoint'); expect(await response.json()).toStrictEqual({ name: 'John Doe' }); - await Promise.race([collectorPromise, new Promise(resolve => setTimeout(resolve, 6000))]); + await new Promise(resolve => setTimeout(resolve, 6000)); expect(apiRouteTransactions).toHaveLength(1); });