Skip to content

feat(astro): Parametrize routes on client-side #17133

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 7 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Expand Up @@ -31,7 +31,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
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('/user-page/[userId]');

// 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',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'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('/catchAll/[path]');

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',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test.describe('tracing in static/pre-rendered routes', () => {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
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 @@ -31,7 +31,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
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('/user-page/[userId]');

// 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',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'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('/catchAll/[...path]');

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',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'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',
data: {
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test.describe('tracing in static routes with server islands', () => {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
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 @@ -36,7 +36,7 @@ test.describe('tracing in static/pre-rendered routes', () => {
data: expect.objectContaining({
'sentry.op': 'pageload',
'sentry.origin': 'auto.pageload.browser',
'sentry.source': 'url',
'sentry.source': 'route',
}),
op: 'pageload',
origin: 'auto.pageload.browser',
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
41 changes: 41 additions & 0 deletions packages/astro/src/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { browserTracingIntegration as originalBrowserTracingIntegration, WINDOW } from '@sentry/browser';
import type { Integration, TransactionSource } from '@sentry/core';
import { debug, 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) {
DEBUG_BUILD && debug.log(`[Tracing] Using route name from Sentry HTML meta-tag: ${routeNameFromMetaTags}`);

pageLoadSpan.updateName(routeNameFromMetaTags);
pageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route' as TransactionSource);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also update the sentry.origin here to reflect that this is coming from the custom browser instrumentation.

}
});
},
};
}
7 changes: 2 additions & 5 deletions packages/astro/src/client/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { BrowserOptions } from '@sentry/browser';
import {
browserTracingIntegration,
getDefaultIntegrations as getBrowserDefaultIntegrations,
init as initBrowserSdk,
} from '@sentry/browser';
import { getDefaultIntegrations as getBrowserDefaultIntegrations, init as initBrowserSdk } from '@sentry/browser';
import type { Client, Integration } from '@sentry/core';
import { applySdkMetadata } from '@sentry/core';
import { browserTracingIntegration } from './browserTracingIntegration';

// Tree-shakable guard to remove all code related to tracing
declare const __SENTRY_TRACING__: boolean;
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/debug-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare const __DEBUG_BUILD__: boolean;

/**
* This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
*
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
*/
export const DEBUG_BUILD = __DEBUG_BUILD__;
3 changes: 3 additions & 0 deletions packages/astro/src/index.client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from '@sentry/browser';

// Override the browserTracingIntegration with the custom Astro version
export { browserTracingIntegration } from './client/browserTracingIntegration';

export { init } from './client/sdk';
46 changes: 26 additions & 20 deletions packages/astro/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@
try {
for await (const chunk of bodyReporter()) {
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
const modifiedHtml = addMetaTagToHead(html);
const modifiedHtml = addMetaTagToHead(html, parametrizedRoute);
controller.enqueue(new TextEncoder().encode(modifiedHtml));
}
} catch (e) {
Expand Down Expand Up @@ -253,11 +253,13 @@
* This function optimistically assumes that the HTML coming in chunks will not be split
* within the <head> tag. If this still happens, we simply won't replace anything.
*/
function addMetaTagToHead(htmlChunk: string): string {
function addMetaTagToHead(htmlChunk: string, parametrizedRoute?: string): string {
if (typeof htmlChunk !== 'string') {
return htmlChunk;
}
const metaTags = getTraceMetaTags();
const metaTags = parametrizedRoute
? `${getTraceMetaTags()}\n<meta name="sentry-route-name" content="${parametrizedRoute}"/>\n`
: getTraceMetaTags();

if (!metaTags) {
return htmlChunk;
Expand Down Expand Up @@ -317,26 +319,30 @@
return acc.replace(key, `[${valuesToMultiSegmentParams[key]}]`);
}, decodedUrlPathname);

return urlWithReplacedMultiSegmentParams
.split('/')
.map(segment => {
if (!segment) {
return '';
}
return (
urlWithReplacedMultiSegmentParams
.split('/')
.map(segment => {
if (!segment) {
return '';
}

if (valuesToParams[segment]) {
return replaceWithParamName(segment);
}
if (valuesToParams[segment]) {
return replaceWithParamName(segment);
}

// astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/
const segmentParts = segment.split('-');
if (segmentParts.length > 1) {
return segmentParts.map(part => replaceWithParamName(part)).join('-');
}
// astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/
const segmentParts = segment.split('-');
if (segmentParts.length > 1) {
return segmentParts.map(part => replaceWithParamName(part)).join('-');
}

return segment;
})
.join('/');
return segment;
})
.join('/')
// Remove trailing slash (only if it's not the only segment)
.replace(/(.+)\/$/, '$1')
);
}

function tryDecodeUrl(url: string): string | undefined {
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/test/server/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,4 +482,11 @@ describe('interpolateRouteFromUrlAndParams', () => {
const expectedRoute = '/usernames/user';
expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute);
});

it('removes trailing slashes from the route', () => {
const rawUrl = '/users/123/';
const params = { id: '123' };
const expectedRoute = '/users/[id]';
expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute);
});
});
Loading