Skip to content

Commit dac0aff

Browse files
authored
feat(v9/astro): Parametrize dynamic server routes (#17141)
Backport of all server-related parametrization in Astro: - #17054 - #17085 - #17102 - #17105
1 parent b6cad7b commit dac0aff

File tree

12 files changed

+451
-12
lines changed

12 files changed

+451
-12
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const prerender = false;
2+
3+
export function GET({ params }) {
4+
return new Response(
5+
JSON.stringify({
6+
greeting: `Hello ${params.userId}`,
7+
userId: params.userId,
8+
}),
9+
);
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import Layout from '../../layouts/Layout.astro';
3+
4+
export const prerender = false;
5+
6+
const params = Astro.params;
7+
8+
---
9+
10+
<Layout title="CatchAll SSR page">
11+
<p>params: {params}</p>
12+
</Layout>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
import Layout from '../../layouts/Layout.astro';
3+
4+
export const prerender = false;
5+
6+
const { userId } = Astro.params;
7+
8+
const response = await fetch(Astro.url.origin + `/api/user/${userId}.json`)
9+
const data = await response.json();
10+
11+
---
12+
13+
<Layout title="Dynamic SSR page">
14+
<h1>{data.greeting}</h1>
15+
16+
<p>data: {JSON.stringify(data)}</p>
17+
</Layout>

dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,196 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
120120
});
121121
});
122122
});
123+
124+
test.describe('nested SSR routes (client, server, server request)', () => {
125+
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
126+
* pageload — /user-page/myUsername123
127+
* ├── browser.** — multiple browser spans
128+
* └── browser.request — /user-page/myUsername123
129+
* └── http.server — GET /user-page/[userId] (SSR page request)
130+
* └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span)
131+
* └── http.server — GET /api/user/myUsername123.json (server request)
132+
*/
133+
test('sends connected server and client pageload and request spans with the same trace id', async ({ page }) => {
134+
const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => {
135+
return txnEvent?.transaction?.startsWith('/user-page/') ?? false;
136+
});
137+
138+
const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => {
139+
return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false;
140+
});
141+
142+
const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-4', txnEvent => {
143+
return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false;
144+
});
145+
146+
await page.goto('/user-page/myUsername123');
147+
148+
const clientPageloadTxn = await clientPageloadTxnPromise;
149+
const serverPageRequestTxn = await serverPageRequestTxnPromise;
150+
const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise;
151+
const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find(
152+
span => span.op === 'http.client' && span.description?.includes('/api/user/'),
153+
);
154+
155+
const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id;
156+
157+
// Verify all spans have the same trace ID
158+
expect(clientPageloadTraceId).toEqual(serverPageRequestTxn.contexts?.trace?.trace_id);
159+
expect(clientPageloadTraceId).toEqual(serverHTTPServerRequestTxn.contexts?.trace?.trace_id);
160+
expect(clientPageloadTraceId).toEqual(serverRequestHTTPClientSpan?.trace_id);
161+
162+
// serverPageRequest has no parent (root span)
163+
expect(serverPageRequestTxn.contexts?.trace?.parent_span_id).toBeUndefined();
164+
165+
// clientPageload's parent and serverRequestHTTPClient's parent is serverPageRequest
166+
const serverPageRequestSpanId = serverPageRequestTxn.contexts?.trace?.span_id;
167+
expect(clientPageloadTxn.contexts?.trace?.parent_span_id).toEqual(serverPageRequestSpanId);
168+
expect(serverRequestHTTPClientSpan?.parent_span_id).toEqual(serverPageRequestSpanId);
169+
170+
// serverHTTPServerRequest's parent is serverRequestHTTPClient
171+
expect(serverHTTPServerRequestTxn.contexts?.trace?.parent_span_id).toEqual(serverRequestHTTPClientSpan?.span_id);
172+
});
173+
174+
test('sends parametrized pageload, server and API request transaction names', async ({ page }) => {
175+
const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => {
176+
return txnEvent?.transaction?.startsWith('/user-page/') ?? false;
177+
});
178+
179+
const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => {
180+
return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false;
181+
});
182+
183+
const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-4', txnEvent => {
184+
return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false;
185+
});
186+
187+
await page.goto('/user-page/myUsername123');
188+
189+
const clientPageloadTxn = await clientPageloadTxnPromise;
190+
const serverPageRequestTxn = await serverPageRequestTxnPromise;
191+
const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise;
192+
193+
const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find(
194+
span => span.op === 'http.client' && span.description?.includes('/api/user/'),
195+
);
196+
197+
// Client pageload transaction - actual URL with pageload operation
198+
expect(clientPageloadTxn).toMatchObject({
199+
transaction: '/user-page/myUsername123', // todo: parametrize
200+
transaction_info: { source: 'url' },
201+
contexts: {
202+
trace: {
203+
op: 'pageload',
204+
origin: 'auto.pageload.browser',
205+
data: {
206+
'sentry.op': 'pageload',
207+
'sentry.origin': 'auto.pageload.browser',
208+
'sentry.source': 'url',
209+
},
210+
},
211+
},
212+
});
213+
214+
// Server page request transaction - parametrized transaction name with actual URL in data
215+
expect(serverPageRequestTxn).toMatchObject({
216+
transaction: 'GET /user-page/[userId]',
217+
transaction_info: { source: 'route' },
218+
contexts: {
219+
trace: {
220+
op: 'http.server',
221+
origin: 'auto.http.astro',
222+
data: {
223+
'sentry.op': 'http.server',
224+
'sentry.origin': 'auto.http.astro',
225+
'sentry.source': 'route',
226+
url: expect.stringContaining('/user-page/myUsername123'),
227+
},
228+
},
229+
},
230+
request: { url: expect.stringContaining('/user-page/myUsername123') },
231+
});
232+
233+
// HTTP client span - actual API URL with client operation
234+
expect(serverRequestHTTPClientSpan).toMatchObject({
235+
op: 'http.client',
236+
origin: 'auto.http.otel.node_fetch',
237+
description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized
238+
data: {
239+
'sentry.op': 'http.client',
240+
'sentry.origin': 'auto.http.otel.node_fetch',
241+
'url.full': expect.stringContaining('/api/user/myUsername123.json'),
242+
'url.path': '/api/user/myUsername123.json',
243+
url: expect.stringContaining('/api/user/myUsername123.json'),
244+
},
245+
});
246+
247+
// Server HTTP request transaction - should be parametrized
248+
expect(serverHTTPServerRequestTxn).toMatchObject({
249+
transaction: 'GET /api/user/myUsername123.json', // todo: parametrize
250+
transaction_info: { source: 'route' },
251+
contexts: {
252+
trace: {
253+
op: 'http.server',
254+
origin: 'auto.http.astro',
255+
data: {
256+
'sentry.op': 'http.server',
257+
'sentry.origin': 'auto.http.astro',
258+
'sentry.source': 'route',
259+
url: expect.stringContaining('/api/user/myUsername123.json'),
260+
},
261+
},
262+
},
263+
request: { url: expect.stringContaining('/api/user/myUsername123.json') },
264+
});
265+
});
266+
267+
test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => {
268+
const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => {
269+
return txnEvent?.transaction?.startsWith('/catchAll/') ?? false;
270+
});
271+
272+
const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => {
273+
return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false;
274+
});
275+
276+
await page.goto('/catchAll/hell0/whatever-do');
277+
278+
const clientPageloadTxn = await clientPageloadTxnPromise;
279+
const serverPageRequestTxn = await serverPageRequestTxnPromise;
280+
281+
expect(clientPageloadTxn).toMatchObject({
282+
transaction: '/catchAll/hell0/whatever-do', // todo: parametrize
283+
transaction_info: { source: 'url' },
284+
contexts: {
285+
trace: {
286+
op: 'pageload',
287+
origin: 'auto.pageload.browser',
288+
data: {
289+
'sentry.op': 'pageload',
290+
'sentry.origin': 'auto.pageload.browser',
291+
'sentry.source': 'url',
292+
},
293+
},
294+
},
295+
});
296+
297+
expect(serverPageRequestTxn).toMatchObject({
298+
transaction: 'GET /catchAll/[path]',
299+
transaction_info: { source: 'route' },
300+
contexts: {
301+
trace: {
302+
op: 'http.server',
303+
origin: 'auto.http.astro',
304+
data: {
305+
'sentry.op': 'http.server',
306+
'sentry.origin': 'auto.http.astro',
307+
'sentry.source': 'route',
308+
url: expect.stringContaining('/catchAll/hell0/whatever-do'),
309+
},
310+
},
311+
},
312+
request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') },
313+
});
314+
});
315+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import Layout from '../../layouts/Layout.astro';
3+
4+
export const prerender = false;
5+
6+
const params = Astro.params;
7+
8+
---
9+
10+
<Layout title="CatchAll SSR page">
11+
<p>params: {params}</p>
12+
</Layout>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
import Layout from '../../layouts/Layout.astro';
3+
---
4+
5+
<Layout>
6+
<h1>User Settings</h1>
7+
</Layout>

dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
233233
expect(serverRequestHTTPClientSpan).toMatchObject({
234234
op: 'http.client',
235235
origin: 'auto.http.otel.node_fetch',
236-
description: 'GET http://localhost:3030/api/user/myUsername123.json', // todo: parametrize (this is just a span though - no transaction)
236+
description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized
237237
data: {
238238
'sentry.op': 'http.client',
239239
'sentry.origin': 'auto.http.otel.node_fetch',
@@ -243,9 +243,9 @@ test.describe('nested SSR routes (client, server, server request)', () => {
243243
},
244244
});
245245

246-
// Server HTTP request transaction - should be parametrized (todo: currently not parametrized)
246+
// Server HTTP request transaction
247247
expect(serverHTTPServerRequestTxn).toMatchObject({
248-
transaction: 'GET /api/user/myUsername123.json', // todo: should be parametrized to 'GET /api/user/[userId].json'
248+
transaction: 'GET /api/user/[userId].json',
249249
transaction_info: { source: 'route' },
250250
contexts: {
251251
trace: {
@@ -262,4 +262,105 @@ test.describe('nested SSR routes (client, server, server request)', () => {
262262
request: { url: expect.stringContaining('/api/user/myUsername123.json') },
263263
});
264264
});
265+
266+
test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => {
267+
const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => {
268+
return txnEvent?.transaction?.startsWith('/catchAll/') ?? false;
269+
});
270+
271+
const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => {
272+
return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false;
273+
});
274+
275+
await page.goto('/catchAll/hell0/whatever-do');
276+
277+
const clientPageloadTxn = await clientPageloadTxnPromise;
278+
const serverPageRequestTxn = await serverPageRequestTxnPromise;
279+
280+
expect(clientPageloadTxn).toMatchObject({
281+
transaction: '/catchAll/hell0/whatever-do', // todo: parametrize to '/catchAll/[...path]'
282+
transaction_info: { source: 'url' },
283+
contexts: {
284+
trace: {
285+
op: 'pageload',
286+
origin: 'auto.pageload.browser',
287+
data: {
288+
'sentry.op': 'pageload',
289+
'sentry.origin': 'auto.pageload.browser',
290+
'sentry.source': 'url',
291+
},
292+
},
293+
},
294+
});
295+
296+
expect(serverPageRequestTxn).toMatchObject({
297+
transaction: 'GET /catchAll/[...path]',
298+
transaction_info: { source: 'route' },
299+
contexts: {
300+
trace: {
301+
op: 'http.server',
302+
origin: 'auto.http.astro',
303+
data: {
304+
'sentry.op': 'http.server',
305+
'sentry.origin': 'auto.http.astro',
306+
'sentry.source': 'route',
307+
url: expect.stringContaining('/catchAll/hell0/whatever-do'),
308+
},
309+
},
310+
},
311+
request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') },
312+
});
313+
});
314+
});
315+
316+
// Case for `user-page/[id]` vs. `user-page/settings` static routes
317+
test.describe('parametrized vs static paths', () => {
318+
test('should use static route name for static route in parametrized path', async ({ page }) => {
319+
const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => {
320+
return txnEvent?.transaction?.startsWith('/user-page/') ?? false;
321+
});
322+
323+
const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => {
324+
return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false;
325+
});
326+
327+
await page.goto('/user-page/settings');
328+
329+
const clientPageloadTxn = await clientPageloadTxnPromise;
330+
const serverPageRequestTxn = await serverPageRequestTxnPromise;
331+
332+
expect(clientPageloadTxn).toMatchObject({
333+
transaction: '/user-page/settings',
334+
transaction_info: { source: 'url' },
335+
contexts: {
336+
trace: {
337+
op: 'pageload',
338+
origin: 'auto.pageload.browser',
339+
data: {
340+
'sentry.op': 'pageload',
341+
'sentry.origin': 'auto.pageload.browser',
342+
'sentry.source': 'url',
343+
},
344+
},
345+
},
346+
});
347+
348+
expect(serverPageRequestTxn).toMatchObject({
349+
transaction: 'GET /user-page/settings',
350+
transaction_info: { source: 'route' },
351+
contexts: {
352+
trace: {
353+
op: 'http.server',
354+
origin: 'auto.http.astro',
355+
data: {
356+
'sentry.op': 'http.server',
357+
'sentry.origin': 'auto.http.astro',
358+
'sentry.source': 'route',
359+
url: expect.stringContaining('/user-page/settings'),
360+
},
361+
},
362+
},
363+
request: { url: expect.stringContaining('/user-page/settings') },
364+
});
365+
});
265366
});

dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ test.describe('tracing in static routes with server islands', () => {
6363
]),
6464
);
6565

66-
expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island%2F'); // URL-encoded for 'GET /test-static/'
66+
expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island'); // URL-encoded for 'GET /server-island'
6767
expect(baggageMetaTagContent).toContain('sentry-sampled=true');
6868

6969
const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise;

0 commit comments

Comments
 (0)