From 747251ae6e6b18ec1afe0b4e8046c2801ccc1b61 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:20:43 +0200 Subject: [PATCH 1/2] test(nuxt): Test imports from `nuxt/server` in the nuxt-5 E2E app --- .../nuxt-5/app/pages/fetch-server-routes.vue | 10 +- .../nuxt-5/app/pages/test-param/[param].vue | 3 +- .../app/pages/test-param/user/[userId].vue | 3 +- .../server/api/nuxt-server/middleware-test.ts | 9 + .../server/api/nuxt-server/nitro-fetch.ts | 7 + .../api/nuxt-server/param-error/[param].ts | 5 + .../server/api/nuxt-server/server-error.ts | 5 + .../api/nuxt-server/third-party-http-error.ts | 16 + .../server/api/nuxt-server/user/[userId].ts | 7 + .../nuxt-server/06.nuxt-server-first.ts | 5 + .../nuxt-server/07.nuxt-server-auth.ts | 9 + .../nuxt-5/tests/errors.server.test.ts | 189 +++++----- .../nuxt-5/tests/importSurfaces.ts | 6 + .../nuxt-5/tests/middleware.test.ts | 327 +++++++----------- .../nuxt-5/tests/tracing.server.test.ts | 58 ++-- .../nuxt-5/tests/tracing.test.ts | 192 +++++----- 16 files changed, 423 insertions(+), 428 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/middleware-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/nitro-fetch.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/third-party-http-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/user/[userId].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/06.nuxt-server-first.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/07.nuxt-server-auth.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5/tests/importSurfaces.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/fetch-server-routes.vue b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/fetch-server-routes.vue index 3547773a1af9..2768eb370f94 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/fetch-server-routes.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/fetch-server-routes.vue @@ -7,17 +7,19 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/[param].vue index 019404aaf460..fde203b1575c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/[param].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/[param].vue @@ -3,9 +3,10 @@ import { useRoute, useFetch } from '#imports'; const route = useRoute(); const param = route.params.param; +const apiPrefix = route.query.apiPrefix ?? '/api'; const fetchError = async () => { - await useFetch(`/api/param-error/${param}`); + await useFetch(`${apiPrefix}/param-error/${param}`); }; const fetchData = async () => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/user/[userId].vue index 41daf0460b05..39d629b25ad6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/user/[userId].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/test-param/user/[userId].vue @@ -3,8 +3,9 @@ import { useFetch, useRoute } from '#imports'; const route = useRoute(); const userId = route.params.userId as string; +const apiPrefix = route.query.apiPrefix ?? '/api'; -const { data } = await useFetch(`/api/user/${userId}`, { +const { data } = await useFetch(`${apiPrefix}/user/${userId}`, { server: false, // Don't fetch during SSR, only client-side }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/middleware-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/middleware-test.ts new file mode 100644 index 000000000000..ab0b283df34b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/middleware-test.ts @@ -0,0 +1,9 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(event => { + return { + message: 'Server middleware test endpoint', + path: event.url.pathname, + method: event.req.method, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/nitro-fetch.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/nitro-fetch.ts new file mode 100644 index 000000000000..8f8f4c4ae4fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/nitro-fetch.ts @@ -0,0 +1,7 @@ +import { defineEventHandler } from 'nuxt/server'; + +// `$fetch` is a Nitro global, not a `nuxt/server` export, so the portable copy uses `fetch`. +export default defineEventHandler(async () => { + const response = await fetch('https://example.com'); + return response.text(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts new file mode 100644 index 000000000000..6ad843e18b9f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(() => { + throw new Error('Nuxt 4 Param Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts new file mode 100644 index 000000000000..106f0ea9110c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(() => { + throw new Error('Nuxt 4 Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/third-party-http-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/third-party-http-error.ts new file mode 100644 index 000000000000..988dd8d3002f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/third-party-http-error.ts @@ -0,0 +1,16 @@ +import { defineEventHandler } from 'nuxt/server'; + +// Mimics ky's and got's `HTTPError`: it shares its `name` with h3's error class, but keeps the +// status on `response` instead of on the error itself. +class ThirdPartyHTTPError extends Error { + public readonly response = { status: 404 }; + + public constructor(message: string) { + super(message); + this.name = 'HTTPError'; + } +} + +export default defineEventHandler(() => { + throw new ThirdPartyHTTPError('Nuxt 5 third-party HTTPError'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/user/[userId].ts new file mode 100644 index 000000000000..6fff4f8f91b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from 'nuxt/server'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/06.nuxt-server-first.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/06.nuxt-server-first.ts new file mode 100644 index 000000000000..58d68a494905 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/06.nuxt-server-first.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(event => { + event.res.headers.set('x-nuxt-server-first-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/07.nuxt-server-auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/07.nuxt-server-auth.ts new file mode 100644 index 000000000000..62b19ad9c1ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/middleware/nuxt-server/07.nuxt-server-auth.ts @@ -0,0 +1,9 @@ +import { defineEventHandler, getQuery } from 'nuxt/server'; + +export default defineEventHandler(event => { + if (getQuery(event).throwNuxtServerError === 'true') { + throw new Error('nuxt/server auth middleware error'); + } + + event.res.headers.set('x-nuxt-server-auth-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts index ebe8b5097d5d..7c1b3ab65221 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts @@ -1,95 +1,112 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; - -test.describe('server-side errors', async () => { - test('captures api fetch error (fetched on click)', async ({ page }) => { - const errorPromise = waitForError('nuxt-5', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Server API Error', { exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/server-error'); - - const exception0 = error.exception.values[0]; - const exception1 = error.exception.values[1]; - - expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Server error'); - expect(exception0.mechanism).toEqual({ - handled: true, - type: 'chained', - exception_id: 1, - parent_id: 0, - source: 'cause', - }); - - expect(exception1.type).toEqual('HTTPError'); - expect(exception1.value).toEqual('Nuxt 4 Server error'); - expect(exception1.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', - exception_id: 0, - }); - }); - - test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { - const errorPromise = waitForError('nuxt-5', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; +import { IMPORT_SURFACES } from './importSurfaces'; + +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test.describe(`server-side errors (${name})`, () => { + test('captures api fetch error (fetched on click)', async ({ page }) => { + const errorPromise = waitForError('nuxt-5', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Server API Error', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/server-error`); + + const exception0 = error.exception.values[0]; + const exception1 = error.exception.values[1]; + + expect(exception0.type).toEqual('Error'); + expect(exception0.value).toEqual('Nuxt 4 Server error'); + expect(exception0.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(exception1.type).toEqual('HTTPError'); + expect(exception1.value).toEqual('Nuxt 4 Server error'); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); - await page.goto(`/test-param/1234`); - await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/param-error/1234'); - - const exception0 = error.exception.values[0]; - const exception1 = error.exception.values[1]; - - expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Param Server error'); - expect(exception0.mechanism).toEqual({ - handled: true, - type: 'chained', - exception_id: 1, - parent_id: 0, - source: 'cause', + test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { + const errorPromise = waitForError('nuxt-5', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; + }); + + await page.goto(`/test-param/1234?apiPrefix=${apiPrefix}`); + await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/param-error/1234`); + + const exception0 = error.exception.values[0]; + const exception1 = error.exception.values[1]; + + expect(exception0.type).toEqual('Error'); + expect(exception0.value).toEqual('Nuxt 4 Param Server error'); + expect(exception0.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(exception1.type).toEqual('HTTPError'); + expect(exception1.value).toEqual('Nuxt 4 Param Server error'); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); - expect(exception1.type).toEqual('HTTPError'); - expect(exception1.value).toEqual('Nuxt 4 Param Server error'); - expect(exception1.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', - exception_id: 0, + // ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the + // hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike + // is covered by the unit tests. + test('captures a thrown third-party `HTTPError`', async ({ page }) => { + const errorPromise = waitForError('nuxt-5', async errorEvent => { + return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 5 third-party HTTPError'); + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/third-party-http-error`); + expect(error.exception.values).toHaveLength(2); + + const [thirdPartyError, h3Error] = error.exception.values; + + expect(thirdPartyError.type).toEqual('HTTPError'); + expect(thirdPartyError.value).toEqual('Nuxt 5 third-party HTTPError'); + expect(thirdPartyError.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(h3Error.type).toEqual('HTTPError'); + expect(h3Error.value).toEqual('Nuxt 5 third-party HTTPError'); + expect(h3Error.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); }); - - // ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the - // hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike - // is covered by the unit tests. - test('captures a thrown third-party `HTTPError`', async ({ page }) => { - const errorPromise = waitForError('nuxt-5', async errorEvent => { - return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 5 third-party HTTPError'); - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/third-party-http-error'); - expect(error.exception.values).toContainEqual( - expect.objectContaining({ - type: 'HTTPError', - value: 'Nuxt 5 third-party HTTPError', - mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }), - }), - ); - }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/importSurfaces.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/importSurfaces.ts new file mode 100644 index 000000000000..8c7d4b02b307 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/importSurfaces.ts @@ -0,0 +1,6 @@ +// The server test routes under `apiPrefix` are copies of each other that differ only in where they import from. +// Reference: https://github.com/nuxt/nuxt/pull/36275 +export const IMPORT_SURFACES = [ + { name: 'nitro', apiPrefix: '/api' }, + { name: 'nuxt/server', apiPrefix: '/api/nuxt-server' }, +] as const; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/middleware.test.ts index fdcade103524..e2c12def7864 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/middleware.test.ts @@ -1,286 +1,193 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpansUntilSegment, getSpanOp, waitForError } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; + +// Nitro runs every file in `server/middleware/` on every request, so each surface tells its spans apart by +// middleware name. `nuxt/server` has no object syntax (`middleware`) and no `eventHandler` alias, so only the +// simple middleware has a copy. +const MIDDLEWARE = { + nitro: { + names: ['01.first', '02.second', '03.auth', '04.hooks', '05.array-hooks'], + simpleNames: ['01.first', '02.second', '03.auth'], + // 3 simple + 2 hooks (middleware+handler) + 3 array hooks (2 middleware + 1 handler) + spanCount: 8, + authName: '03.auth', + authErrorQuery: 'throwError=true', + authErrorMessage: 'Auth middleware error', + }, + 'nuxt/server': { + names: ['06.nuxt-server-first', '07.nuxt-server-auth'], + simpleNames: ['06.nuxt-server-first', '07.nuxt-server-auth'], + spanCount: 2, + authName: '07.nuxt-server-auth', + authErrorQuery: 'throwNuxtServerError=true', + authErrorMessage: 'nuxt/server auth middleware error', + }, +} as const; + +async function collectRequestSpans(path = '/api/middleware-test') { + const spans = await collectStreamedSpansUntilSegment('nuxt-5', span => span.attributes['url.path']?.value === path); + const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === path); + + return spans.filter(span => span.trace_id === rootSpan?.trace_id); +} -function collectRequestSpans() { - return collectStreamedSpansUntilSegment( - 'nuxt-5', - span => span.attributes['url.path']?.value === '/api/middleware-test', +function findMiddlewareSpan(spans: SerializedStreamedSpan[], name: string, hook?: string, index?: number) { + return spans.find( + span => + getSpanOp(span) === 'middleware' && + span.attributes['nuxt.middleware.name']?.value === name && + (hook === undefined || span.attributes['nuxt.middleware.hook.name']?.value === hook) && + (index === undefined || span.attributes['nuxt.middleware.hook.index']?.value === index), ); } -test.describe('Server Middleware Instrumentation', () => { - test('should create separate spans for each server middleware', async ({ request }) => { - const spansPromise = collectRequestSpans(); +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + const middleware = MIDDLEWARE[name]; + const path = `${apiPrefix}/middleware-test`; + const getSurfaceMiddlewareSpans = (spans: SerializedStreamedSpan[]) => + spans.filter( + span => + getSpanOp(span) === 'middleware' && + (middleware.names as readonly unknown[]).includes(span.attributes['nuxt.middleware.name']?.value), + ); - // Make request to the API endpoint that will trigger all server middleware - const response = await request.get('/api/middleware-test'); - expect(response.status()).toBe(200); + test.describe(`Server Middleware Instrumentation (${name})`, () => { + test('creates one span per server middleware', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - const responseData = await response.json(); - expect(responseData.message).toBe('Server middleware test endpoint'); + const response = await request.get(path); + expect(response.status()).toBe(200); + expect((await response.json()).message).toBe('Server middleware test endpoint'); - const spans = await spansPromise; + const middlewareSpans = getSurfaceMiddlewareSpans(await spansPromise); - // Verify that we have spans for each middleware - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); - - // 3 simple + 2 hooks (middleware+handler) + 3 array hooks (2 middleware + 1 handler) - expect(middlewareSpans).toHaveLength(8); - - // Check for specific middleware spans - const findSpanByName = (name: string) => - middlewareSpans.find(span => span.attributes['nuxt.middleware.name']?.value === name); - - const firstMiddlewareSpan = findSpanByName('01.first'); - const secondMiddlewareSpan = findSpanByName('02.second'); - const authMiddlewareSpan = findSpanByName('03.auth'); - const hooksOnRequestSpan = findSpanByName('04.hooks'); - const arrayHooksHandlerSpan = findSpanByName('05.array-hooks'); - - expect(firstMiddlewareSpan).toBeDefined(); - expect(secondMiddlewareSpan).toBeDefined(); - expect(authMiddlewareSpan).toBeDefined(); - expect(hooksOnRequestSpan).toBeDefined(); - expect(arrayHooksHandlerSpan).toBeDefined(); - - // Verify each span has the correct attributes - [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { - expect(span).toEqual( - expect.objectContaining({ - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'middleware' }, - 'sentry.origin': { type: 'string', value: 'auto.middleware.nuxt' }, - 'http.request.method': { type: 'string', value: 'GET' }, - 'http.route': { type: 'string', value: '/api/middleware-test' }, - }), - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }), + expect(middlewareSpans).toHaveLength(middleware.spanCount); + expect(new Set(middlewareSpans.map(span => span.span_id)).size).toBe(middleware.spanCount); + expect(new Set(middlewareSpans.map(span => span.attributes['nuxt.middleware.name']?.value))).toEqual( + new Set(middleware.names), ); - }); - - // Verify spans have different span IDs (each middleware gets its own span) - const spanIds = middlewareSpans.map(span => span.span_id); - const uniqueSpanIds = new Set(spanIds); - // 3 simple + 2 hooks (middleware+handler) + 3 array hooks (2 middleware + 1 handler) - expect(uniqueSpanIds.size).toBe(8); - - // Verify spans share the same trace ID - const traceIds = middlewareSpans.map(span => span.trace_id); - const uniqueTraceIds = new Set(traceIds); - expect(uniqueTraceIds.size).toBe(1); - }); - - test('middleware spans should have proper parent-child relationship', async ({ request }) => { - const spansPromise = collectRequestSpans(); - await request.get('/api/middleware-test'); - const spans = await spansPromise; + middleware.simpleNames.forEach(middlewareName => { + const span = findMiddlewareSpan(middlewareSpans, middlewareName); - const segmentSpan = spans.find( - span => span.is_segment && span.attributes['url.path']?.value === '/api/middleware-test', - ); - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); - - // All middleware spans should be children of the request's segment span - middlewareSpans.forEach(span => { - expect(span.parent_span_id).toBe(segmentSpan?.span_id); + expect(span?.name).toBe(middlewareName); + expect(span?.is_segment).toBe(false); + expect(span?.attributes['sentry.op']).toEqual({ type: 'string', value: 'middleware' }); + expect(span?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.middleware.nuxt' }); + expect(span?.attributes['http.request.method']).toEqual({ type: 'string', value: 'GET' }); + expect(span?.attributes['http.route']).toEqual({ type: 'string', value: path }); + }); }); - }); - test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { - const spansPromise = collectRequestSpans(); + test('parents middleware spans to the request segment', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - const errorEventPromise = waitForError('nuxt-5', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Auth middleware error'; - }); + await request.get(path); + const spans = await spansPromise; - // Make request with query param to trigger error in auth middleware - const response = await request.get('/api/middleware-test?throwError=true'); + const segmentSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === path); + const middlewareSpans = getSurfaceMiddlewareSpans(spans); - // The request should fail due to the middleware error - expect(response.status()).toBe(500); - - const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); - - // Find the auth middleware span - const authMiddlewareSpan = spans.find( - span => getSpanOp(span) === 'middleware' && span.attributes['nuxt.middleware.name']?.value === '03.auth', - ); + expect(segmentSpan).toBeDefined(); + expect(middlewareSpans).toHaveLength(middleware.spanCount); + middlewareSpans.forEach(span => { + expect(span.parent_span_id).toBe(segmentSpan?.span_id); + }); + }); - expect(authMiddlewareSpan).toBeDefined(); + test('captures a middleware error and marks its span as errored', async ({ request }) => { + const spansPromise = collectRequestSpans(path); + const errorEventPromise = waitForError('nuxt-5', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === middleware.authErrorMessage; + }); - // Verify the span has error status - expect(authMiddlewareSpan?.status).toBe('error'); + const response = await request.get(`${path}?${middleware.authErrorQuery}`); + expect(response.status()).toBe(500); - // Verify the error event is associated with the correct request - expect(errorEvent.transaction).toContain('GET /api/middleware-test'); + const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); - // Verify the error has the correct mechanism - expect(errorEvent.exception?.values?.[0]).toEqual( - expect.objectContaining({ - value: 'Auth middleware error', - type: 'Error', - mechanism: expect.objectContaining({ - handled: false, - type: 'auto.middleware.nuxt', - }), - }), - ); + expect(findMiddlewareSpan(spans, middleware.authName)?.status).toBe('error'); + expect(errorEvent.transaction).toBe(`GET ${path}`); + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.type).toBe('Error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe(middleware.authErrorMessage); + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ handled: false, type: 'auto.middleware.nuxt' }); + }); }); +}); - test('should create spans for middleware and handler hooks', async ({ request }) => { +// `nuxt/server` has no object syntax for middleware hooks, so these only run on the classic surface. +test.describe('Server Middleware Instrumentation (nitro hooks)', () => { + test('creates spans for the middleware and handler hooks', async ({ request }) => { const spansPromise = collectRequestSpans(); - // Make request to trigger middleware with hooks const response = await request.get('/api/middleware-test'); expect(response.status()).toBe(200); const spans = await spansPromise; - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); + const middlewareSpan = findMiddlewareSpan(spans, '04.hooks', 'middleware'); + const handlerSpan = findMiddlewareSpan(spans, '04.hooks', 'handler'); - // Find spans for the hooks middleware - const hooksSpans = middlewareSpans.filter(span => span.attributes['nuxt.middleware.name']?.value === '04.hooks'); + // h3 v2 has no `onBeforeResponse`, so a hooks object gives only `middleware` and `handler` spans. + expect(spans.filter(span => span.attributes['nuxt.middleware.name']?.value === '04.hooks')).toHaveLength(2); - // Should have spans for middleware and handler (h3 v2 no longer has onBeforeResponse) - expect(hooksSpans).toHaveLength(2); - - // Find specific hook spans - const findSpanByHook = (hook: string) => - hooksSpans.find(span => span.attributes['nuxt.middleware.hook.name']?.value === hook); - - const middlewareSpan = findSpanByHook('middleware'); - const handlerSpan = findSpanByHook('handler'); - - expect(middlewareSpan).toBeDefined(); - expect(handlerSpan).toBeDefined(); - - // Verify span names include hook types expect(middlewareSpan?.name).toBe('04.hooks.middleware'); - expect(handlerSpan?.name).toBe('04.hooks'); - - // Verify all spans have correct middleware name (without hook suffix) - [middlewareSpan, handlerSpan].forEach(span => { - expect(span?.attributes['nuxt.middleware.name']?.value).toBe('04.hooks'); - }); - - // Verify hook-specific attributes - expect(middlewareSpan?.attributes['nuxt.middleware.hook.name']?.value).toBe('middleware'); - expect(handlerSpan?.attributes['nuxt.middleware.hook.name']?.value).toBe('handler'); - - // Verify middleware has index (middleware is always an array in h3 v2) + // h3 v2 always stores `middleware` as an array, so even a single hook has an index. expect(middlewareSpan?.attributes['nuxt.middleware.hook.index']?.value).toBe(0); + + expect(handlerSpan?.name).toBe('04.hooks'); expect(handlerSpan?.attributes['nuxt.middleware.hook.index']).toBeUndefined(); }); - test('should create spans with index attributes for array middleware', async ({ request }) => { + test('sets index attributes on array middleware spans', async ({ request }) => { const spansPromise = collectRequestSpans(); - // Make request to trigger middleware with array hooks const response = await request.get('/api/middleware-test'); expect(response.status()).toBe(200); const spans = await spansPromise; - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); - - // Find spans for the array hooks middleware - const arrayHooksSpans = middlewareSpans.filter( - span => span.attributes['nuxt.middleware.name']?.value === '05.array-hooks', - ); + const arrayHooksSpans = spans.filter(span => span.attributes['nuxt.middleware.name']?.value === '05.array-hooks'); + const middleware0Span = findMiddlewareSpan(spans, '05.array-hooks', 'middleware', 0); + const middleware1Span = findMiddlewareSpan(spans, '05.array-hooks', 'middleware', 1); + const handlerSpan = findMiddlewareSpan(spans, '05.array-hooks', 'handler'); - // Should have spans for 2 middleware + 1 handler = 3 spans (h3 v2 no longer has onBeforeResponse) expect(arrayHooksSpans).toHaveLength(3); - - // Find middleware array spans - const middlewareArraySpans = arrayHooksSpans.filter( - span => span.attributes['nuxt.middleware.hook.name']?.value === 'middleware', - ); - expect(middlewareArraySpans).toHaveLength(2); - - // Find handler span - const handlerSpan = arrayHooksSpans.find(span => span.attributes['nuxt.middleware.hook.name']?.value === 'handler'); - expect(handlerSpan).toBeDefined(); - - // Verify index attributes for middleware array - const middleware0Span = middlewareArraySpans.find( - span => span.attributes['nuxt.middleware.hook.index']?.value === 0, - ); - const middleware1Span = middlewareArraySpans.find( - span => span.attributes['nuxt.middleware.hook.index']?.value === 1, - ); - - expect(middleware0Span).toBeDefined(); - expect(middleware1Span).toBeDefined(); - - // Verify span names for array middleware handlers expect(middleware0Span?.name).toBe('05.array-hooks.middleware'); expect(middleware1Span?.name).toBe('05.array-hooks.middleware'); - - // Verify handler has no index + expect(handlerSpan?.name).toBe('05.array-hooks'); expect(handlerSpan?.attributes['nuxt.middleware.hook.index']).toBeUndefined(); }); - test('should handle errors in middleware hooks', async ({ request }) => { + test('marks a failing middleware hook span as errored', async ({ request }) => { const spansPromise = collectRequestSpans(); - const errorEventPromise = waitForError('nuxt-5', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'OnRequest hook error'; }); - // Make request with query param to trigger error in middleware const response = await request.get('/api/middleware-test?throwOnRequestError=true'); expect(response.status()).toBe(500); const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); - // Find the middleware span that should have error status - const middlewareSpan = spans.find( - span => - getSpanOp(span) === 'middleware' && - span.attributes['nuxt.middleware.name']?.value === '04.hooks' && - span.attributes['nuxt.middleware.hook.name']?.value === 'middleware', - ); - - expect(middlewareSpan).toBeDefined(); - expect(middlewareSpan?.status).toBe('error'); + expect(findMiddlewareSpan(spans, '04.hooks', 'middleware')?.status).toBe('error'); expect(errorEvent.exception?.values?.[0]?.value).toBe('OnRequest hook error'); }); - test('should handle errors in array middleware with proper index attribution', async ({ request }) => { + test('marks only the failing array middleware span as errored', async ({ request }) => { const spansPromise = collectRequestSpans(); - const errorEventPromise = waitForError('nuxt-5', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'OnRequest[1] hook error'; }); - // Make request with query param to trigger error in second middleware handler const response = await request.get('/api/middleware-test?throwOnRequest1Error=true'); expect(response.status()).toBe(500); const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); - const findArrayHookSpan = (index: number) => - spans.find( - span => - getSpanOp(span) === 'middleware' && - span.attributes['nuxt.middleware.name']?.value === '05.array-hooks' && - span.attributes['nuxt.middleware.hook.name']?.value === 'middleware' && - span.attributes['nuxt.middleware.hook.index']?.value === index, - ); - - // Find the second middleware span that should have error status - const middleware1Span = findArrayHookSpan(1); - - expect(middleware1Span).toBeDefined(); - expect(middleware1Span?.status).toBe('error'); + expect(findMiddlewareSpan(spans, '05.array-hooks', 'middleware', 0)?.status).toBe('ok'); + expect(findMiddlewareSpan(spans, '05.array-hooks', 'middleware', 1)?.status).toBe('error'); expect(errorEvent.exception?.values?.[0]?.value).toBe('OnRequest[1] hook error'); - - // Verify the first middleware handler still executed successfully - const middleware0Span = findArrayHookSpan(0); - - expect(middleware0Span).toBeDefined(); - expect(middleware0Span?.status).not.toBe('error'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.server.test.ts index 9ffae3e03885..2a0dcebe48c4 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.server.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; -import { collectStreamedSpansUntilSegment, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; test('sends a server root span on pageload', async ({ page }) => { const serverSpanPromise = waitForStreamedSpan('nuxt-5', span => { @@ -38,27 +39,36 @@ test('does not send spans for build asset folder "_nuxt"', async ({ page }) => { expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('route'); }); -// TODO: Make test work with Nuxt 5 -test.skip('captures server API calls made with Nitro $fetch', async ({ page }) => { - const spansPromise = collectStreamedSpansUntilSegment( - 'nuxt-5', - span => span.attributes['url.path']?.value === '/api/nitro-fetch', - ); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Nitro $fetch', { exact: true }).click(); - - const spans = await spansPromise; - - const httpServerSpan = spans.find( - span => span.is_segment && span.attributes['url.path']?.value === '/api/nitro-fetch', - ); - const httpClientSpan = spans.find( - span => span.trace_id === httpServerSpan?.trace_id && span.attributes['url.full']?.value === 'https://example.com/', - ); - - expect(getSpanOp(httpServerSpan!)).toEqual('http.server'); - - expect(httpClientSpan?.parent_span_id).toEqual(httpServerSpan?.span_id); - expect(getSpanOp(httpClientSpan!)).toEqual('http.client'); +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test(`captures server API calls made with Nitro $fetch (${name})`, async ({ page }) => { + // The server-side http.client span can flush before its segment. Accumulate until both arrived. + const spansPromise = collectStreamedSpans('nuxt-5', spans => { + return ( + spans.some(span => span.is_segment && span.attributes['url.path']?.value === `${apiPrefix}/nitro-fetch`) && + spans.some( + span => getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'https://example.com/', + ) + ); + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Nitro $fetch', { exact: true }).click(); + + const spans = await spansPromise; + + const serverSegmentSpan = spans.find( + span => span.is_segment && span.attributes['url.path']?.value === `${apiPrefix}/nitro-fetch`, + ); + const httpClientSpan = spans.find( + span => getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'https://example.com/', + ); + + expect(serverSegmentSpan).toBeDefined(); + expect(getSpanOp(serverSegmentSpan!)).toBe('http.server'); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan?.name).toBe('GET example.com'); + expect(httpClientSpan?.trace_id).toBe(serverSegmentSpan?.trace_id); + expect(httpClientSpan?.parent_span_id).toBe(serverSegmentSpan?.span_id); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.test.ts index 9476ea84df14..5580819bec1f 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.test.ts @@ -1,11 +1,23 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; + +function expectSegmentSpan( + span: SerializedStreamedSpan | undefined, + expected: { name: string; op: string; origin: string }, +) { + expect(span?.name).toBe(expected.name); + expect(span?.is_segment).toBe(true); + expect(span?.attributes['sentry.op']?.value).toBe(expected.op); + expect(span?.attributes['sentry.origin']?.value).toBe(expected.origin); + expect(span?.attributes['sentry.segment.name.source']?.value).toBe('route'); +} test.describe('distributed tracing', () => { const PARAM = 's0me-param'; - const API_PATH = `/api/user/${PARAM}`; - test('capture a distributed pageload trace', async ({ page }) => { + test('captures a distributed pageload trace', async ({ page }) => { const clientSpanPromise = waitForStreamedSpan('nuxt-5', span => { return getSpanOp(span) === 'pageload' && span.is_segment; }); @@ -34,115 +46,91 @@ test.describe('distributed tracing', () => { expect(metaSampled).toBe('1'); - expect(clientSpan).toMatchObject({ - name: '/test-param/:param()', - is_segment: true, - trace_id: metaTraceId, - parent_span_id: metaParentSpanId, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'pageload' }, - 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); + expectSegmentSpan(clientSpan, { name: '/test-param/:param()', op: 'pageload', origin: 'auto.pageload.vue' }); + expect(clientSpan.trace_id).toBe(metaTraceId); + expect(clientSpan.parent_span_id).toBe(metaParentSpanId); - expect(serverSpan).toMatchObject({ - name: 'GET /test-param/:param()', // parametrized - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), + expectSegmentSpan(serverSpan, { + name: 'GET /test-param/:param()', + op: 'http.server', + origin: 'auto.http.http_server', }); - - // connected trace - expect(clientSpan.trace_id).toBe(serverSpan.trace_id); - expect(clientSpan.parent_span_id).toBe(serverSpan.span_id); expect(serverSpan.trace_id).toBe(metaTraceId); + expect(clientSpan.parent_span_id).toBe(serverSpan.span_id); }); - test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { - // The `http.client` span ends after the pageload segment, so it can be flushed in a later - // envelope. Accumulate until both spans have arrived. - const clientSpansPromise = collectStreamedSpans('nuxt-5', spans => { - return ( - spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && - spans.some( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), - ) + IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test(`captures a distributed trace from a client-side API request with parametrized routes (${name})`, async ({ + page, + baseURL, + }) => { + const API_PATH = `${apiPrefix}/user/${PARAM}`; + + // The `http.client` span ends after the pageload segment, so it can be flushed in a later + // envelope. Accumulate until both spans have arrived. + const clientSpansPromise = collectStreamedSpans('nuxt-5', spans => { + return ( + spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && + spans.some( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), + ) + ); + }); + const ssrSpanPromise = waitForStreamedSpan('nuxt-5', span => { + return span.is_segment && span.name.includes('GET /test-param/user'); + }); + const serverReqSpanPromise = waitForStreamedSpan('nuxt-5', span => { + return span.is_segment && span.name.includes(`GET ${apiPrefix}/user/`); + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}?apiPrefix=${apiPrefix}`); + + const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ + clientSpansPromise, + ssrSpanPromise, + serverReqSpanPromise, + ]); + + const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); + const httpClientSpan = clientSpans.find( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), ); - }); - const ssrSpanPromise = waitForStreamedSpan('nuxt-5', span => { - return span.is_segment && span.name.includes('GET /test-param/user'); - }); - const serverReqSpanPromise = waitForStreamedSpan('nuxt-5', span => { - return span.is_segment && span.name.includes('GET /api/user/'); - }); - - // Navigate to the page which will trigger an API call from the client-side - await page.goto(`/test-param/user/${PARAM}`); - const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ - clientSpansPromise, - ssrSpanPromise, - serverReqSpanPromise, - ]); - - const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); - const httpClientSpan = clientSpans.find( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), - ); - - expect(pageloadSpan).toMatchObject({ - name: '/test-param/user/:userId()', - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'pageload' }, - 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); + expectSegmentSpan(pageloadSpan, { + name: '/test-param/user/:userId()', + op: 'pageload', + origin: 'auto.pageload.vue', + }); - expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan).toMatchObject({ // A relative fetch has no domain of its own, so it resolves against the page origin. - name: 'GET localhost', - parent_span_id: pageloadSpan?.span_id, // pageload span is parent - attributes: expect.objectContaining({ - type: { type: 'string', value: 'fetch' }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: expect.stringContaining(API_PATH) }, - 'url.domain': { type: 'string', value: 'localhost' }, - }), - }); - - expect(ssrSpan).toMatchObject({ - name: 'GET /test-param/user/:userId()', // parametrized route - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); - - expect(serverReqSpan).toMatchObject({ - name: 'GET /api/user/:userId', // parametrized route - is_segment: true, - parent_span_id: httpClientSpan?.span_id, // http.client span is parent - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - }), + expect(httpClientSpan?.name).toBe('GET localhost'); + expect(httpClientSpan?.is_segment).toBe(false); + expect(httpClientSpan?.parent_span_id).toBe(pageloadSpan?.span_id); + expect(httpClientSpan?.attributes['type']?.value).toBe('fetch'); + expect(httpClientSpan?.attributes['sentry.op']?.value).toBe('http.client'); + expect(httpClientSpan?.attributes['sentry.origin']?.value).toBe('auto.http.browser'); + expect(httpClientSpan?.attributes['http.request.method']?.value).toBe('GET'); + expect(httpClientSpan?.attributes['url.full']?.value).toBe(`${baseURL}${API_PATH}`); + expect(httpClientSpan?.attributes['url.domain']?.value).toBe('localhost'); + + expectSegmentSpan(ssrSpan, { + name: 'GET /test-param/user/:userId()', + op: 'http.server', + origin: 'auto.http.http_server', + }); + + expectSegmentSpan(serverReqSpan, { + name: `GET ${apiPrefix}/user/:userId`, + op: 'http.server', + origin: 'auto.http.http_server', + }); + expect(serverReqSpan.parent_span_id).toBe(httpClientSpan?.span_id); + + // `collectStreamedSpans` already guarantees the pageload and http.client spans share a trace, + // so only the independently awaited server spans need the check. + expect(ssrSpan.trace_id).toBe(pageloadSpan?.trace_id); + expect(serverReqSpan.trace_id).toBe(pageloadSpan?.trace_id); }); - - // All 3 root spans and the http.client span should share the same trace_id - expect(pageloadSpan?.trace_id).toBeDefined(); - expect(pageloadSpan?.trace_id).toBe(httpClientSpan?.trace_id); - expect(pageloadSpan?.trace_id).toBe(ssrSpan.trace_id); - expect(pageloadSpan?.trace_id).toBe(serverReqSpan.trace_id); }); }); From bd349ad0adb34063238ba695d99d0c150d8724e8 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:45:33 +0200 Subject: [PATCH 2/2] rename to nuxt 5 --- .../server/api/nuxt-server/param-error/[param].ts | 2 +- .../nuxt-5/server/api/nuxt-server/server-error.ts | 2 +- .../nuxt-5/server/api/param-error/[param].ts | 2 +- .../nuxt-5/server/api/server-error.ts | 2 +- .../nuxt-5/tests/environment.test.ts | 2 +- .../nuxt-5/tests/errors.server.test.ts | 12 ++++++------ 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts index 6ad843e18b9f..6444b45fd145 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/param-error/[param].ts @@ -1,5 +1,5 @@ import { defineEventHandler } from 'nuxt/server'; export default defineEventHandler(() => { - throw new Error('Nuxt 4 Param Server error'); + throw new Error('Nuxt 5 Param Server error'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts index 106f0ea9110c..1d6b6fe10f88 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/nuxt-server/server-error.ts @@ -1,5 +1,5 @@ import { defineEventHandler } from 'nuxt/server'; export default defineEventHandler(() => { - throw new Error('Nuxt 4 Server error'); + throw new Error('Nuxt 5 Server error'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/param-error/[param].ts index 3422c275abe0..a0e597ead72b 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/param-error/[param].ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/param-error/[param].ts @@ -1,5 +1,5 @@ import { defineHandler } from 'nitro'; export default defineHandler(_e => { - throw new Error('Nuxt 4 Param Server error'); + throw new Error('Nuxt 5 Param Server error'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/server-error.ts index 23b89ce2c287..7533424e6657 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/server-error.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/server-error.ts @@ -1,5 +1,5 @@ import { defineHandler } from 'nitro'; export default defineHandler(event => { - throw new Error('Nuxt 4 Server error'); + throw new Error('Nuxt 5 Server error'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/environment.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/environment.test.ts index d7ac4e437d7e..ecd946274a68 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/environment.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/environment.test.ts @@ -39,7 +39,7 @@ test.describe('environment detection', async () => { test('sets correct environment for server-side errors', async ({ page }) => { const errorPromise = waitForError('nuxt-5', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 5 Server error'; }); await page.goto(`/fetch-server-routes`, isDevMode ? { waitUntil: 'networkidle' } : {}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts index 7c1b3ab65221..1917990af924 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts @@ -6,7 +6,7 @@ IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { test.describe(`server-side errors (${name})`, () => { test('captures api fetch error (fetched on click)', async ({ page }) => { const errorPromise = waitForError('nuxt-5', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 5 Server error'; }); await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); @@ -20,7 +20,7 @@ IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { const exception1 = error.exception.values[1]; expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Server error'); + expect(exception0.value).toEqual('Nuxt 5 Server error'); expect(exception0.mechanism).toEqual({ handled: true, type: 'chained', @@ -30,7 +30,7 @@ IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { }); expect(exception1.type).toEqual('HTTPError'); - expect(exception1.value).toEqual('Nuxt 4 Server error'); + expect(exception1.value).toEqual('Nuxt 5 Server error'); expect(exception1.mechanism).toEqual({ handled: false, type: 'auto.function.nuxt.nitro', @@ -40,7 +40,7 @@ IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { const errorPromise = waitForError('nuxt-5', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 5 Param Server error'; }); await page.goto(`/test-param/1234?apiPrefix=${apiPrefix}`); @@ -54,7 +54,7 @@ IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { const exception1 = error.exception.values[1]; expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Param Server error'); + expect(exception0.value).toEqual('Nuxt 5 Param Server error'); expect(exception0.mechanism).toEqual({ handled: true, type: 'chained', @@ -64,7 +64,7 @@ IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { }); expect(exception1.type).toEqual('HTTPError'); - expect(exception1.value).toEqual('Nuxt 4 Param Server error'); + expect(exception1.value).toEqual('Nuxt 5 Param Server error'); expect(exception1.mechanism).toEqual({ handled: false, type: 'auto.function.nuxt.nitro',