diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js new file mode 100644 index 000000000000..562bbf7b2cb4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js @@ -0,0 +1,10 @@ +export const prerender = false; + +export function GET({ params }) { + return new Response( + JSON.stringify({ + greeting: `Hello ${params.userId}`, + userId: params.userId, + }), + ); +} diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro new file mode 100644 index 000000000000..bb225c166241 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro @@ -0,0 +1,12 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const params = Astro.params; + +--- + + +

params: {params}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro new file mode 100644 index 000000000000..e35bd3a34d97 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro @@ -0,0 +1,17 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const { userId } = Astro.params; + +const response = await fetch(Astro.url.origin + `/api/user/${userId}.json`) +const data = await response.json(); + +--- + + +

{data.greeting}

+ +

data: {JSON.stringify(data)}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts index 644afc377545..2699e827718e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts @@ -120,3 +120,196 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { }); }); }); + +test.describe('nested SSR routes (client, server, server request)', () => { + /** The user-page route fetches from an endpoint and creates a deeply nested span structure: + * pageload — /user-page/myUsername123 + * ├── browser.** — multiple browser spans + * └── browser.request — /user-page/myUsername123 + * └── http.server — GET /user-page/[userId] (SSR page request) + * └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span) + * └── http.server — GET /api/user/myUsername123.json (server request) + */ + test('sends connected server and client pageload and request spans with the same trace id', 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; + }); + + const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; + }); + + await page.goto('/user-page/myUsername123'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; + const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( + span => span.op === 'http.client' && span.description?.includes('/api/user/'), + ); + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + + // Verify all spans have the same trace ID + expect(clientPageloadTraceId).toEqual(serverPageRequestTxn.contexts?.trace?.trace_id); + expect(clientPageloadTraceId).toEqual(serverHTTPServerRequestTxn.contexts?.trace?.trace_id); + expect(clientPageloadTraceId).toEqual(serverRequestHTTPClientSpan?.trace_id); + + // serverPageRequest has no parent (root span) + expect(serverPageRequestTxn.contexts?.trace?.parent_span_id).toBeUndefined(); + + // clientPageload's parent and serverRequestHTTPClient's parent is serverPageRequest + const serverPageRequestSpanId = serverPageRequestTxn.contexts?.trace?.span_id; + expect(clientPageloadTxn.contexts?.trace?.parent_span_id).toEqual(serverPageRequestSpanId); + expect(serverRequestHTTPClientSpan?.parent_span_id).toEqual(serverPageRequestSpanId); + + // serverHTTPServerRequest's parent is serverRequestHTTPClient + expect(serverHTTPServerRequestTxn.contexts?.trace?.parent_span_id).toEqual(serverRequestHTTPClientSpan?.span_id); + }); + + test('sends parametrized pageload, server and API request transaction names', 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; + }); + + const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; + }); + + await page.goto('/user-page/myUsername123'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; + + const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( + span => span.op === 'http.client' && span.description?.includes('/api/user/'), + ); + + // Client pageload transaction - actual URL with pageload operation + expect(clientPageloadTxn).toMatchObject({ + transaction: '/user-page/myUsername123', // todo: parametrize + transaction_info: { source: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + // Server page request transaction - parametrized transaction name with actual URL in data + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /user-page/[userId]', + 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/myUsername123'), + }, + }, + }, + request: { url: expect.stringContaining('/user-page/myUsername123') }, + }); + + // HTTP client span - actual API URL with client operation + expect(serverRequestHTTPClientSpan).toMatchObject({ + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized + data: { + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.node_fetch', + 'url.full': expect.stringContaining('/api/user/myUsername123.json'), + 'url.path': '/api/user/myUsername123.json', + url: expect.stringContaining('/api/user/myUsername123.json'), + }, + }); + + // Server HTTP request transaction - should be parametrized + expect(serverHTTPServerRequestTxn).toMatchObject({ + transaction: 'GET /api/user/myUsername123.json', // todo: parametrize + 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('/api/user/myUsername123.json'), + }, + }, + }, + request: { url: expect.stringContaining('/api/user/myUsername123.json') }, + }); + }); + + test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('/catchAll/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false; + }); + + await page.goto('/catchAll/hell0/whatever-do'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/catchAll/hell0/whatever-do', // todo: parametrize + transaction_info: { source: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /catchAll/[path]', + 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('/catchAll/hell0/whatever-do'), + }, + }, + }, + request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro new file mode 100644 index 000000000000..bb225c166241 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro @@ -0,0 +1,12 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const params = Astro.params; + +--- + + +

params: {params}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro new file mode 100644 index 000000000000..8260e632c07b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro @@ -0,0 +1,7 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + +

User Settings

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index 36f32cd5dc0e..8267adcb4ea9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -233,7 +233,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { expect(serverRequestHTTPClientSpan).toMatchObject({ op: 'http.client', origin: 'auto.http.otel.node_fetch', - description: 'GET http://localhost:3030/api/user/myUsername123.json', // todo: parametrize (this is just a span though - no transaction) + description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized data: { 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.otel.node_fetch', @@ -243,9 +243,9 @@ test.describe('nested SSR routes (client, server, server request)', () => { }, }); - // Server HTTP request transaction - should be parametrized (todo: currently not parametrized) + // Server HTTP request transaction expect(serverHTTPServerRequestTxn).toMatchObject({ - transaction: 'GET /api/user/myUsername123.json', // todo: should be parametrized to 'GET /api/user/[userId].json' + transaction: 'GET /api/user/[userId].json', transaction_info: { source: 'route' }, contexts: { trace: { @@ -262,4 +262,105 @@ test.describe('nested SSR routes (client, server, server request)', () => { request: { url: expect.stringContaining('/api/user/myUsername123.json') }, }); }); + + test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction?.startsWith('/catchAll/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false; + }); + + await page.goto('/catchAll/hell0/whatever-do'); + + 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' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /catchAll/[...path]', + 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('/catchAll/hell0/whatever-do'), + }, + }, + }, + request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') }, + }); + }); +}); + +// 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-5', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-5', 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: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + 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') }, + }); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts index fc396999d76e..dda88afe4714 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -63,7 +63,7 @@ test.describe('tracing in static routes with server islands', () => { ]), ); - expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island'); // URL-encoded for 'GET /server-island' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise; diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts index 9db35c72a47d..90b26fb645e9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -53,7 +53,7 @@ test.describe('tracing in static/pre-rendered routes', () => { 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 diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 28092cad84be..69564fcc6535 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -1,14 +1,18 @@ -import { consoleSandbox } from '@sentry/core'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { consoleSandbox, debug } from '@sentry/core'; import { sentryVitePlugin } from '@sentry/vite-plugin'; -import type { AstroConfig, AstroIntegration } from 'astro'; +import type { AstroConfig, AstroIntegration, RoutePart } from 'astro'; import * as fs from 'fs'; import * as path from 'path'; import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets'; -import type { SentryOptions } from './types'; +import type { IntegrationResolvedRoute, SentryOptions } from './types'; const PKG_NAME = '@sentry/astro'; export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { + let sentryServerInitPath: string | undefined; + let didSaveRouteData = false; + return { name: PKG_NAME, hooks: { @@ -134,6 +138,8 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { injectScript('page-ssr', buildServerSnippet(options || {})); } + sentryServerInitPath = pathToServerInit; + // Prevent Sentry from being externalized for SSR. // Cloudflare like environments have Node.js APIs are available under `node:` prefix. // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ @@ -165,6 +171,36 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { }); } }, + + // @ts-expect-error - This hook is available in Astro 5+ + 'astro:routes:resolved': ({ routes }: { routes: IntegrationResolvedRoute[] }) => { + if (!sentryServerInitPath || didSaveRouteData) { + return; + } + + try { + const serverInitContent = readFileSync(sentryServerInitPath, 'utf8'); + + const updatedServerInitContent = `${serverInitContent}\nglobalThis["__sentryRouteInfo"] = ${JSON.stringify( + routes.map(route => { + return { + ...route, + patternCaseSensitive: joinRouteSegments(route.segments), // Store parametrized routes with correct casing on `globalThis` to be able to use them on the server during runtime + patternRegex: route.patternRegex.source, // using `source` to be able to serialize the regex + }; + }), + null, + 2, + )};`; + + writeFileSync(sentryServerInitPath, updatedServerInitContent, 'utf8'); + + didSaveRouteData = true; // Prevents writing the file multiple times during runtime + debug.log('Successfully added route pattern information to Sentry config file:', sentryServerInitPath); + } catch (error) { + debug.warn(`Failed to write to Sentry config file at ${sentryServerInitPath}:`, error); + } + }, }, }; }; @@ -271,3 +307,18 @@ export function getUpdatedSourceMapSettings( return { previousUserSourceMapSetting, updatedSourceMapSetting }; } + +/** + * Join Astro route segments into a case-sensitive single path string. + * + * Astro lowercases the parametrized route. Joining segments manually is recommended to get the correct casing of the routes. + * Recommendation in comment: https://github.com/withastro/astro/issues/13885#issuecomment-2934203029 + * Function Reference: https://github.com/joanrieu/astro-typed-links/blob/b3dc12c6fe8d672a2bc2ae2ccc57c8071bbd09fa/package/src/integration.ts#L16 + */ +function joinRouteSegments(segments: RoutePart[][]): string { + const parthArray = segments.map(segment => + segment.map(routePart => (routePart.dynamic ? `[${routePart.content}]` : routePart.content)).join(''), + ); + + return `/${parthArray.join('/')}`; +} diff --git a/packages/astro/src/integration/types.ts b/packages/astro/src/integration/types.ts index 08a8635889fe..aed2b7e1d193 100644 --- a/packages/astro/src/integration/types.ts +++ b/packages/astro/src/integration/types.ts @@ -1,4 +1,5 @@ import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; +import type { RouteData } from 'astro'; type SdkInitPaths = { /** @@ -224,3 +225,27 @@ export type SentryOptions = SdkInitPaths & debug?: boolean; // eslint-disable-next-line deprecation/deprecation } & DeprecatedRuntimeOptions; + +/** + * Routes inside 'astro:routes:resolved' hook (Astro v5+) + * + * Inline type for official `IntegrationResolvedRoute`. + * The type includes more properties, but we only need some of them. + * + * @see https://github.com/withastro/astro/blob/04e60119afee668264a2ff6665c19a32150f4c91/packages/astro/src/types/public/integrations.ts#L287 + */ +export type IntegrationResolvedRoute = { + isPrerendered: RouteData['prerender']; + pattern: RouteData['route']; + patternRegex: RouteData['pattern']; + segments: RouteData['segments']; +}; + +/** + * Internal type for Astro routes, as we store an additional `patternCaseSensitive` property alongside the + * lowercased parametrized `pattern` of each Astro route. + */ +export type ResolvedRouteWithCasedPattern = IntegrationResolvedRoute & { + patternRegex: string; // RegEx gets stringified + patternCaseSensitive: string; +}; diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index fb2f2e572fa4..ec9b8cd2f82f 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -23,6 +23,7 @@ import { withIsolationScope, } from '@sentry/node'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; +import type { ResolvedRouteWithCasedPattern } from '../integration/types'; type MiddlewareOptions = { /** @@ -95,6 +96,9 @@ async function instrumentRequest( addNonEnumerableProperty(locals, '__sentry_wrapped__', true); } + const storedBuildTimeRoutes = (globalThis as unknown as { __sentryRouteInfo?: ResolvedRouteWithCasedPattern[] }) + ?.__sentryRouteInfo; + const isDynamicPageRequest = checkIsDynamicPageRequest(ctx); const request = ctx.request; @@ -128,8 +132,15 @@ async function instrumentRequest( } try { - const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); - const source = interpolatedRoute ? 'route' : 'url'; + // `routePattern` is available after Astro 5 + const contextWithRoutePattern = ctx as Parameters[0] & { routePattern?: string }; + const rawRoutePattern = contextWithRoutePattern.routePattern; + const foundRoute = storedBuildTimeRoutes?.find(route => route.pattern === rawRoutePattern); + + const parametrizedRoute = + foundRoute?.patternCaseSensitive || interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); + + const source = parametrizedRoute ? 'route' : 'url'; // storing res in a variable instead of directly returning is necessary to // invoke the catch block if next() throws @@ -148,12 +159,12 @@ async function instrumentRequest( attributes['http.fragment'] = ctx.url.hash; } - isolationScope?.setTransactionName(`${method} ${interpolatedRoute || ctx.url.pathname}`); + isolationScope?.setTransactionName(`${method} ${parametrizedRoute || ctx.url.pathname}`); const res = await startSpan( { attributes, - name: `${method} ${interpolatedRoute || ctx.url.pathname}`, + name: `${method} ${parametrizedRoute || ctx.url.pathname}`, op: 'http.server', }, async span => {