Skip to content

feat(v9/astro): Parametrize routes on client-side #17143

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

Merged
merged 1 commit into from
Jul 24, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import Layout from '../../layouts/Layout.astro';
export const prerender = false;
---

<Layout title="User Settings">
<h1>User Settings</h1>
</Layout>
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
trace: {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
Expand All @@ -55,9 +55,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
transaction: '/test-ssr',
transaction_info: {
source: 'url',
},
transaction_info: { source: 'route' },
type: 'transaction',
});

Expand Down Expand Up @@ -113,9 +111,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
transaction: 'GET /test-ssr',
transaction_info: {
source: 'route',
},
transaction_info: { source: 'route' },
type: 'transaction',
});
});
Expand Down Expand Up @@ -194,18 +190,21 @@ test.describe('nested SSR routes (client, server, server request)', () => {
span => span.op === 'http.client' && span.description?.includes('/api/user/'),
);

const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
expect(routeNameMetaContent).toBe('%2Fuser-page%2F%5BuserId%5D');

// Client pageload transaction - actual URL with pageload operation
expect(clientPageloadTxn).toMatchObject({
transaction: '/user-page/myUsername123', // todo: parametrize
transaction_info: { source: 'url' },
transaction: '/user-page/[userId]',
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
},
},
},
Expand Down Expand Up @@ -275,20 +274,23 @@ test.describe('nested SSR routes (client, server, server request)', () => {

await page.goto('/catchAll/hell0/whatever-do');

const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
expect(routeNameMetaContent).toBe('%2FcatchAll%2F%5Bpath%5D');

const clientPageloadTxn = await clientPageloadTxnPromise;
const serverPageRequestTxn = await serverPageRequestTxnPromise;

expect(clientPageloadTxn).toMatchObject({
transaction: '/catchAll/hell0/whatever-do', // todo: parametrize
transaction_info: { source: 'url' },
transaction: '/catchAll/[path]',
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
},
},
},
Expand All @@ -313,3 +315,55 @@ test.describe('nested SSR routes (client, server, server request)', () => {
});
});
});

// Case for `user-page/[id]` vs. `user-page/settings` static routes
test.describe('parametrized vs static paths', () => {
test('should use static route name for static route in parametrized path', async ({ page }) => {
const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => {
return txnEvent?.transaction?.startsWith('/user-page/') ?? false;
});

const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => {
return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false;
});

await page.goto('/user-page/settings');

const clientPageloadTxn = await clientPageloadTxnPromise;
const serverPageRequestTxn = await serverPageRequestTxnPromise;

expect(clientPageloadTxn).toMatchObject({
transaction: '/user-page/settings',
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.astro',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
},
},
},
});

expect(serverPageRequestTxn).toMatchObject({
transaction: 'GET /user-page/settings',
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.astro',
data: {
'sentry.op': 'http.server',
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/user-page/settings'),
},
},
},
request: { url: expect.stringContaining('/user-page/settings') },
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ test.describe('tracing in static/pre-rendered routes', () => {
trace: {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
parent_span_id: metaParentSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
trace_id: metaTraceId,
Expand All @@ -48,12 +48,12 @@ test.describe('tracing in static/pre-rendered routes', () => {
platform: 'javascript',
transaction: '/test-static',
transaction_info: {
source: 'url',
source: 'route',
},
type: 'transaction',
});

expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static%2F'); // URL-encoded for 'GET /test-static/'
expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static'); // URL-encoded for 'GET /test-static'
expect(baggageMetaTagContent).toContain('sentry-sampled=true');

await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
trace: {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
Expand All @@ -56,7 +56,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
timestamp: expect.any(Number),
transaction: '/test-ssr',
transaction_info: {
source: 'url',
source: 'route',
},
type: 'transaction',
});
Expand Down Expand Up @@ -193,18 +193,21 @@ test.describe('nested SSR routes (client, server, server request)', () => {
span => span.op === 'http.client' && span.description?.includes('/api/user/'),
);

const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
expect(routeNameMetaContent).toBe('%2Fuser-page%2F%5BuserId%5D');

// Client pageload transaction - actual URL with pageload operation
expect(clientPageloadTxn).toMatchObject({
transaction: '/user-page/myUsername123', // todo: parametrize to '/user-page/[userId]'
transaction_info: { source: 'url' },
transaction: '/user-page/[userId]',
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
},
},
},
Expand Down Expand Up @@ -274,20 +277,23 @@ test.describe('nested SSR routes (client, server, server request)', () => {

await page.goto('/catchAll/hell0/whatever-do');

const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
expect(routeNameMetaContent).toBe('%2FcatchAll%2F%5B...path%5D');

const clientPageloadTxn = await clientPageloadTxnPromise;
const serverPageRequestTxn = await serverPageRequestTxnPromise;

expect(clientPageloadTxn).toMatchObject({
transaction: '/catchAll/hell0/whatever-do', // todo: parametrize to '/catchAll/[...path]'
transaction_info: { source: 'url' },
transaction: '/catchAll/[...path]',
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
},
},
},
Expand Down Expand Up @@ -331,15 +337,15 @@ test.describe('parametrized vs static paths', () => {

expect(clientPageloadTxn).toMatchObject({
transaction: '/user-page/settings',
transaction_info: { source: 'url' },
transaction_info: { source: 'route' },
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ test.describe('tracing in static routes with server islands', () => {
trace: {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
parent_span_id: metaParentSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
trace_id: metaTraceId,
Expand All @@ -45,7 +45,7 @@ test.describe('tracing in static routes with server islands', () => {
platform: 'javascript',
transaction: '/server-island',
transaction_info: {
source: 'url',
source: 'route',
},
type: 'transaction',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ test.describe('tracing in static/pre-rendered routes', () => {
trace: {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.origin': 'auto.pageload.astro',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
origin: 'auto.pageload.astro',
parent_span_id: metaParentSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
trace_id: metaTraceId,
Expand All @@ -48,7 +48,7 @@ test.describe('tracing in static/pre-rendered routes', () => {
platform: 'javascript',
transaction: '/test-static',
transaction_info: {
source: 'url',
source: 'route',
},
type: 'transaction',
});
Expand Down
52 changes: 52 additions & 0 deletions packages/astro/src/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { browserTracingIntegration as originalBrowserTracingIntegration, WINDOW } from '@sentry/browser';
import type { Integration, TransactionSource } from '@sentry/core';
import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';

/**
* Returns the value of a meta-tag
*/
function getMetaContent(metaName: string): string | undefined {
const optionalDocument = WINDOW.document as (typeof WINDOW)['document'] | undefined;
const metaTag = optionalDocument?.querySelector(`meta[name=${metaName}]`);
return metaTag?.getAttribute('content') || undefined;
}

/**
* A custom browser tracing integrations for Astro.
*/
export function browserTracingIntegration(
options: Parameters<typeof originalBrowserTracingIntegration>[0] = {},
): Integration {
const integration = originalBrowserTracingIntegration(options);

return {
...integration,
setup(client) {
// Original integration setup call
integration.setup?.(client);

client.on('afterStartPageLoadSpan', pageLoadSpan => {
const routeNameFromMetaTags = getMetaContent('sentry-route-name');

if (routeNameFromMetaTags) {
let decodedRouteName;
try {
decodedRouteName = decodeURIComponent(routeNameFromMetaTags);
} catch {
// We ignore errors here, e.g. if the value cannot be URL decoded.
return;
}

DEBUG_BUILD && debug.log(`[Tracing] Using route name from Sentry HTML meta-tag: ${decodedRouteName}`);

pageLoadSpan.updateName(decodedRouteName);
pageLoadSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' as TransactionSource,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.astro',
});
}
});
},
};
}
Loading
Loading