Skip to content

fix(nuxt): Parametrize routes on the server-side #16785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions dev-packages/e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<div>
<button @click="fetchData">Fetch Server Data</button>
<button @click="fetchError">Fetch Server API Error</button>
</div>
</template>

<script setup lang="ts">
import { useFetch} from '#imports'
import { useFetch } from '#imports';
const fetchData = async () => {
const fetchError = async () => {
await useFetch('/api/server-error');
}
};
</script>
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useRoute, useFetch } from '#imports'
import { useRoute, useFetch } from '#imports';
const route = useRoute();
const param = route.params.param;
const fetchError = async () => {
await useFetch(`/api/param-error/${param}`);
}
};
const fetchData = async () => {
await useFetch(`/api/test-param/${param}`);
Expand All @@ -18,6 +18,5 @@ const fetchData = async () => {

<ErrorButton id="errorBtn" errorText="Error thrown from Param Route Button" />
<button @click="fetchData">Fetch Server Data</button>
<button @click="fetchError">Fetch Server Error</button>
<button @click="fetchError">Fetch Server API Error</button>
</template>

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { useFetch, useRoute } from '#imports';
const route = useRoute();
const userId = route.params.userId as string;
const { data } = await useFetch(`/api/user/${userId}`, {
server: false, // Don't fetch during SSR, only client-side
});
</script>

<template>
<div>
<p v-if="data">User ID: {{ data }}</p>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineEventHandler, getRouterParam } from '#imports';

export default defineEventHandler(event => {
const userId = getRouterParam(event, 'userId');

return `UserId Param: ${userId}!`;
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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: {
Expand 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-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/:userId`, // parametrized route
transaction_info: { source: 'route' },
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);
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<div>
<button @click="fetchData">Fetch Server Data</button>
<button @click="fetchError">Fetch Server API Error</button>
</div>
</template>

<script setup lang="ts">
import { useFetch} from '#imports'
import { useFetch } from '#imports';
const fetchData = async () => {
const fetchError = async () => {
await useFetch('/api/server-error');
}
};
</script>
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useRoute, useFetch } from '#imports'
import { useRoute, useFetch } from '#imports';
const route = useRoute();
const param = route.params.param;
const fetchError = async () => {
await useFetch(`/api/param-error/${param}`);
}
};
const fetchData = async () => {
await useFetch(`/api/test-param/${param}`);
Expand All @@ -18,6 +18,5 @@ const fetchData = async () => {

<ErrorButton id="errorBtn" errorText="Error thrown from Param Route Button" />
<button @click="fetchData">Fetch Server Data</button>
<button @click="fetchError">Fetch Server Error</button>
<button @click="fetchError">Fetch Server API Error</button>
</template>

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { useFetch, useRoute } from '#imports';
const route = useRoute();
const userId = route.params.userId as string;
const { data } = await useFetch(`/api/user/${userId}`, {
server: false, // Don't fetch during SSR, only client-side
});
</script>

<template>
<div>
<p v-if="data">User ID: {{ data }}</p>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineEventHandler, getRouterParam } from '#imports';

export default defineEventHandler(event => {
const userId = getRouterParam(event, 'userId');

return `UserId Param: ${userId}!`;
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down
Loading
Loading