diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md
index 919d74e78542..a1eb1b559b9c 100644
--- a/dev-packages/e2e-tests/README.md
+++ b/dev-packages/e2e-tests/README.md
@@ -133,3 +133,24 @@ A standardized frontend test application has the following features:
### Standardized Backend Test Apps
TBD
+
+### Standardized Frontend-to-Backend Test Apps
+
+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
+ - 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-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..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,13 +1,13 @@
- Fetch Server Data
+ Fetch Server API Error
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-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/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts
index b781642c2b4f..b18da9ba0a3b 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 API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
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/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 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 @@
- Fetch Server Data
+ Fetch Server API Error
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
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/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts
index 8f20aa938893..40904b51b993 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 API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
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/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 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 @@
- Fetch Server Data
+ Fetch Server API Error
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
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/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts
index 053ec5b6ab67..551a33ee1fc1 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 API 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 API Error', { exact: true }).click();
const transactionEvent = await transactionEventPromise;
const error = await errorPromise;
@@ -57,7 +57,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
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/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 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 @@
- Fetch Server Data
+ Fetch Server API Error
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
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/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts
index d1556d511bf0..b62654b60c47 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 API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
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);
+ });
});
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..0e9aeb34b4fc 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 @@
- Fetch Server Data
+ Fetch Server API Error
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
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/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts
index 396870d19925..1d593cb09caf 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 API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
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..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
@@ -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([
@@ -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-4', txnEvent => {
+ return txnEvent.transaction === '/test-param/user/:userId()';
+ });
+ const ssrTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
+ });
+ const serverReqTxnEventPromise = waitForTransaction('nuxt-4', 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);
+ });
});