From 0b08be95bfed38f882a284259164f67512a9d579 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 6 Jul 2026 15:17:52 +0200 Subject: [PATCH 1/3] feat: Set url.template on pageload and navigation spans for framework SDKs Apply url.template to pageload and navigation root spans across Astro, Next.js, React, React Router, Remix, Solid, SvelteKit, and Vue integrations. Co-Authored-By: Cursor --- .../astro/src/client/browserTracingIntegration.ts | 1 + .../routing/appRouterRoutingInstrumentation.ts | 8 ++++++++ .../routing/pagesRouterRoutingInstrumentation.ts | 2 ++ .../src/client/createClientInstrumentation.ts | 1 + packages/react-router/src/client/hydratedRouter.ts | 2 ++ .../reactrouter-compat-utils/instrumentation.tsx | 13 +++++++++++++ packages/react/src/reactrouterv3.ts | 2 ++ packages/react/src/tanstackrouter.ts | 6 ++++++ packages/remix/src/client/performance.tsx | 5 +++++ packages/solid/src/solidrouter.ts | 1 + packages/solid/src/tanstackrouter.ts | 6 ++++++ .../src/client/browserTracingIntegration.ts | 2 ++ packages/vue/src/router.ts | 4 ++++ packages/vue/src/tanstackrouter.ts | 6 ++++++ 14 files changed, 59 insertions(+) diff --git a/packages/astro/src/client/browserTracingIntegration.ts b/packages/astro/src/client/browserTracingIntegration.ts index 5ffdb3fd8860..6bc4adaf6392 100644 --- a/packages/astro/src/client/browserTracingIntegration.ts +++ b/packages/astro/src/client/browserTracingIntegration.ts @@ -48,6 +48,7 @@ export function browserTracingIntegration( attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.astro', + ...(source === 'route' && { 'url.template': name }), }, }); } diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 3ecc2aec7a6b..8f43341e3cf9 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -54,6 +54,7 @@ export function appRouterInstrumentPageLoad(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', + ...(parameterizedPathname && { 'url.template': parameterizedPathname }), }, }); } @@ -116,6 +117,7 @@ export function appRouterInstrumentNavigation(client: Client): void { currentNavigationSpan.setAttributes({ 'navigation.type': `router.${navigationType}`, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', + ...(parameterizedPathname && { 'url.template': parameterizedPathname }), }); currentRouterPatchingNavigationSpanRef.current = undefined; } else { @@ -126,6 +128,7 @@ export function appRouterInstrumentNavigation(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': `router.${navigationType}`, + ...(parameterizedPathname && { 'url.template': parameterizedPathname }), }, }); } @@ -140,6 +143,9 @@ export function appRouterInstrumentNavigation(client: Client): void { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, parameterizedPathname ? 'route' : 'url', ); + if (parameterizedPathname) { + currentRouterPatchingNavigationSpanRef.current.setAttribute('url.template', parameterizedPathname); + } } else { currentRouterPatchingNavigationSpanRef.current = startBrowserTracingNavigationSpan(client, { name: parameterizedPathname ?? pathname, @@ -147,6 +153,7 @@ export function appRouterInstrumentNavigation(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': 'browser.popstate', + ...(parameterizedPathname && { 'url.template': parameterizedPathname }), }, }); } @@ -245,6 +252,7 @@ function patchRouter(client: Client, router: NextRouter, currentNavigationSpanRe attributes: { ...transactionAttributes, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', + ...(parameterizedPathname && { 'url.template': parameterizedPathname }), }, }); diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 0b9146b26f5b..498026eb3d63 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -129,6 +129,7 @@ export function pagesRouterInstrumentPageLoad(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url', + ...(route && { 'url.template': route }), ...(params && { ...params }), }, }, @@ -166,6 +167,7 @@ export function pagesRouterInstrumentNavigation(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, + ...(spanSource === 'route' && { 'url.template': newLocation }), }, }); }); diff --git a/packages/react-router/src/client/createClientInstrumentation.ts b/packages/react-router/src/client/createClientInstrumentation.ts index 0cd25d8a4219..cb96122d314d 100644 --- a/packages/react-router/src/client/createClientInstrumentation.ts +++ b/packages/react-router/src/client/createClientInstrumentation.ts @@ -359,6 +359,7 @@ function updateRootSpanRoute(routeName: string, hasPattern: boolean): void { updateSpanName(rootSpan, routeName); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + rootSpan.setAttribute('url.template', routeName); } /** diff --git a/packages/react-router/src/client/hydratedRouter.ts b/packages/react-router/src/client/hydratedRouter.ts index ca07a0e50885..572b35c757b6 100644 --- a/packages/react-router/src/client/hydratedRouter.ts +++ b/packages/react-router/src/client/hydratedRouter.ts @@ -50,6 +50,7 @@ export function instrumentHydratedRouter(): void { pageloadSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react_router', + 'url.template': parameterizePageloadRoute, }); } } @@ -99,6 +100,7 @@ export function instrumentHydratedRouter(): void { ) { rootSpan.updateName(parameterizedRoute); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + rootSpan.setAttribute('url.template', parameterizedRoute); } }); return true; diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index 5bc58325f963..936f22ad3016 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -398,6 +398,9 @@ export function updateNavigationSpan( if (isImprovement) { activeRootSpan.updateName(name); activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + if (source === 'route') { + activeRootSpan.setAttribute('url.template', name); + } // Only mark as finalized for non-wildcard route names (allows URL→route upgrades). if (!transactionNameHasWildcard(name) && source === 'route') { @@ -997,6 +1000,9 @@ export function handleNavigation(opts: { // Update existing real span from wildcard to parameterized route name trackedNav.span.updateName(name); trackedNav.span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source as 'route' | 'url' | 'custom'); + if (source === 'route') { + trackedNav.span.setAttribute('url.template', name); + } addNonEnumerableProperty( trackedNav.span as { __sentry_navigation_name_set__?: boolean }, '__sentry_navigation_name_set__', @@ -1032,6 +1038,7 @@ export function handleNavigation(opts: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter${version ? `_v${version}` : ''}`, + ...(source === 'route' && { 'url.template': placeholderEntry.routeName }), }, }); } catch (e) { @@ -1120,6 +1127,9 @@ function updatePageloadTransaction({ if (activeRootSpan) { activeRootSpan.updateName(name); activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + if (source === 'route') { + activeRootSpan.setAttribute('url.template', name); + } // Patch span.end() to ensure we update the name one last time before the span is sent patchSpanEnd(activeRootSpan, location, routes, basename, 'pageload'); @@ -1216,6 +1226,9 @@ function tryUpdateSpanNameBeforeEnd( if (isImprovement && spanNotEnded) { span.updateName(name); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + if (source === 'route') { + span.setAttribute('url.template', name); + } } } catch (error) { DEBUG_BUILD && debug.warn(`Error updating span details before ending: ${error}`); diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index cdf97c02d03b..86f7c2980bf7 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -68,6 +68,7 @@ export function reactRouterV3BrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(source === 'route' && { 'url.template': localName }), }, }); }, @@ -88,6 +89,7 @@ export function reactRouterV3BrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(source === 'route' && { 'url.template': localName }), }, }); }, diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 4473cb252512..12a7f8d94a01 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -64,6 +64,7 @@ export function tanstackRouterBrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url', + ...(routeMatch && { 'url.template': routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), }, }); @@ -79,6 +80,7 @@ export function tanstackRouterBrowserTracingIntegration( if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) { pageloadSpan.updateName(resolvedMatch.routeId); pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + pageloadSpan.setAttribute('url.template', resolvedMatch.routeId); pageloadSpan.setAttributes(routeMatchToParamSpanAttributes(resolvedMatch)); } }); @@ -98,6 +100,9 @@ export function tanstackRouterBrowserTracingIntegration( ): void => { span.updateName(match ? match.routeId : fallbackName); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url'); + if (match) { + span.setAttribute('url.template', match.routeId); + } span.setAttributes(routeMatchToParamSpanAttributes(match)); }; @@ -128,6 +133,7 @@ export function tanstackRouterBrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.tanstack_router', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url', + ...(routeMatch && { 'url.template': routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), }, }); diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index b3cde64d72de..1cedebc0fdc9 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -103,6 +103,7 @@ export function startPageloadSpan(client: Client): void { attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.remix', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(source === 'route' && { 'url.template': spanName }), }, }; @@ -126,6 +127,7 @@ function startNavigationSpan(matches: RouteMatch[], location: ReturnType attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(source === 'route' && { 'url.template': name }), }, }; @@ -182,6 +184,9 @@ export function withSentry

, R extends React.Co if (transaction) { transaction.updateName(name); transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + if (source === 'route') { + transaction.setAttribute('url.template', name); + } } } } diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index 0424183aea18..874d6820db99 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -105,6 +105,7 @@ function withSentryRouterRoot(Root: Component): Component( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url', + ...(routeMatch && { 'url.template': routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), }, }); @@ -75,6 +76,7 @@ export function tanstackRouterBrowserTracingIntegration( if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) { pageloadSpan.updateName(resolvedMatch.routeId); pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + pageloadSpan.setAttribute('url.template', resolvedMatch.routeId); pageloadSpan.setAttributes(routeMatchToParamSpanAttributes(resolvedMatch)); } }); @@ -94,6 +96,9 @@ export function tanstackRouterBrowserTracingIntegration( ): void => { span.updateName(match ? match.routeId : fallbackName); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url'); + if (match) { + span.setAttribute('url.template', match.routeId); + } span.setAttributes(routeMatchToParamSpanAttributes(match)); }; @@ -124,6 +129,7 @@ export function tanstackRouterBrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url', + ...(routeMatch && { 'url.template': routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), }, }); diff --git a/packages/sveltekit/src/client/browserTracingIntegration.ts b/packages/sveltekit/src/client/browserTracingIntegration.ts index 22539045cfb2..4ffc4038597c 100644 --- a/packages/sveltekit/src/client/browserTracingIntegration.ts +++ b/packages/sveltekit/src/client/browserTracingIntegration.ts @@ -67,6 +67,7 @@ function _instrumentPageload(client: Client): void { if (routeId) { pageloadSpan.updateName(routeId); pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + pageloadSpan.setAttribute('url.template', routeId); getCurrentScope().setTransactionName(routeId); } }); @@ -133,6 +134,7 @@ function _instrumentNavigations(client: Client): void { attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedRouteDestination ? 'route' : 'url', + ...(parameterizedRouteDestination && { 'url.template': parameterizedRouteDestination }), ...navigationInfo, }, }); diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 8d4100e9b6c8..4e6c91cf8aa7 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -94,6 +94,10 @@ export function instrumentVueRouter( transactionSource = 'route'; } + if (transactionSource === 'route') { + attributes['url.template'] = spanName; + } + getCurrentScope().setTransactionName(spanName); // Update the existing page load span with parametrized route information diff --git a/packages/vue/src/tanstackrouter.ts b/packages/vue/src/tanstackrouter.ts index 6b2700db0bb2..de86832a3812 100644 --- a/packages/vue/src/tanstackrouter.ts +++ b/packages/vue/src/tanstackrouter.ts @@ -66,6 +66,7 @@ export function tanstackRouterBrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url', + ...(routeMatch && { 'url.template': routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), }, }); @@ -83,6 +84,7 @@ export function tanstackRouterBrowserTracingIntegration( if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) { pageloadSpan.updateName(resolvedMatch.routeId); pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + pageloadSpan.setAttribute('url.template', resolvedMatch.routeId); pageloadSpan.setAttributes(routeMatchToParamSpanAttributes(resolvedMatch)); } }); @@ -102,6 +104,9 @@ export function tanstackRouterBrowserTracingIntegration( ): void => { span.updateName(match ? match.routeId : fallbackName); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url'); + if (match) { + span.setAttribute('url.template', match.routeId); + } span.setAttributes(routeMatchToParamSpanAttributes(match)); }; @@ -129,6 +134,7 @@ export function tanstackRouterBrowserTracingIntegration( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url', + ...(routeMatch && { 'url.template': routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), }, }); From 66cf987d5cf2aed53b069c5010f3211b547c5c07 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 6 Jul 2026 15:46:02 +0200 Subject: [PATCH 2/3] astro: test adjustments and add conventions --- .../astro-4/tests/tracing.dynamic.test.ts | 3 +++ .../astro-4/tests/tracing.static.test.ts | 3 +++ .../astro-5/tests/tracing.dynamic.test.ts | 12 ++++++++++++ .../astro-5/tests/tracing.serverIslands.test.ts | 3 +++ .../astro-5/tests/tracing.static.test.ts | 3 +++ .../astro-6/tests/tracing.dynamic.test.ts | 12 ++++++++++++ .../astro-6/tests/tracing.serverIslands.test.ts | 3 +++ .../astro-6/tests/tracing.static.test.ts | 3 +++ packages/astro/package.json | 1 + .../astro/src/client/browserTracingIntegration.ts | 3 ++- 10 files changed, 45 insertions(+), 1 deletion(-) 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 4d3d93fdf81e..e9d7c87eb5ac 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 @@ -32,6 +32,9 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/test-ssr', + 'url.path': '/test-ssr', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-ssr$/), }), op: 'pageload', origin: 'auto.pageload.astro', diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts index 7af2115dc29e..f1cfcc78789c 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts @@ -37,6 +37,9 @@ test.describe('tracing in static/pre-rendered routes', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/test-static', + 'url.path': '/test-static', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-static$/), }), op: 'pageload', origin: 'auto.pageload.astro', 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 0c69d1f59698..9a6ab99afbec 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 @@ -32,6 +32,9 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/test-ssr', + 'url.path': '/test-ssr', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-ssr$/), }), op: 'pageload', origin: 'auto.pageload.astro', @@ -213,6 +216,9 @@ test.describe('nested SSR routes (client, server, server request)', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/user-page/[userId]', + 'url.path': '/user-page/myUsername123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/myUsername123$/), }, }, }, @@ -309,6 +315,9 @@ test.describe('nested SSR routes (client, server, server request)', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/catchAll/[...path]', + 'url.path': '/catchAll/hell0/whatever-do', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/catchAll\/hell0\/whatever-do$/), }, }, }, @@ -366,6 +375,9 @@ test.describe('parametrized vs static paths', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/user-page/settings', + 'url.path': '/user-page/settings', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/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 354655a4ac7e..1416a706f971 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 @@ -33,6 +33,9 @@ test.describe('tracing in static routes with server islands', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/server-island', + 'url.path': '/server-island', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/server-island$/), }), op: 'pageload', origin: 'auto.pageload.astro', 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 4563dc2f306d..8297dd2b08a5 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 @@ -37,6 +37,9 @@ test.describe('tracing in static/pre-rendered routes', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/test-static', + 'url.path': '/test-static', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-static$/), }), op: 'pageload', origin: 'auto.pageload.astro', diff --git a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts index a89d1efd9f3a..83f6db0fae4f 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts @@ -32,6 +32,9 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/test-ssr', + 'url.path': '/test-ssr', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-ssr$/), }), op: 'pageload', origin: 'auto.pageload.astro', @@ -213,6 +216,9 @@ test.describe('nested SSR routes (client, server, server request)', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/user-page/[userId]', + 'url.path': '/user-page/myUsername123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/myUsername123$/), }, }, }, @@ -309,6 +315,9 @@ test.describe('nested SSR routes (client, server, server request)', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/catchAll/[...path]', + 'url.path': '/catchAll/hell0/whatever-do', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/catchAll\/hell0\/whatever-do$/), }, }, }, @@ -366,6 +375,9 @@ test.describe('parametrized vs static paths', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/user-page/settings', + 'url.path': '/user-page/settings', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/settings$/), }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.serverIslands.test.ts index 829b44a035f3..ce30defcd1fa 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.serverIslands.test.ts @@ -33,6 +33,9 @@ test.describe('tracing in static routes with server islands', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/server-island', + 'url.path': '/server-island', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/server-island$/), }), op: 'pageload', origin: 'auto.pageload.astro', diff --git a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.static.test.ts index c76a66101775..23a54221de50 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.static.test.ts @@ -37,6 +37,9 @@ test.describe('tracing in static/pre-rendered routes', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.astro', 'sentry.source': 'route', + 'url.template': '/test-static', + 'url.path': '/test-static', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-static$/), }), op: 'pageload', origin: 'auto.pageload.astro', diff --git a/packages/astro/package.json b/packages/astro/package.json index db4235823629..da23a6eaea8a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -58,6 +58,7 @@ "dependencies": { "@sentry/browser": "10.63.0", "@sentry/core": "10.63.0", + "@sentry/conventions": "^0.15.1", "@sentry/node": "10.63.0", "@sentry/vite-plugin": "^5.3.0" }, diff --git a/packages/astro/src/client/browserTracingIntegration.ts b/packages/astro/src/client/browserTracingIntegration.ts index 6bc4adaf6392..219c76902e41 100644 --- a/packages/astro/src/client/browserTracingIntegration.ts +++ b/packages/astro/src/client/browserTracingIntegration.ts @@ -11,6 +11,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; +import { URL_TEMPLATE } from '@sentry/conventions/attributes'; /** * Returns the value of a meta-tag @@ -48,7 +49,7 @@ export function browserTracingIntegration( attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.astro', - ...(source === 'route' && { 'url.template': name }), + ...(source === 'route' && { [URL_TEMPLATE]: name }), }, }); } From cb9d2ee216222ee4493fee4affe4451f0dcd28f6 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 6 Jul 2026 18:34:21 +0200 Subject: [PATCH 3/3] nextjs: Fix navigation timing, adjust e2e tests --- .../tests/client/app-dir-pageloads.test.ts | 3 ++ .../tests/client/pages-dir-navigation.test.ts | 3 ++ .../tests/client/pages-dir-pageload.test.ts | 3 ++ .../tests/parameterized-routes.test.ts | 12 +++++ .../routing-basepath-transaction.test.ts | 13 ++++- .../tests/parameterized-routes.test.ts | 12 +++++ .../tests/parameterized-routes.test.ts | 12 +++++ .../nextjs-app-dir/tests/transactions.test.ts | 3 ++ .../tests/transactions.test.ts | 6 +++ .../appRouterRoutingInstrumentation.ts | 49 +++++++++++-------- .../pagesRouterRoutingInstrumentation.ts | 31 ++++++++---- 11 files changed, 117 insertions(+), 30 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts index c656ade91202..cc2c6491dd37 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts @@ -27,6 +27,9 @@ test('should create a pageload transaction when the `app` directory is used', as 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'url', + 'url.path': '/pageload-transaction', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/pageload-transaction$/), + // no url.template because source is url and we didn't paremeterize the route }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts index 1969429f3103..2198d11da56c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts @@ -34,6 +34,9 @@ test('should report a navigation transaction for pages router navigations', asyn 'sentry.op': 'navigation', 'sentry.origin': 'auto.navigation.nextjs.pages_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/foo\/navigation-target-page$/), + 'url.path': '/foo/navigation-target-page', + 'url.template': '/[param]/navigation-target-page', }, op: 'navigation', origin: 'auto.navigation.nextjs.pages_router_instrumentation', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts index 313fe49a9a83..13d13094beac 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts @@ -27,6 +27,9 @@ test('should create a pageload transaction when the `pages` directory is used', 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/foo\/pages-pageload$/), + 'url.path': '/foo/pages-pageload', + 'url.template': '/[param]/pages-pageload', }, op: 'pageload', origin: 'auto.pageload.nextjs.pages_router_instrumentation', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/parameterized-routes.test.ts index 55ac655dfc5a..a2db3a58735b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/parameterized-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/parameterized-routes.test.ts @@ -20,6 +20,9 @@ test('should create a parameterized transaction when the `app` directory is used 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino$/), + 'url.path': '/parameterized/cappuccino', + 'url.template': '/parameterized/:one', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -61,6 +64,9 @@ test('should create a static transaction when the `app` directory is used and th 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'url', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/static$/), + 'url.path': '/parameterized/static', + // no url.template because source is url and we didn't paremeterize the route }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -100,6 +106,9 @@ test('should create a partially parameterized transaction when the `app` directo 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino\/beep$/), + 'url.path': '/parameterized/cappuccino/beep', + 'url.template': '/parameterized/:one/beep', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -140,6 +149,9 @@ test('should create a nested parameterized transaction when the `app` directory 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino\/beep\/espresso$/), + 'url.path': '/parameterized/cappuccino/beep/espresso', + 'url.template': '/parameterized/:one/beep/:two', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/routing-basepath-transaction.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/routing-basepath-transaction.test.ts index 12fe814d2fff..67d823b0fb82 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/routing-basepath-transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/routing-basepath-transaction.test.ts @@ -54,7 +54,18 @@ test('Creates a navigation transaction for basePath router with prefix', async ( await page.waitForTimeout(1000); await page.getByText('router.push()').click(); - expect(await navigationTransactionPromise).toBeDefined(); + const navigationTransaction = await navigationTransactionPromise; + expect(navigationTransaction).toBeDefined(); + + const attributes = navigationTransaction.contexts?.trace?.data; + expect(attributes).toMatchObject({ + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.nextjs.app_router_instrumentation', + 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/my-app\/navigation\/router-push$/), + 'url.path': '/my-app/navigation/router-push', + 'url.template': '/my-app/navigation/:param/router-push', + }); }); test('Creates a navigation transaction for basePath with prefix', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/parameterized-routes.test.ts index ab2086622ca3..e6a98c998a5c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/parameterized-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/parameterized-routes.test.ts @@ -20,6 +20,9 @@ test('should create a parameterized transaction when the `app` directory is used 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino$/), + 'url.path': '/parameterized/cappuccino', + 'url.template': '/parameterized/:one', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -61,6 +64,9 @@ test('should create a static transaction when the `app` directory is used and th 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'url', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/static$/), + 'url.path': '/parameterized/static', + // no url.template because source is url and we didn't paremeterize the route }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -100,6 +106,9 @@ test('should create a partially parameterized transaction when the `app` directo 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino\/beep$/), + 'url.path': '/parameterized/cappuccino/beep', + 'url.template': '/parameterized/:one/beep', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -140,6 +149,9 @@ test('should create a nested parameterized transaction when the `app` directory 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino\/beep\/espresso$/), + 'url.path': '/parameterized/cappuccino/beep/espresso', + 'url.template': '/parameterized/:one/beep/:two', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/parameterized-routes.test.ts index 43a2aa6191de..17f1a5636cdc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/parameterized-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/parameterized-routes.test.ts @@ -20,6 +20,9 @@ test('should create a parameterized transaction when the `app` directory is used 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino$/), + 'url.path': '/parameterized/cappuccino', + 'url.template': '/parameterized/:one', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -61,6 +64,9 @@ test('should create a static transaction when the `app` directory is used and th 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'url', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/static$/), + 'url.path': '/parameterized/static', + // no url.template because source is url and we didn't paremeterize the route }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -100,6 +106,9 @@ test('should create a partially parameterized transaction when the `app` directo 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino\/beep$/), + 'url.path': '/parameterized/cappuccino/beep', + 'url.template': '/parameterized/:one/beep', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -140,6 +149,9 @@ test('should create a nested parameterized transaction when the `app` directory 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/cappuccino\/beep\/espresso$/), + 'url.path': '/parameterized/cappuccino/beep/espresso', + 'url.template': '/parameterized/:one/beep/:two', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index eabaaaf7a919..8fe315a51ec9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -38,6 +38,9 @@ test('Sends a pageload transaction', async ({ page }) => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', 'sentry.source': 'url', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + 'url.path': '/', + // no url.template because the route isn't parameterized (sentry.source: 'url') }), }, }), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts index 8305cb447ca0..34c0afbb9110 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts @@ -38,6 +38,9 @@ test('Sends a pageload transaction', async ({ page }) => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + 'url.path': '/', + 'url.template': '/', }), }, }), @@ -86,6 +89,9 @@ test('Sends a navigation transaction', async ({ page }) => { 'sentry.origin': 'auto.navigation.nextjs.pages_router_instrumentation', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + 'url.path': '/user/5', + 'url.template': '/user/[id]', }), links: [ { diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 8f43341e3cf9..2ad2608f6901 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -8,6 +8,7 @@ import { } from '@sentry/core'; import { startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, WINDOW } from '@sentry/react'; import { maybeParameterizeRoute } from './parameterization'; +import { URL_TEMPLATE } from '@sentry/conventions/attributes'; /** * Strips trailing slash from a pathname, unless it's the root path. @@ -54,7 +55,7 @@ export function appRouterInstrumentPageLoad(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', - ...(parameterizedPathname && { 'url.template': parameterizedPathname }), + ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), }, }); } @@ -117,20 +118,24 @@ export function appRouterInstrumentNavigation(client: Client): void { currentNavigationSpan.setAttributes({ 'navigation.type': `router.${navigationType}`, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', - ...(parameterizedPathname && { 'url.template': parameterizedPathname }), + ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), }); currentRouterPatchingNavigationSpanRef.current = undefined; } else { - startBrowserTracingNavigationSpan(client, { - name: pathname, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', - 'navigation.type': `router.${navigationType}`, - ...(parameterizedPathname && { 'url.template': parameterizedPathname }), + startBrowserTracingNavigationSpan( + client, + { + name: pathname, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', + 'navigation.type': `router.${navigationType}`, + ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), + }, }, - }); + { url: normalizedHref }, + ); } }; @@ -144,7 +149,7 @@ export function appRouterInstrumentNavigation(client: Client): void { parameterizedPathname ? 'route' : 'url', ); if (parameterizedPathname) { - currentRouterPatchingNavigationSpanRef.current.setAttribute('url.template', parameterizedPathname); + currentRouterPatchingNavigationSpanRef.current.setAttribute(URL_TEMPLATE, parameterizedPathname); } } else { currentRouterPatchingNavigationSpanRef.current = startBrowserTracingNavigationSpan(client, { @@ -153,7 +158,7 @@ export function appRouterInstrumentNavigation(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': 'browser.popstate', - ...(parameterizedPathname && { 'url.template': parameterizedPathname }), + ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), }, }); } @@ -247,14 +252,18 @@ function patchRouter(client: Client, router: NextRouter, currentNavigationSpanRe const parameterizedPathname = maybeParameterizeRoute(transactionName); - currentNavigationSpanRef.current = startBrowserTracingNavigationSpan(client, { - name: parameterizedPathname ?? transactionName, - attributes: { - ...transactionAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', - ...(parameterizedPathname && { 'url.template': parameterizedPathname }), + currentNavigationSpanRef.current = startBrowserTracingNavigationSpan( + client, + { + name: parameterizedPathname ?? transactionName, + attributes: { + ...transactionAttributes, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url', + ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), + }, }, - }); + { url: normalizedHref }, + ); return target.apply(thisArg, argArray); }, diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 498026eb3d63..4fba55949df5 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -13,6 +13,7 @@ import type { NEXT_DATA } from 'next/dist/shared/lib/utils'; import RouterImport from 'next/router'; import type { ParsedUrlQuery } from 'querystring'; import { DEBUG_BUILD } from '../../common/debug-build'; +import { URL_TEMPLATE } from '@sentry/conventions/attributes'; // next/router v10 is CJS // @@ -129,7 +130,7 @@ export function pagesRouterInstrumentPageLoad(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url', - ...(route && { 'url.template': route }), + ...(route && { [URL_TEMPLATE]: route }), ...(params && { ...params }), }, }, @@ -161,15 +162,27 @@ export function pagesRouterInstrumentNavigation(client: Client): void { spanSource = 'url'; } - startBrowserTracingNavigationSpan(client, { - name: newLocation, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, - ...(spanSource === 'route' && { 'url.template': newLocation }), + // construct the full URL from navigationTarget that is the relative URL + let fullUrl = navigationTarget; + try { + fullUrl = new URL(navigationTarget, globalObject.location.origin).href; + } catch { + // keep relative URL as fallback + } + + startBrowserTracingNavigationSpan( + client, + { + name: newLocation, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, + ...(spanSource === 'route' && { [URL_TEMPLATE]: newLocation }), + }, }, - }); + { url: fullUrl }, + ); }); }