Skip to content

Commit 868d01a

Browse files
authored
test(nuxt): Add test for distributed client-to-server request (#16788)
While working on route parametrization, [it came up in this PR](#16785) that there are different transactions/spans which should be parametrized: - `pageload` transaction -> it's already parametrized because of the `vueIntegration` - SSR `http.server` transaction -> not parametrized - `http.client` span when doing an extra server request -> not parametrized - `http.server` span when doing an extra server request -> not parametrized This test is added to have a visualization of the current state of parametrization to be able to iterate on it. **The PR is quite long but the test case and the tests are just copy-pasted to all Nuxt E2E tests.**
1 parent f342341 commit 868d01a

File tree

31 files changed

+618
-52
lines changed

31 files changed

+618
-52
lines changed

dev-packages/e2e-tests/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,24 @@ A standardized frontend test application has the following features:
133133
### Standardized Backend Test Apps
134134

135135
TBD
136+
137+
### Standardized Frontend-to-Backend Test Apps
138+
139+
A standardized Meta-Framework test application has the following features:
140+
141+
- Has a parameterized backend API route `/user/:id` that returns a JSON object with the user ID.
142+
- 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.
143+
144+
This setup creates the scenario where the frontend page loads, and then immediately makes an API request to the backend API.
145+
146+
The following test cases for connected tracing should be implemented in the test app:
147+
148+
- Capturing a distributed page load trace when a page is loaded
149+
- The HTML meta-tag should include the Sentry trace data and baggage
150+
- The server root span should be the parent of the client pageload span
151+
- All routes (server and client) should be parameterized, e.g. `/user/5` should be captured as `/user/:id` route
152+
- Capturing a distributed trace when requesting the API from the client-side
153+
- There should be three transactions involved: the client pageload, the server "pageload", and the server API request
154+
- The client pageload should include an `http.client` span that is the parent of the server API request span
155+
- All three transactions and the `http.client` span should share the same `trace_id`
156+
- All `transaction` names and the `span` description should be parameterized, e.g. `/user/5` should be captured as `/user/:id` route
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<template>
22
<div>
3-
<button @click="fetchData">Fetch Server Data</button>
3+
<button @click="fetchError">Fetch Server API Error</button>
44
</div>
55
</template>
66

77
<script setup lang="ts">
8-
import { useFetch} from '#imports'
8+
import { useFetch } from '#imports';
99
10-
const fetchData = async () => {
10+
const fetchError = async () => {
1111
await useFetch('/api/server-error');
12-
}
12+
};
1313
</script>
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<script setup lang="ts">
2-
import { useRoute, useFetch } from '#imports'
2+
import { useRoute, useFetch } from '#imports';
33
44
const route = useRoute();
55
const param = route.params.param;
66
77
const fetchError = async () => {
88
await useFetch(`/api/param-error/${param}`);
9-
}
9+
};
1010
1111
const fetchData = async () => {
1212
await useFetch(`/api/test-param/${param}`);
@@ -18,6 +18,5 @@ const fetchData = async () => {
1818

1919
<ErrorButton id="errorBtn" errorText="Error thrown from Param Route Button" />
2020
<button @click="fetchData">Fetch Server Data</button>
21-
<button @click="fetchError">Fetch Server Error</button>
21+
<button @click="fetchError">Fetch Server API Error</button>
2222
</template>
23-
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup lang="ts">
2+
import { useFetch, useRoute } from '#imports';
3+
4+
const route = useRoute();
5+
const userId = route.params.userId as string;
6+
7+
const { data } = await useFetch(`/api/user/${userId}`, {
8+
server: false, // Don't fetch during SSR, only client-side
9+
});
10+
</script>
11+
12+
<template>
13+
<div>
14+
<p v-if="data">User ID: {{ data }}</p>
15+
</div>
16+
</template>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineEventHandler, getRouterParam } from '#imports';
2+
3+
export default defineEventHandler(event => {
4+
const userId = getRouterParam(event, 'userId');
5+
6+
return `UserId Param: ${userId}!`;
7+
});

dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test.describe('server-side errors', async () => {
88
});
99

1010
await page.goto(`/fetch-server-error`);
11-
await page.getByText('Fetch Server Data', { exact: true }).click();
11+
await page.getByText('Fetch Server API Error', { exact: true }).click();
1212

1313
const error = await errorPromise;
1414

@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
2626
});
2727

2828
await page.goto(`/test-param/1234`);
29-
await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
29+
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
3030

3131
const error = await errorPromise;
3232

dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
6666
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
6767
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
6868
});
69+
70+
test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
71+
const clientTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
72+
return txnEvent.transaction === '/test-param/user/:userId()';
73+
});
74+
const ssrTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
75+
return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
76+
});
77+
const serverReqTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
78+
return txnEvent.transaction?.includes('GET /api/user/') ?? false;
79+
});
80+
81+
// Navigate to the page which will trigger an API call from the client-side
82+
await page.goto(`/test-param/user/${PARAM}`);
83+
84+
const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
85+
clientTxnEventPromise,
86+
ssrTxnEventPromise,
87+
serverReqTxnEventPromise,
88+
]);
89+
90+
const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
91+
92+
expect(clientTxnEvent).toEqual(
93+
expect.objectContaining({
94+
type: 'transaction',
95+
transaction: '/test-param/user/:userId()', // parametrized route
96+
transaction_info: { source: 'route' },
97+
contexts: expect.objectContaining({
98+
trace: expect.objectContaining({
99+
op: 'pageload',
100+
origin: 'auto.pageload.vue',
101+
}),
102+
}),
103+
}),
104+
);
105+
106+
expect(httpClientSpan).toBeDefined();
107+
expect(httpClientSpan).toEqual(
108+
expect.objectContaining({
109+
description: `GET /api/user/${PARAM}`, // fixme: parametrize
110+
parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
111+
data: expect.objectContaining({
112+
url: `/api/user/${PARAM}`,
113+
type: 'fetch',
114+
'sentry.op': 'http.client',
115+
'sentry.origin': 'auto.http.browser',
116+
'http.method': 'GET',
117+
}),
118+
}),
119+
);
120+
121+
expect(ssrTxnEvent).toEqual(
122+
expect.objectContaining({
123+
type: 'transaction',
124+
transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
125+
transaction_info: { source: 'url' },
126+
contexts: expect.objectContaining({
127+
trace: expect.objectContaining({
128+
op: 'http.server',
129+
origin: 'auto.http.otel.http',
130+
}),
131+
}),
132+
}),
133+
);
134+
135+
expect(serverReqTxnEvent).toEqual(
136+
expect.objectContaining({
137+
type: 'transaction',
138+
transaction: `GET /api/user/${PARAM}`,
139+
transaction_info: { source: 'url' },
140+
contexts: expect.objectContaining({
141+
trace: expect.objectContaining({
142+
op: 'http.server',
143+
origin: 'auto.http.otel.http',
144+
parent_span_id: httpClientSpan?.span_id, // http.client span is parent
145+
}),
146+
}),
147+
}),
148+
);
149+
150+
// All 3 transactions and the http.client span should share the same trace_id
151+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
152+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
153+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
154+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
155+
});
69156
});
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<template>
22
<div>
3-
<button @click="fetchData">Fetch Server Data</button>
3+
<button @click="fetchError">Fetch Server API Error</button>
44
</div>
55
</template>
66

77
<script setup lang="ts">
8-
import { useFetch} from '#imports'
8+
import { useFetch } from '#imports';
99
10-
const fetchData = async () => {
10+
const fetchError = async () => {
1111
await useFetch('/api/server-error');
12-
}
12+
};
1313
</script>
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<script setup lang="ts">
2-
import { useRoute, useFetch } from '#imports'
2+
import { useRoute, useFetch } from '#imports';
33
44
const route = useRoute();
55
const param = route.params.param;
66
77
const fetchError = async () => {
88
await useFetch(`/api/param-error/${param}`);
9-
}
9+
};
1010
1111
const fetchData = async () => {
1212
await useFetch(`/api/test-param/${param}`);
@@ -18,6 +18,5 @@ const fetchData = async () => {
1818

1919
<ErrorButton id="errorBtn" errorText="Error thrown from Param Route Button" />
2020
<button @click="fetchData">Fetch Server Data</button>
21-
<button @click="fetchError">Fetch Server Error</button>
21+
<button @click="fetchError">Fetch Server API Error</button>
2222
</template>
23-
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup lang="ts">
2+
import { useFetch, useRoute } from '#imports';
3+
4+
const route = useRoute();
5+
const userId = route.params.userId as string;
6+
7+
const { data } = await useFetch(`/api/user/${userId}`, {
8+
server: false, // Don't fetch during SSR, only client-side
9+
});
10+
</script>
11+
12+
<template>
13+
<div>
14+
<p v-if="data">User ID: {{ data }}</p>
15+
</div>
16+
</template>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineEventHandler, getRouterParam } from '#imports';
2+
3+
export default defineEventHandler(event => {
4+
const userId = getRouterParam(event, 'userId');
5+
6+
return `UserId Param: ${userId}!`;
7+
});

dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test.describe('server-side errors', async () => {
88
});
99

1010
await page.goto(`/fetch-server-error`);
11-
await page.getByText('Fetch Server Data', { exact: true }).click();
11+
await page.getByText('Fetch Server API Error', { exact: true }).click();
1212

1313
const error = await errorPromise;
1414

@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
2626
});
2727

2828
await page.goto(`/test-param/1234`);
29-
await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
29+
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
3030

3131
const error = await errorPromise;
3232

dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
6666
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
6767
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
6868
});
69+
70+
test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
71+
const clientTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => {
72+
return txnEvent.transaction === '/test-param/user/:userId()';
73+
});
74+
const ssrTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => {
75+
return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
76+
});
77+
const serverReqTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => {
78+
return txnEvent.transaction?.includes('GET /api/user/') ?? false;
79+
});
80+
81+
// Navigate to the page which will trigger an API call from the client-side
82+
await page.goto(`/test-param/user/${PARAM}`);
83+
84+
const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
85+
clientTxnEventPromise,
86+
ssrTxnEventPromise,
87+
serverReqTxnEventPromise,
88+
]);
89+
90+
const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
91+
92+
expect(clientTxnEvent).toEqual(
93+
expect.objectContaining({
94+
type: 'transaction',
95+
transaction: '/test-param/user/:userId()', // parametrized route
96+
transaction_info: { source: 'route' },
97+
contexts: expect.objectContaining({
98+
trace: expect.objectContaining({
99+
op: 'pageload',
100+
origin: 'auto.pageload.vue',
101+
}),
102+
}),
103+
}),
104+
);
105+
106+
expect(httpClientSpan).toBeDefined();
107+
expect(httpClientSpan).toEqual(
108+
expect.objectContaining({
109+
description: `GET /api/user/${PARAM}`, // fixme: parametrize
110+
parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
111+
data: expect.objectContaining({
112+
url: `/api/user/${PARAM}`,
113+
type: 'fetch',
114+
'sentry.op': 'http.client',
115+
'sentry.origin': 'auto.http.browser',
116+
'http.method': 'GET',
117+
}),
118+
}),
119+
);
120+
121+
expect(ssrTxnEvent).toEqual(
122+
expect.objectContaining({
123+
type: 'transaction',
124+
transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
125+
transaction_info: { source: 'url' },
126+
contexts: expect.objectContaining({
127+
trace: expect.objectContaining({
128+
op: 'http.server',
129+
origin: 'auto.http.otel.http',
130+
}),
131+
}),
132+
}),
133+
);
134+
135+
expect(serverReqTxnEvent).toEqual(
136+
expect.objectContaining({
137+
type: 'transaction',
138+
transaction: `GET /api/user/${PARAM}`,
139+
transaction_info: { source: 'url' },
140+
contexts: expect.objectContaining({
141+
trace: expect.objectContaining({
142+
op: 'http.server',
143+
origin: 'auto.http.otel.http',
144+
parent_span_id: httpClientSpan?.span_id, // http.client span is parent
145+
}),
146+
}),
147+
}),
148+
);
149+
150+
// All 3 transactions and the http.client span should share the same trace_id
151+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
152+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
153+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
154+
expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
155+
});
69156
});
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<template>
22
<div>
3-
<button @click="fetchData">Fetch Server Data</button>
3+
<button @click="fetchError">Fetch Server API Error</button>
44
</div>
55
</template>
66

77
<script setup lang="ts">
8-
import { useFetch} from '#imports'
8+
import { useFetch } from '#imports';
99
10-
const fetchData = async () => {
10+
const fetchError = async () => {
1111
await useFetch('/api/server-error');
12-
}
12+
};
1313
</script>

0 commit comments

Comments
 (0)