diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/[param]/error/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/[param]/error/route.ts new file mode 100644 index 000000000000..dbc0c6193131 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/[param]/error/route.ts @@ -0,0 +1,3 @@ +export async function GET(request: Request) { + throw new Error('Dynamic route handler error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/[param]/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/[param]/route.ts new file mode 100644 index 000000000000..581a4d68b640 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/[param]/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ name: 'Beep' }); +} + +export async function POST() { + return NextResponse.json({ name: 'Boop' }, { status: 400 }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/static/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/static/route.ts new file mode 100644 index 000000000000..c2407f908b8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/route-handlers/static/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + return NextResponse.json({ name: 'Static' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index 1cfbd8eb6628..e28db5352884 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -17,7 +17,7 @@ "@types/node": "^18.19.1", "@types/react": "^19", "@types/react-dom": "^19", - "next": "^15.3.5", + "next": "^15.5.4", "react": "^19", "react-dom": "^19", "typescript": "~5.0.0" diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/route-handlers.test.ts new file mode 100644 index 000000000000..544ba0084167 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/route-handlers.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should create a transaction for dynamic route handlers', async ({ request }) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === 'GET /route-handlers/[param]'; + }); + + const response = await request.get('/route-handlers/foo'); + expect(await response.json()).toStrictEqual({ name: 'Beep' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); +}); + +test('Should create a transaction for static route handlers', async ({ request }) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === 'GET /route-handlers/static'; + }); + + const response = await request.get('/route-handlers/static'); + expect(await response.json()).toStrictEqual({ name: 'Static' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); +}); + +test('Should create a transaction for route handlers and correctly set span status depending on http status', async ({ + request, +}) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === 'POST /route-handlers/[param]'; + }); + + const response = await request.post('/route-handlers/bar'); + expect(await response.json()).toStrictEqual({ name: 'Boop' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('invalid_argument'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); +}); + +test('Should record exceptions and transactions for faulty route handlers', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-turbo', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Dynamic route handler error'; + }); + + const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === 'GET /route-handlers/[param]/error'; + }); + + await request.get('/route-handlers/boop/error').catch(() => {}); + + const routehandlerTransaction = await routehandlerTransactionPromise; + const routehandlerError = await errorEventPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto'); + + expect(routehandlerError.exception?.values?.[0].value).toBe('Dynamic route handler error'); + + expect(routehandlerError.request?.method).toBe('GET'); + // todo: make sure url is attached to request object + // expect(routehandlerError.request?.url).toContain('/route-handlers/boop/error'); + + expect(routehandlerError.transaction).toBe('/route-handlers/[param]/error'); +});