From 2edf0afc720698923d65e6d479b3cb4b50f2d6ec Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Wed, 2 Jul 2025 13:23:12 +0200 Subject: [PATCH 01/10] rename error button --- .../nuxt-3-dynamic-import/pages/fetch-server-error.vue | 8 ++++---- .../nuxt-3-dynamic-import/tests/errors.server.test.ts | 2 +- .../nuxt-3-min/pages/fetch-server-error.vue | 8 ++++---- .../nuxt-3-min/tests/errors.server.test.ts | 2 +- .../nuxt-3-top-level-import/pages/fetch-server-error.vue | 8 ++++---- .../nuxt-3-top-level-import/tests/errors.server.test.ts | 4 ++-- .../test-applications/nuxt-3/pages/fetch-server-error.vue | 8 ++++---- .../test-applications/nuxt-3/tests/errors.server.test.ts | 2 +- .../nuxt-4/app/pages/fetch-server-error.vue | 8 ++++---- .../test-applications/nuxt-4/tests/errors.server.test.ts | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue index 8cb2a9997e58..25ed45233d98 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue @@ -1,13 +1,13 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts index b781642c2b4f..3dbc2d8e55e9 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts @@ -8,7 +8,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data', { exact: true }).click(); + await page.getByText('Fetch Server Error', { exact: true }).click(); const error = await errorPromise; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue index 8cb2a9997e58..25ed45233d98 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue @@ -1,13 +1,13 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts index 8f20aa938893..099ec8a24fa7 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts @@ -8,7 +8,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data', { exact: true }).click(); + await page.getByText('Fetch Server Error', { exact: true }).click(); const error = await errorPromise; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue index 8cb2a9997e58..25ed45233d98 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue @@ -1,13 +1,13 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts index 053ec5b6ab67..de520ed57f8c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts @@ -12,7 +12,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data', { exact: true }).click(); + await page.getByText('Fetch Server Error', { exact: true }).click(); const transactionEvent = await transactionEventPromise; const error = await errorPromise; @@ -40,7 +40,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data', { exact: true }).click(); + await page.getByText('Fetch Server Error', { exact: true }).click(); const transactionEvent = await transactionEventPromise; const error = await errorPromise; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue index 8cb2a9997e58..25ed45233d98 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue @@ -1,13 +1,13 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts index d1556d511bf0..c590413e4b14 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts @@ -8,7 +8,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data', { exact: true }).click(); + await page.getByText('Fetch Server Error', { exact: true }).click(); const error = await errorPromise; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue index 8cb2a9997e58..25ed45233d98 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue @@ -1,13 +1,13 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts index 396870d19925..862b92217f7b 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts @@ -8,7 +8,7 @@ test.describe('server-side errors', async () => { }); await page.goto(`/fetch-server-error`); - await page.getByText('Fetch Server Data', { exact: true }).click(); + await page.getByText('Fetch Server Error', { exact: true }).click(); const error = await errorPromise; From 963cc3f8dc1b01b9607c2a13bcfd472cdfd13dda Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Wed, 2 Jul 2025 13:59:20 +0200 Subject: [PATCH 02/10] test(nuxt): Add test for distributed server request --- .../nuxt-4/tests/tracing.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index 505a912c95d5..511880da17a6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -66,4 +66,70 @@ test.describe('distributed tracing', () => { expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); }); + + test('capture a distributed server request with parametrization', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === '/test-param/:param()'; + }); + + const ssrTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction.includes('GET /test-param/'); + }); + + const serverReqTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction.includes('GET /api/test-param/'); + }); + + const [, clientTxnEvent, ssrTxnEvent, , , serverReqTxnEvent] = await Promise.all([ + page.goto(`/test-param/${PARAM}`), + clientTxnEventPromise, + ssrTxnEventPromise, + expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), + page.getByText('Fetch Server Data', { exact: true }).click(), + serverReqTxnEventPromise, + ]); + + const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/test-param/${PARAM}`); + + expect(ssrTxnEvent).toMatchObject({ + transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro) + transaction_info: { source: 'url' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + }); + + expect(httpClientSpan).toMatchObject({ + description: `GET /api/test-param/${PARAM}`, // todo: parametrize (nitro) + parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent + data: expect.objectContaining({ + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'http.request_method': 'GET', + }), + }); + + expect(serverReqTxnEvent).toMatchObject({ + transaction: `GET /api/test-param/${PARAM}`, // todo: parametrize (nitro) + transaction_info: { source: 'url' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.otel.http', + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + }, + }, + }); + + // All share the same trace_id + expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id); + }); }); From 86de9e060db826fcaad8ffb85425842255fc3707 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Wed, 2 Jul 2025 14:28:55 +0200 Subject: [PATCH 03/10] fix button naming to be exact --- .../nuxt-3-dynamic-import/pages/fetch-server-error.vue | 2 +- .../nuxt-3-dynamic-import/pages/test-param/[param].vue | 7 +++---- .../nuxt-3-dynamic-import/tests/errors.server.test.ts | 4 ++-- .../nuxt-3-min/pages/fetch-server-error.vue | 2 +- .../nuxt-3-min/pages/test-param/[param].vue | 7 +++---- .../nuxt-3-min/tests/errors.server.test.ts | 4 ++-- .../nuxt-3-top-level-import/pages/fetch-server-error.vue | 2 +- .../nuxt-3-top-level-import/pages/test-param/[param].vue | 7 +++---- .../nuxt-3-top-level-import/tests/errors.server.test.ts | 6 +++--- .../test-applications/nuxt-3/pages/fetch-server-error.vue | 2 +- .../test-applications/nuxt-3/pages/test-param/[param].vue | 7 +++---- .../test-applications/nuxt-3/tests/errors.server.test.ts | 4 ++-- .../nuxt-4/app/pages/fetch-server-error.vue | 2 +- .../nuxt-4/app/pages/test-param/[param].vue | 7 +++---- .../test-applications/nuxt-4/tests/errors.server.test.ts | 4 ++-- 15 files changed, 31 insertions(+), 36 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue index 25ed45233d98..0e9aeb34b4fc 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue @@ -1,6 +1,6 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue index e83392b37b5c..019404aaf460 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue @@ -1,12 +1,12 @@ + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index 511880da17a6..f8bcc819dc58 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -10,7 +10,7 @@ test.describe('distributed tracing', () => { }); const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction.includes('GET /test-param/'); + return txnEvent.transaction?.includes('GET /test-param/') ?? false; }); const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ @@ -67,66 +67,87 @@ test.describe('distributed tracing', () => { expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); }); - test('capture a distributed server request with parametrization', async ({ page }) => { + test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { const clientTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/test-param/:param()'; + return txnEvent.transaction === '/test-param/fetch-api/:param()'; }); - const ssrTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction.includes('GET /test-param/'); + return txnEvent.transaction?.includes('GET /test-param/fetch-api') ?? false; }); - const serverReqTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction.includes('GET /api/test-param/'); + return txnEvent.transaction?.includes('GET /api/test-param/') ?? false; }); - const [, clientTxnEvent, ssrTxnEvent, , , serverReqTxnEvent] = await Promise.all([ - page.goto(`/test-param/${PARAM}`), + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/fetch-api/${PARAM}`); + + const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ clientTxnEventPromise, ssrTxnEventPromise, - expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), - page.getByText('Fetch Server Data', { exact: true }).click(), serverReqTxnEventPromise, ]); const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/test-param/${PARAM}`); - expect(ssrTxnEvent).toMatchObject({ - transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro) - transaction_info: { source: 'url' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.otel.http', - }, - }, - }); - - expect(httpClientSpan).toMatchObject({ - description: `GET /api/test-param/${PARAM}`, // todo: parametrize (nitro) - parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent - data: expect.objectContaining({ - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'http.request_method': 'GET', + expect(clientTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: '/test-param/fetch-api/:param()', // parametrized route + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'pageload', + origin: 'auto.pageload.vue', + }), + }), }), - }); - - expect(serverReqTxnEvent).toMatchObject({ - transaction: `GET /api/test-param/${PARAM}`, // todo: parametrize (nitro) - transaction_info: { source: 'url' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.otel.http', - parent_span_id: httpClientSpan?.span_id, // http.client span is parent - }, - }, - }); + ); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toEqual( + expect.objectContaining({ + description: `GET /api/test-param/${PARAM}`, // fixme: parametrize + parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent + data: expect.objectContaining({ + url: `/api/test-param/${PARAM}`, // fixme: parametrize + type: 'fetch', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'http.method': 'GET', + }), + }), + ); + + expect(ssrTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /test-param/fetch-api/${PARAM}`, // fixme: parametrize (nitro) + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + }), + }), + }), + ); + + expect(serverReqTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /api/test-param/${PARAM}`, + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + }), + }), + }), + ); - // All share the same trace_id + // All 3 transactions and the http.client span should share the same trace_id expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); From 56c916f03c5c608ea445adc30c179021544b2696 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 7 Jul 2025 13:56:57 +0200 Subject: [PATCH 05/10] Standardize test case --- dev-packages/e2e-tests/README.md | 19 ++++++++++++++++++ .../pages/test-param/fetch-api/[param].vue | 16 --------------- .../app/pages/test-param/user/[userId].vue | 16 +++++++++++++++ .../nuxt-4/server/api/user/[userId].ts | 7 +++++++ .../nuxt-4/tests/tracing.test.ts | 20 +++++++++---------- 5 files changed, 52 insertions(+), 26 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/fetch-api/[param].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 919d74e78542..7a52495d3b4c 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -133,3 +133,22 @@ A standardized frontend test application has the following features: ### Standardized Backend Test Apps TBD + +### Standardized Frontend-to-Backend Test Apps + +A standardized Meta test application has the following features: + +- Has a parameterized backend API route `/user/:id` that returns a JSON object with the user ID. +- Has a parameterized frontend page (can be SSR) `/user/:id` that fetches the user data on the client-side from the API route and displays it. + +The following test cases for connected tracing should be implemented in the test app: + +- Capturing a distributed page load trace when a page is loaded + - The HTML meta-tag should include the Sentry trace data and baggage + - The server root span should be the parent of the client pageload span + - All routes (server and client) should be parameterized, e.g. `/user/5` should be captured as `/user/:id` route +- Capturing a distributed trace when requesting the API from the client-side + - There should be three transactions involved: the client pageload, the server "pageload", and the server API request + - The client pageload should include an `http.client` span that is the parent of the server API request span + - All three transactions and the `http.client` span should share the same `trace_id` + - All `transaction` names and the `span` description should be parameterized, e.g. `/user/5` should be captured as `/user/:id` route diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/fetch-api/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/fetch-api/[param].vue deleted file mode 100644 index 8a65881c84e8..000000000000 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/fetch-api/[param].vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue new file mode 100644 index 000000000000..41daf0460b05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue @@ -0,0 +1,16 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts new file mode 100644 index 000000000000..eb268d2f3e13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index f8bcc819dc58..bb824f60f7d1 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -69,17 +69,17 @@ test.describe('distributed tracing', () => { test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { const clientTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/test-param/fetch-api/:param()'; + return txnEvent.transaction === '/test-param/user/:userId()'; }); const ssrTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /test-param/fetch-api') ?? false; + return txnEvent.transaction?.includes('GET /test-param/user') ?? false; }); const serverReqTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /api/test-param/') ?? false; + return txnEvent.transaction?.includes('GET /api/user/') ?? false; }); // Navigate to the page which will trigger an API call from the client-side - await page.goto(`/test-param/fetch-api/${PARAM}`); + await page.goto(`/test-param/user/${PARAM}`); const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ clientTxnEventPromise, @@ -87,12 +87,12 @@ test.describe('distributed tracing', () => { serverReqTxnEventPromise, ]); - const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/test-param/${PARAM}`); + const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`); expect(clientTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: '/test-param/fetch-api/:param()', // parametrized route + transaction: '/test-param/user/:userId()', // parametrized route transaction_info: { source: 'route' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ @@ -106,10 +106,10 @@ test.describe('distributed tracing', () => { expect(httpClientSpan).toBeDefined(); expect(httpClientSpan).toEqual( expect.objectContaining({ - description: `GET /api/test-param/${PARAM}`, // fixme: parametrize + description: `GET /api/user/${PARAM}`, // fixme: parametrize parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent data: expect.objectContaining({ - url: `/api/test-param/${PARAM}`, // fixme: parametrize + url: `/api/user/${PARAM}`, type: 'fetch', 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.browser', @@ -121,7 +121,7 @@ test.describe('distributed tracing', () => { expect(ssrTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /test-param/fetch-api/${PARAM}`, // fixme: parametrize (nitro) + transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro) transaction_info: { source: 'url' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ @@ -135,7 +135,7 @@ test.describe('distributed tracing', () => { expect(serverReqTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /api/test-param/${PARAM}`, + transaction: `GET /api/user/${PARAM}`, transaction_info: { source: 'url' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ From 2be99a2165ce9290fdb41f14c4d21c758bf3b9f8 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 7 Jul 2025 14:20:38 +0200 Subject: [PATCH 06/10] add distributed test case to all nuxt tests --- .../pages/test-param/user/[userId].vue | 16 ++++ .../server/api/user/[userId].ts | 7 ++ .../tests/tracing.test.ts | 87 +++++++++++++++++++ .../pages/test-param/user/[userId].vue | 16 ++++ .../nuxt-3-min/server/api/user/[userId].ts | 7 ++ .../nuxt-3-min/tests/tracing.test.ts | 87 +++++++++++++++++++ .../pages/test-param/user/[userId].vue | 16 ++++ .../server/api/user/[userId].ts | 7 ++ .../tests/tracing.test.ts | 87 +++++++++++++++++++ .../nuxt-3/pages/test-param/user/[userId].vue | 16 ++++ .../nuxt-3/server/api/user/[userId].ts | 7 ++ .../nuxt-3/tests/tracing.test.ts | 87 +++++++++++++++++++ 12 files changed, 440 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/user/[userId].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/user/[userId].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/user/[userId].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/user/[userId].vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/user/[userId].vue new file mode 100644 index 000000000000..41daf0460b05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/user/[userId].vue @@ -0,0 +1,16 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts new file mode 100644 index 000000000000..eb268d2f3e13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts index 0bc6ffa80b73..17c2704279b3 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts @@ -66,4 +66,91 @@ test.describe('distributed tracing', () => { expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); }); + + test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { + return txnEvent.transaction === '/test-param/user/:userId()'; + }); + const ssrTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { + return txnEvent.transaction?.includes('GET /test-param/user') ?? false; + }); + const serverReqTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { + return txnEvent.transaction?.includes('GET /api/user/') ?? false; + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}`); + + const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ + clientTxnEventPromise, + ssrTxnEventPromise, + serverReqTxnEventPromise, + ]); + + const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`); + + expect(clientTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: '/test-param/user/:userId()', // parametrized route + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'pageload', + origin: 'auto.pageload.vue', + }), + }), + }), + ); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toEqual( + expect.objectContaining({ + description: `GET /api/user/${PARAM}`, // fixme: parametrize + parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent + data: expect.objectContaining({ + url: `/api/user/${PARAM}`, + type: 'fetch', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'http.method': 'GET', + }), + }), + ); + + expect(ssrTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro) + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + }), + }), + }), + ); + + expect(serverReqTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /api/user/${PARAM}`, + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + }), + }), + }), + ); + + // All 3 transactions and the http.client span should share the same trace_id + expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/user/[userId].vue new file mode 100644 index 000000000000..41daf0460b05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/user/[userId].vue @@ -0,0 +1,16 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts new file mode 100644 index 000000000000..eb268d2f3e13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts index cb86df11fe84..0daa9a856236 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts @@ -66,4 +66,91 @@ test.describe('distributed tracing', () => { expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); }); + + test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { + return txnEvent.transaction === '/test-param/user/:userId()'; + }); + const ssrTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { + return txnEvent.transaction?.includes('GET /test-param/user') ?? false; + }); + const serverReqTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { + return txnEvent.transaction?.includes('GET /api/user/') ?? false; + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}`); + + const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ + clientTxnEventPromise, + ssrTxnEventPromise, + serverReqTxnEventPromise, + ]); + + const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`); + + expect(clientTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: '/test-param/user/:userId()', // parametrized route + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'pageload', + origin: 'auto.pageload.vue', + }), + }), + }), + ); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toEqual( + expect.objectContaining({ + description: `GET /api/user/${PARAM}`, // fixme: parametrize + parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent + data: expect.objectContaining({ + url: `/api/user/${PARAM}`, + type: 'fetch', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'http.method': 'GET', + }), + }), + ); + + expect(ssrTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro) + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + }), + }), + }), + ); + + expect(serverReqTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /api/user/${PARAM}`, + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + }), + }), + }), + ); + + // All 3 transactions and the http.client span should share the same trace_id + expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/user/[userId].vue new file mode 100644 index 000000000000..41daf0460b05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/user/[userId].vue @@ -0,0 +1,16 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts new file mode 100644 index 000000000000..eb268d2f3e13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts index 69c4bd2833c4..5248926e30fb 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts @@ -66,4 +66,91 @@ test.describe('distributed tracing', () => { expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); }); + + test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => { + return txnEvent.transaction === '/test-param/user/:userId()'; + }); + const ssrTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => { + return txnEvent.transaction?.includes('GET /test-param/user') ?? false; + }); + const serverReqTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => { + return txnEvent.transaction?.includes('GET /api/user/') ?? false; + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}`); + + const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ + clientTxnEventPromise, + ssrTxnEventPromise, + serverReqTxnEventPromise, + ]); + + const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`); + + expect(clientTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: '/test-param/user/:userId()', // parametrized route + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'pageload', + origin: 'auto.pageload.vue', + }), + }), + }), + ); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toEqual( + expect.objectContaining({ + description: `GET /api/user/${PARAM}`, // fixme: parametrize + parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent + data: expect.objectContaining({ + url: `/api/user/${PARAM}`, + type: 'fetch', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'http.method': 'GET', + }), + }), + ); + + expect(ssrTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro) + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + }), + }), + }), + ); + + expect(serverReqTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /api/user/${PARAM}`, + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + }), + }), + }), + ); + + // All 3 transactions and the http.client span should share the same trace_id + expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/user/[userId].vue new file mode 100644 index 000000000000..41daf0460b05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/user/[userId].vue @@ -0,0 +1,16 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts new file mode 100644 index 000000000000..eb268d2f3e13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts index 523ece4cc085..58eb03eccb88 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts @@ -66,4 +66,91 @@ test.describe('distributed tracing', () => { expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); }); + + test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction === '/test-param/user/:userId()'; + }); + const ssrTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /test-param/user') ?? false; + }); + const serverReqTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => { + return txnEvent.transaction?.includes('GET /api/user/') ?? false; + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}`); + + const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ + clientTxnEventPromise, + ssrTxnEventPromise, + serverReqTxnEventPromise, + ]); + + const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`); + + expect(clientTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: '/test-param/user/:userId()', // parametrized route + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'pageload', + origin: 'auto.pageload.vue', + }), + }), + }), + ); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toEqual( + expect.objectContaining({ + description: `GET /api/user/${PARAM}`, // fixme: parametrize + parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent + data: expect.objectContaining({ + url: `/api/user/${PARAM}`, + type: 'fetch', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'http.method': 'GET', + }), + }), + ); + + expect(ssrTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro) + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + }), + }), + }), + ); + + expect(serverReqTxnEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: `GET /api/user/${PARAM}`, + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.otel.http', + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + }), + }), + }), + ); + + // All 3 transactions and the http.client span should share the same trace_id + expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id); + }); }); From 32d72fbb42aded3d197cbf4924f8505eb6d4719d Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 7 Jul 2025 14:23:55 +0200 Subject: [PATCH 07/10] update readme --- dev-packages/e2e-tests/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 7a52495d3b4c..a1eb1b559b9c 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -136,11 +136,13 @@ TBD ### Standardized Frontend-to-Backend Test Apps -A standardized Meta test application has the following features: +A standardized Meta-Framework test application has the following features: - Has a parameterized backend API route `/user/:id` that returns a JSON object with the user ID. - Has a parameterized frontend page (can be SSR) `/user/:id` that fetches the user data on the client-side from the API route and displays it. +This setup creates the scenario where the frontend page loads, and then immediately makes an API request to the backend API. + The following test cases for connected tracing should be implemented in the test app: - Capturing a distributed page load trace when a page is loaded From 3537d54635e46285e5a28e9c4954c516ab3e4c96 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 1 Jul 2025 17:38:18 +0200 Subject: [PATCH 08/10] fix(nuxt): Parametrize routes on the server-side --- .../tests/tracing.test.ts | 6 +-- .../nuxt-3-min/tests/tracing.test.ts | 4 +- .../nuxt-3/tests/tracing.test.ts | 6 +-- .../nuxt-4/tests/tracing.test.ts | 6 +-- .../hooks/updateRouteBeforeResponse.ts | 39 +++++++++++++++++++ .../plugins/sentry-cloudflare.server.ts | 3 ++ .../nuxt/src/runtime/plugins/sentry.server.ts | 3 ++ 7 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts index 17c2704279b3..2cb69802dd57 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts @@ -10,7 +10,7 @@ test.describe('distributed tracing', () => { }); const serverTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { - return txnEvent.transaction.includes('GET /test-param/'); + return txnEvent.transaction?.includes('GET /test-param/') || false; }); const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ @@ -47,8 +47,8 @@ test.describe('distributed tracing', () => { }); expect(serverTxnEvent).toMatchObject({ - transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro) - transaction_info: { source: 'url' }, + transaction: `GET /test-param/:param`, + transaction_info: { source: 'route' }, type: 'transaction', contexts: { trace: { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts index 0daa9a856236..15f85dd301b4 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts @@ -47,8 +47,8 @@ test.describe('distributed tracing', () => { }); expect(serverTxnEvent).toMatchObject({ - transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro) - transaction_info: { source: 'url' }, + transaction: `GET /test-param/:param`, + transaction_info: { source: 'route' }, type: 'transaction', contexts: { trace: { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts index 58eb03eccb88..0eae9bc910ef 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts @@ -23,7 +23,7 @@ test.describe('distributed tracing', () => { const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`); - expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F${PARAM}`); // URL-encoded for 'GET /test-param/s0me-param' + expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`); // URL-encoded for 'GET /test-param/:param' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); expect(baggageMetaTagContent).toContain('sentry-sample_rate=1'); @@ -47,8 +47,8 @@ test.describe('distributed tracing', () => { }); expect(serverTxnEvent).toMatchObject({ - transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro) - transaction_info: { source: 'url' }, + transaction: `GET /test-param/:param`, + transaction_info: { source: 'route' }, type: 'transaction', contexts: { trace: { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index bb824f60f7d1..7685d9955791 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -23,7 +23,7 @@ test.describe('distributed tracing', () => { const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`); - expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F${PARAM}`); // URL-encoded for 'GET /test-param/s0me-param' + expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`); // URL-encoded for 'GET /test-param/:param' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); expect(baggageMetaTagContent).toContain('sentry-sample_rate=1'); @@ -47,8 +47,8 @@ test.describe('distributed tracing', () => { }); expect(serverTxnEvent).toMatchObject({ - transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro) - transaction_info: { source: 'url' }, + transaction: `GET /test-param/:param`, + transaction_info: { source: 'route' }, type: 'transaction', contexts: { trace: { diff --git a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts new file mode 100644 index 000000000000..d1c0b7801cc2 --- /dev/null +++ b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts @@ -0,0 +1,39 @@ +import { getActiveSpan, getCurrentScope, getRootSpan, logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import type { H3Event } from 'h3'; + +/** + * Update the root span (transaction) name for routes with parameters based on the matched route. + */ +export function updateRouteBeforeResponse(event: H3Event): void { + if (event.context.matchedRoute) { + const matchedRoute = event.context.matchedRoute; + const matchedRoutePath = matchedRoute.path; + const params = event.context?.params || null; + const method = event._method || 'GET'; + + // If the matched route path is defined and differs from the event's path, it indicates a parametrized route + // Example: If the matched route is "/users/:id" and the event's path is "/users/123", + if (matchedRoutePath && matchedRoutePath !== event._path) { + const parametrizedTransactionName = `${method.toUpperCase()} ${matchedRoutePath}`; + getCurrentScope().setTransactionName(parametrizedTransactionName); + + const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + if (rootSpan) { + rootSpan.updateName(parametrizedTransactionName); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + rootSpan.setAttribute('http.route', matchedRoutePath); + + if (params && typeof params === 'object') { + Object.entries(params).forEach(([key, value]) => { + rootSpan.setAttribute(`params.${key}`, String(value)); + }); + } + + logger.log(`Updated transaction name for parametrized route: ${parametrizedTransactionName}`); + } + } + } + } +} diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 9d10e9bd86d0..96fa59a4c643 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -6,6 +6,7 @@ import type { H3Event } from 'h3'; import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; +import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse'; import { addSentryTracingMetaTags } from '../utils'; interface CfEventType { @@ -139,6 +140,8 @@ export const sentryCloudflareNitroPlugin = }, }); + nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse); + // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => { const storedTraceData = event?.context?.cf ? traceDataMap.get(event.context.cf) : undefined; diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index baf9f2029051..0f13fbec0fd3 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -5,11 +5,14 @@ import { type EventHandler } from 'h3'; import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; +import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse'; import { addSentryTracingMetaTags, flushIfServerless } from '../utils'; export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); + nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse); + nitroApp.hooks.hook('error', sentryCaptureErrorHook); // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context From 4e75465156ef51677e66cf6aba549c7012f0ec71 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 7 Jul 2025 16:29:48 +0200 Subject: [PATCH 09/10] parametrize server api requests --- .../tests/tracing.test.ts | 4 ++-- .../nuxt-3-min/tests/tracing.test.ts | 4 ++-- .../tests/tracing.test.ts | 4 ++-- .../nuxt-3/tests/tracing.test.ts | 4 ++-- .../nuxt-4/tests/tracing.test.ts | 10 ++++---- .../hooks/updateRouteBeforeResponse.ts | 23 +++++++++++++------ 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts index 2cb69802dd57..216c3b27ec62 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => { expect(serverReqTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /api/user/${PARAM}`, - transaction_info: { source: 'url' }, + transaction: `GET /api/user/:userId`, // parametrized route + transaction_info: { source: 'route' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ op: 'http.server', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts index 15f85dd301b4..214bbdbb5f11 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => { expect(serverReqTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /api/user/${PARAM}`, - transaction_info: { source: 'url' }, + transaction: `GET /api/user/:userId`, // parametrized route + transaction_info: { source: 'route' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ op: 'http.server', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts index 5248926e30fb..9199daa8f074 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => { expect(serverReqTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /api/user/${PARAM}`, - transaction_info: { source: 'url' }, + transaction: `GET /api/user/:userId`, // parametrized route + transaction_info: { source: 'route' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ op: 'http.server', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts index 0eae9bc910ef..0be48299c0a5 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => { expect(serverReqTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /api/user/${PARAM}`, - transaction_info: { source: 'url' }, + transaction: `GET /api/user/:userId`, // parametrized route + transaction_info: { source: 'route' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ op: 'http.server', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index 7685d9955791..5fa12df6b121 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -23,7 +23,7 @@ test.describe('distributed tracing', () => { const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`); - expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`); // URL-encoded for 'GET /test-param/:param' + expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F${PARAM}`); // URL-encoded for 'GET /test-param/s0me-param' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); expect(baggageMetaTagContent).toContain('sentry-sample_rate=1'); @@ -47,8 +47,8 @@ test.describe('distributed tracing', () => { }); expect(serverTxnEvent).toMatchObject({ - transaction: `GET /test-param/:param`, - transaction_info: { source: 'route' }, + transaction: `GET /test-param/${PARAM}`, + transaction_info: { source: 'url' }, type: 'transaction', contexts: { trace: { @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => { expect(serverReqTxnEvent).toEqual( expect.objectContaining({ type: 'transaction', - transaction: `GET /api/user/${PARAM}`, - transaction_info: { source: 'url' }, + transaction: `GET /api/user/:userId`, // parametrized route + transaction_info: { source: 'route' }, contexts: expect.objectContaining({ trace: expect.objectContaining({ op: 'http.server', diff --git a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts index d1c0b7801cc2..77805c734b6d 100644 --- a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts +++ b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts @@ -6,14 +6,19 @@ import type { H3Event } from 'h3'; */ export function updateRouteBeforeResponse(event: H3Event): void { if (event.context.matchedRoute) { - const matchedRoute = event.context.matchedRoute; - const matchedRoutePath = matchedRoute.path; - const params = event.context?.params || null; - const method = event._method || 'GET'; + const matchedRoutePath = event.context.matchedRoute.path; // If the matched route path is defined and differs from the event's path, it indicates a parametrized route - // Example: If the matched route is "/users/:id" and the event's path is "/users/123", + // Example: Matched route is "/users/:id" and the event's path is "/users/123", if (matchedRoutePath && matchedRoutePath !== event._path) { + if (matchedRoutePath === '/**') { + // todo: support parametrized SSR pageload spans + // If page is server-side rendered, the whole path gets transformed to `/**` (Example : `/users/123` becomes `/**` instead of `/users/:id`). + return; // Skip if the matched route is a catch-all route. + } + + const method = event._method || 'GET'; + const parametrizedTransactionName = `${method.toUpperCase()} ${matchedRoutePath}`; getCurrentScope().setTransactionName(parametrizedTransactionName); @@ -22,8 +27,12 @@ export function updateRouteBeforeResponse(event: H3Event): void { const rootSpan = getRootSpan(activeSpan); if (rootSpan) { rootSpan.updateName(parametrizedTransactionName); - rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - rootSpan.setAttribute('http.route', matchedRoutePath); + rootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + 'http.route': matchedRoutePath, + }); + + const params = event.context?.params || null; if (params && typeof params === 'object') { Object.entries(params).forEach(([key, value]) => { From 5362333c287b8e0cc5596c5a74f26fa48dce562d Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 7 Jul 2025 16:36:31 +0200 Subject: [PATCH 10/10] update attribute --- packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts index 77805c734b6d..4dd1db22bf8e 100644 --- a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts +++ b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts @@ -36,7 +36,8 @@ export function updateRouteBeforeResponse(event: H3Event): void { if (params && typeof params === 'object') { Object.entries(params).forEach(([key, value]) => { - rootSpan.setAttribute(`params.${key}`, String(value)); + // Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey + rootSpan.setAttribute(`url.path.parameter.${key}`, String(value)); }); }