Skip to content

Commit ea536c9

Browse files
committed
feat(astro): Parametrize routes on client-side
1 parent 726c868 commit ea536c9

File tree

11 files changed

+125
-55
lines changed

11 files changed

+125
-55
lines changed

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
3131
data: expect.objectContaining({
3232
'sentry.op': 'pageload',
3333
'sentry.origin': 'auto.pageload.browser',
34-
'sentry.source': 'url',
34+
'sentry.source': 'route',
3535
}),
3636
op: 'pageload',
3737
origin: 'auto.pageload.browser',
@@ -55,9 +55,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
5555
start_timestamp: expect.any(Number),
5656
timestamp: expect.any(Number),
5757
transaction: '/test-ssr',
58-
transaction_info: {
59-
source: 'url',
60-
},
58+
transaction_info: { source: 'route' },
6159
type: 'transaction',
6260
});
6361

@@ -113,9 +111,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
113111
start_timestamp: expect.any(Number),
114112
timestamp: expect.any(Number),
115113
transaction: 'GET /test-ssr',
116-
transaction_info: {
117-
source: 'route',
118-
},
114+
transaction_info: { source: 'route' },
119115
type: 'transaction',
120116
});
121117
});
@@ -194,18 +190,21 @@ test.describe('nested SSR routes (client, server, server request)', () => {
194190
span => span.op === 'http.client' && span.description?.includes('/api/user/'),
195191
);
196192

193+
const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
194+
expect(routeNameMetaContent).toBe('/user-page/[userId]');
195+
197196
// Client pageload transaction - actual URL with pageload operation
198197
expect(clientPageloadTxn).toMatchObject({
199-
transaction: '/user-page/myUsername123', // todo: parametrize
200-
transaction_info: { source: 'url' },
198+
transaction: '/user-page/[userId]',
199+
transaction_info: { source: 'route' },
201200
contexts: {
202201
trace: {
203202
op: 'pageload',
204203
origin: 'auto.pageload.browser',
205204
data: {
206205
'sentry.op': 'pageload',
207206
'sentry.origin': 'auto.pageload.browser',
208-
'sentry.source': 'url',
207+
'sentry.source': 'route',
209208
},
210209
},
211210
},
@@ -275,20 +274,23 @@ test.describe('nested SSR routes (client, server, server request)', () => {
275274

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

277+
const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
278+
expect(routeNameMetaContent).toBe('/catchAll/[path]');
279+
278280
const clientPageloadTxn = await clientPageloadTxnPromise;
279281
const serverPageRequestTxn = await serverPageRequestTxnPromise;
280282

281283
expect(clientPageloadTxn).toMatchObject({
282-
transaction: '/catchAll/hell0/whatever-do', // todo: parametrize
283-
transaction_info: { source: 'url' },
284+
transaction: '/catchAll/[path]',
285+
transaction_info: { source: 'route' },
284286
contexts: {
285287
trace: {
286288
op: 'pageload',
287289
origin: 'auto.pageload.browser',
288290
data: {
289291
'sentry.op': 'pageload',
290292
'sentry.origin': 'auto.pageload.browser',
291-
'sentry.source': 'url',
293+
'sentry.source': 'route',
292294
},
293295
},
294296
},

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test.describe('tracing in static/pre-rendered routes', () => {
3636
data: expect.objectContaining({
3737
'sentry.op': 'pageload',
3838
'sentry.origin': 'auto.pageload.browser',
39-
'sentry.source': 'url',
39+
'sentry.source': 'route',
4040
}),
4141
op: 'pageload',
4242
origin: 'auto.pageload.browser',
@@ -48,12 +48,12 @@ test.describe('tracing in static/pre-rendered routes', () => {
4848
platform: 'javascript',
4949
transaction: '/test-static',
5050
transaction_info: {
51-
source: 'url',
51+
source: 'route',
5252
},
5353
type: 'transaction',
5454
});
5555

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

5959
await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
3131
data: expect.objectContaining({
3232
'sentry.op': 'pageload',
3333
'sentry.origin': 'auto.pageload.browser',
34-
'sentry.source': 'url',
34+
'sentry.source': 'route',
3535
}),
3636
op: 'pageload',
3737
origin: 'auto.pageload.browser',
@@ -56,7 +56,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
5656
timestamp: expect.any(Number),
5757
transaction: '/test-ssr',
5858
transaction_info: {
59-
source: 'url',
59+
source: 'route',
6060
},
6161
type: 'transaction',
6262
});
@@ -193,18 +193,21 @@ test.describe('nested SSR routes (client, server, server request)', () => {
193193
span => span.op === 'http.client' && span.description?.includes('/api/user/'),
194194
);
195195

196+
const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
197+
expect(routeNameMetaContent).toBe('/user-page/[userId]');
198+
196199
// Client pageload transaction - actual URL with pageload operation
197200
expect(clientPageloadTxn).toMatchObject({
198-
transaction: '/user-page/myUsername123', // todo: parametrize to '/user-page/[userId]'
199-
transaction_info: { source: 'url' },
201+
transaction: '/user-page/[userId]',
202+
transaction_info: { source: 'route' },
200203
contexts: {
201204
trace: {
202205
op: 'pageload',
203206
origin: 'auto.pageload.browser',
204207
data: {
205208
'sentry.op': 'pageload',
206209
'sentry.origin': 'auto.pageload.browser',
207-
'sentry.source': 'url',
210+
'sentry.source': 'route',
208211
},
209212
},
210213
},
@@ -274,20 +277,23 @@ test.describe('nested SSR routes (client, server, server request)', () => {
274277

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

280+
const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content');
281+
expect(routeNameMetaContent).toBe('/catchAll/[...path]');
282+
277283
const clientPageloadTxn = await clientPageloadTxnPromise;
278284
const serverPageRequestTxn = await serverPageRequestTxnPromise;
279285

280286
expect(clientPageloadTxn).toMatchObject({
281-
transaction: '/catchAll/hell0/whatever-do', // todo: parametrize to '/catchAll/[...path]'
282-
transaction_info: { source: 'url' },
287+
transaction: '/catchAll/[...path]',
288+
transaction_info: { source: 'route' },
283289
contexts: {
284290
trace: {
285291
op: 'pageload',
286292
origin: 'auto.pageload.browser',
287293
data: {
288294
'sentry.op': 'pageload',
289295
'sentry.origin': 'auto.pageload.browser',
290-
'sentry.source': 'url',
296+
'sentry.source': 'route',
291297
},
292298
},
293299
},
@@ -331,15 +337,15 @@ test.describe('parametrized vs static paths', () => {
331337

332338
expect(clientPageloadTxn).toMatchObject({
333339
transaction: '/user-page/settings',
334-
transaction_info: { source: 'url' },
340+
transaction_info: { source: 'route' },
335341
contexts: {
336342
trace: {
337343
op: 'pageload',
338344
origin: 'auto.pageload.browser',
339345
data: {
340346
'sentry.op': 'pageload',
341347
'sentry.origin': 'auto.pageload.browser',
342-
'sentry.source': 'url',
348+
'sentry.source': 'route',
343349
},
344350
},
345351
},

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ test.describe('tracing in static routes with server islands', () => {
3333
data: expect.objectContaining({
3434
'sentry.op': 'pageload',
3535
'sentry.origin': 'auto.pageload.browser',
36-
'sentry.source': 'url',
36+
'sentry.source': 'route',
3737
}),
3838
op: 'pageload',
3939
origin: 'auto.pageload.browser',
@@ -45,7 +45,7 @@ test.describe('tracing in static routes with server islands', () => {
4545
platform: 'javascript',
4646
transaction: '/server-island',
4747
transaction_info: {
48-
source: 'url',
48+
source: 'route',
4949
},
5050
type: 'transaction',
5151
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test.describe('tracing in static/pre-rendered routes', () => {
3636
data: expect.objectContaining({
3737
'sentry.op': 'pageload',
3838
'sentry.origin': 'auto.pageload.browser',
39-
'sentry.source': 'url',
39+
'sentry.source': 'route',
4040
}),
4141
op: 'pageload',
4242
origin: 'auto.pageload.browser',
@@ -48,7 +48,7 @@ test.describe('tracing in static/pre-rendered routes', () => {
4848
platform: 'javascript',
4949
transaction: '/test-static',
5050
transaction_info: {
51-
source: 'url',
51+
source: 'route',
5252
},
5353
type: 'transaction',
5454
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { browserTracingIntegration as originalBrowserTracingIntegration, WINDOW } from '@sentry/browser';
2+
import type { Integration, TransactionSource } from '@sentry/core';
3+
import { debug, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
4+
import { DEBUG_BUILD } from '../debug-build';
5+
6+
/**
7+
* Returns the value of a meta-tag
8+
*/
9+
function getMetaContent(metaName: string): string | undefined {
10+
const optionalDocument = WINDOW.document as (typeof WINDOW)['document'] | undefined;
11+
const metaTag = optionalDocument?.querySelector(`meta[name=${metaName}]`);
12+
return metaTag?.getAttribute('content') || undefined;
13+
}
14+
15+
/**
16+
* A custom browser tracing integrations for Astro.
17+
*/
18+
export function browserTracingIntegration(
19+
options: Parameters<typeof originalBrowserTracingIntegration>[0] = {},
20+
): Integration {
21+
const integration = originalBrowserTracingIntegration(options);
22+
23+
return {
24+
...integration,
25+
setup(client) {
26+
// Original integration setup call
27+
integration.setup?.(client);
28+
29+
client.on('afterStartPageLoadSpan', pageLoadSpan => {
30+
const routeNameFromMetaTags = getMetaContent('sentry-route-name');
31+
32+
if (routeNameFromMetaTags) {
33+
DEBUG_BUILD && debug.log(`[Tracing] Using route name from Sentry HTML meta-tag: ${routeNameFromMetaTags}`);
34+
35+
pageLoadSpan.updateName(routeNameFromMetaTags);
36+
pageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route' as TransactionSource);
37+
}
38+
});
39+
},
40+
};
41+
}

packages/astro/src/client/sdk.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import type { BrowserOptions } from '@sentry/browser';
2-
import {
3-
browserTracingIntegration,
4-
getDefaultIntegrations as getBrowserDefaultIntegrations,
5-
init as initBrowserSdk,
6-
} from '@sentry/browser';
2+
import { getDefaultIntegrations as getBrowserDefaultIntegrations, init as initBrowserSdk } from '@sentry/browser';
73
import type { Client, Integration } from '@sentry/core';
84
import { applySdkMetadata } from '@sentry/core';
5+
import { browserTracingIntegration } from './browserTracingIntegration';
96

107
// Tree-shakable guard to remove all code related to tracing
118
declare const __SENTRY_TRACING__: boolean;

packages/astro/src/debug-build.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare const __DEBUG_BUILD__: boolean;
2+
3+
/**
4+
* 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.
5+
*
6+
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
7+
*/
8+
export const DEBUG_BUILD = __DEBUG_BUILD__;

packages/astro/src/index.client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
export * from '@sentry/browser';
22

3+
// Override the browserTracingIntegration with the custom Astro version
4+
export { browserTracingIntegration } from './client/browserTracingIntegration';
5+
36
export { init } from './client/sdk';

packages/astro/src/server/middleware.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ async function instrumentRequest(
213213
try {
214214
for await (const chunk of bodyReporter()) {
215215
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
216-
const modifiedHtml = addMetaTagToHead(html);
216+
const modifiedHtml = addMetaTagToHead(html, parametrizedRoute);
217217
controller.enqueue(new TextEncoder().encode(modifiedHtml));
218218
}
219219
} catch (e) {
@@ -253,11 +253,13 @@ async function instrumentRequest(
253253
* This function optimistically assumes that the HTML coming in chunks will not be split
254254
* within the <head> tag. If this still happens, we simply won't replace anything.
255255
*/
256-
function addMetaTagToHead(htmlChunk: string): string {
256+
function addMetaTagToHead(htmlChunk: string, parametrizedRoute?: string): string {
257257
if (typeof htmlChunk !== 'string') {
258258
return htmlChunk;
259259
}
260-
const metaTags = getTraceMetaTags();
260+
const metaTags = parametrizedRoute
261+
? `${getTraceMetaTags()}\n<meta name="sentry-route-name" content="${parametrizedRoute}"/>\n`
262+
: getTraceMetaTags();
261263

262264
if (!metaTags) {
263265
return htmlChunk;
@@ -317,26 +319,30 @@ export function interpolateRouteFromUrlAndParams(
317319
return acc.replace(key, `[${valuesToMultiSegmentParams[key]}]`);
318320
}, decodedUrlPathname);
319321

320-
return urlWithReplacedMultiSegmentParams
321-
.split('/')
322-
.map(segment => {
323-
if (!segment) {
324-
return '';
325-
}
322+
return (
323+
urlWithReplacedMultiSegmentParams
324+
.split('/')
325+
.map(segment => {
326+
if (!segment) {
327+
return '';
328+
}
326329

327-
if (valuesToParams[segment]) {
328-
return replaceWithParamName(segment);
329-
}
330+
if (valuesToParams[segment]) {
331+
return replaceWithParamName(segment);
332+
}
330333

331-
// astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/
332-
const segmentParts = segment.split('-');
333-
if (segmentParts.length > 1) {
334-
return segmentParts.map(part => replaceWithParamName(part)).join('-');
335-
}
334+
// astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/
335+
const segmentParts = segment.split('-');
336+
if (segmentParts.length > 1) {
337+
return segmentParts.map(part => replaceWithParamName(part)).join('-');
338+
}
336339

337-
return segment;
338-
})
339-
.join('/');
340+
return segment;
341+
})
342+
.join('/')
343+
// Remove trailing slash (only if it's not the only segment)
344+
.replace(/(.+)\/$/, '$1')
345+
);
340346
}
341347

342348
function tryDecodeUrl(url: string): string | undefined {

0 commit comments

Comments
 (0)