Skip to content

Commit c1471bb

Browse files
Lms24cursoragent
andcommitted
feat: Set url.template on pageload and navigation spans across framework SDKs
Applies url.template to pageload and navigation root spans across React, Remix, Solid, SvelteKit, and Vue integrations, and refactors Angular's routing instrumentation to use the shared getAbsoluteUrl helper introduced in #22006. part of #21921 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 58bf332 commit c1471bb

10 files changed

Lines changed: 51 additions & 30 deletions

File tree

packages/angular/src/tracing.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import {
1919
spanToJSON,
2020
startBrowserTracingNavigationSpan,
2121
startInactiveSpan,
22-
WINDOW,
22+
getAbsoluteUrl,
2323
} from '@sentry/browser';
2424
import type { Integration, Span } from '@sentry/core';
25-
import { debug, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/core';
25+
import { debug, parseStringToURLObject, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/core';
2626
import type { Observable } from 'rxjs';
2727
import { Subscription } from 'rxjs';
2828
import { filter, tap } from 'rxjs/operators';
@@ -66,26 +66,13 @@ export function _updateSpanAttributesForParametrizedUrl(route: string, url: stri
6666
if (!attributes || attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') {
6767
span.updateName(route);
6868

69-
// Angular router gives us relative paths (e.g. `/users/123`). Resolve against the
70-
// current origin so that `url.full` contains the absolute URL including protocol and host.
71-
const locationOrigin = WINDOW.location?.origin;
72-
let urlFull = url;
73-
let urlPath = url;
74-
if (locationOrigin) {
75-
try {
76-
const parsed = new URL(url, locationOrigin);
77-
urlFull = parsed.href;
78-
urlPath = parsed.pathname;
79-
} catch {
80-
// fall back to the raw string
81-
}
82-
}
69+
const absoluteUrl = getAbsoluteUrl(url);
8370

8471
span.setAttributes({
8572
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.${op}.angular`,
8673
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
87-
[URL_FULL]: urlFull,
88-
[URL_PATH]: urlPath,
74+
[URL_FULL]: absoluteUrl,
75+
[URL_PATH]: parseStringToURLObject(absoluteUrl)?.pathname,
8976
[URL_TEMPLATE]: route,
9077
});
9178
}
@@ -118,17 +105,6 @@ export class TraceService implements OnDestroy {
118105
// see comment in `_isPageloadOngoing` for rationale
119106
if (!this._isPageloadOngoing()) {
120107
runOutsideAngular(() => {
121-
// Angular router gives us a relative path; resolve it against the current origin
122-
// so the browser tracing integration can set url.full correctly from the start.
123-
const locationOrigin = WINDOW.location?.origin;
124-
let absoluteUrl: string = navigationEvent.url;
125-
if (locationOrigin) {
126-
try {
127-
absoluteUrl = new URL(navigationEvent.url, locationOrigin).href;
128-
} catch {
129-
// fall back to relative path
130-
}
131-
}
132108
startBrowserTracingNavigationSpan(
133109
client,
134110
{
@@ -139,7 +115,7 @@ export class TraceService implements OnDestroy {
139115
},
140116
},
141117
{
142-
url: absoluteUrl,
118+
url: getAbsoluteUrl(navigationEvent.url),
143119
},
144120
);
145121
});

packages/react/src/reactrouter-compat-utils/instrumentation.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,9 @@ export function updateNavigationSpan(
398398
if (isImprovement) {
399399
activeRootSpan.updateName(name);
400400
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
401+
if (source === 'route') {
402+
activeRootSpan.setAttribute('url.template', name);
403+
}
401404

402405
// Only mark as finalized for non-wildcard route names (allows URL→route upgrades).
403406
if (!transactionNameHasWildcard(name) && source === 'route') {
@@ -997,6 +1000,9 @@ export function handleNavigation(opts: {
9971000
// Update existing real span from wildcard to parameterized route name
9981001
trackedNav.span.updateName(name);
9991002
trackedNav.span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source as 'route' | 'url' | 'custom');
1003+
if (source === 'route') {
1004+
trackedNav.span.setAttribute('url.template', name);
1005+
}
10001006
addNonEnumerableProperty(
10011007
trackedNav.span as { __sentry_navigation_name_set__?: boolean },
10021008
'__sentry_navigation_name_set__',
@@ -1032,6 +1038,7 @@ export function handleNavigation(opts: {
10321038
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
10331039
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
10341040
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter${version ? `_v${version}` : ''}`,
1041+
...(source === 'route' && { 'url.template': placeholderEntry.routeName }),
10351042
},
10361043
});
10371044
} catch (e) {
@@ -1120,6 +1127,9 @@ function updatePageloadTransaction({
11201127
if (activeRootSpan) {
11211128
activeRootSpan.updateName(name);
11221129
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
1130+
if (source === 'route') {
1131+
activeRootSpan.setAttribute('url.template', name);
1132+
}
11231133

11241134
// Patch span.end() to ensure we update the name one last time before the span is sent
11251135
patchSpanEnd(activeRootSpan, location, routes, basename, 'pageload');
@@ -1216,6 +1226,9 @@ function tryUpdateSpanNameBeforeEnd(
12161226
if (isImprovement && spanNotEnded) {
12171227
span.updateName(name);
12181228
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
1229+
if (source === 'route') {
1230+
span.setAttribute('url.template', name);
1231+
}
12191232
}
12201233
} catch (error) {
12211234
DEBUG_BUILD && debug.warn(`Error updating span details before ending: ${error}`);

packages/react/src/reactrouterv3.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function reactRouterV3BrowserTracingIntegration(
6868
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
6969
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3',
7070
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
71+
...(source === 'route' && { 'url.template': localName }),
7172
},
7273
});
7374
},
@@ -88,6 +89,7 @@ export function reactRouterV3BrowserTracingIntegration(
8889
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
8990
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3',
9091
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
92+
...(source === 'route' && { 'url.template': localName }),
9193
},
9294
});
9395
},

packages/react/src/tanstackrouter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function tanstackRouterBrowserTracingIntegration(
6464
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
6565
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router',
6666
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
67+
...(routeMatch && { 'url.template': routeMatch.routeId }),
6768
...routeMatchToParamSpanAttributes(routeMatch),
6869
},
6970
});
@@ -79,6 +80,7 @@ export function tanstackRouterBrowserTracingIntegration(
7980
if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) {
8081
pageloadSpan.updateName(resolvedMatch.routeId);
8182
pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
83+
pageloadSpan.setAttribute('url.template', resolvedMatch.routeId);
8284
pageloadSpan.setAttributes(routeMatchToParamSpanAttributes(resolvedMatch));
8385
}
8486
});
@@ -98,6 +100,9 @@ export function tanstackRouterBrowserTracingIntegration(
98100
): void => {
99101
span.updateName(match ? match.routeId : fallbackName);
100102
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url');
103+
if (match) {
104+
span.setAttribute('url.template', match.routeId);
105+
}
101106
span.setAttributes(routeMatchToParamSpanAttributes(match));
102107
};
103108

@@ -128,6 +133,7 @@ export function tanstackRouterBrowserTracingIntegration(
128133
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
129134
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.tanstack_router',
130135
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
136+
...(routeMatch && { 'url.template': routeMatch.routeId }),
131137
...routeMatchToParamSpanAttributes(routeMatch),
132138
},
133139
});

packages/remix/src/client/performance.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export function startPageloadSpan(client: Client): void {
103103
attributes: {
104104
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.remix',
105105
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
106+
...(source === 'route' && { 'url.template': spanName }),
106107
},
107108
};
108109

@@ -126,6 +127,7 @@ function startNavigationSpan(matches: RouteMatch<string>[], location: ReturnType
126127
attributes: {
127128
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix',
128129
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
130+
...(source === 'route' && { 'url.template': name }),
129131
},
130132
};
131133

@@ -182,6 +184,9 @@ export function withSentry<P extends Record<string, unknown>, R extends React.Co
182184
if (transaction) {
183185
transaction.updateName(name);
184186
transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
187+
if (source === 'route') {
188+
transaction.setAttribute('url.template', name);
189+
}
185190
}
186191
}
187192
}

packages/solid/src/solidrouter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ function withSentryRouterRoot(Root: Component<RouteSectionProps>): Component<Rou
105105
const parametrizedRoute = lastMatch.route.pattern || name;
106106
rootSpan.updateName(parametrizedRoute);
107107
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
108+
rootSpan.setAttribute('url.template', parametrizedRoute);
108109

109110
const params = lastMatch.params;
110111
for (const [key, value] of Object.entries(params)) {

packages/solid/src/tanstackrouter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
6060
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
6161
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router',
6262
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
63+
...(routeMatch && { 'url.template': routeMatch.routeId }),
6364
...routeMatchToParamSpanAttributes(routeMatch),
6465
},
6566
});
@@ -75,6 +76,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
7576
if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) {
7677
pageloadSpan.updateName(resolvedMatch.routeId);
7778
pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
79+
pageloadSpan.setAttribute('url.template', resolvedMatch.routeId);
7880
pageloadSpan.setAttributes(routeMatchToParamSpanAttributes(resolvedMatch));
7981
}
8082
});
@@ -94,6 +96,9 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
9496
): void => {
9597
span.updateName(match ? match.routeId : fallbackName);
9698
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url');
99+
if (match) {
100+
span.setAttribute('url.template', match.routeId);
101+
}
97102
span.setAttributes(routeMatchToParamSpanAttributes(match));
98103
};
99104

@@ -124,6 +129,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
124129
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
125130
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router',
126131
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
132+
...(routeMatch && { 'url.template': routeMatch.routeId }),
127133
...routeMatchToParamSpanAttributes(routeMatch),
128134
},
129135
});

packages/sveltekit/src/client/browserTracingIntegration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function _instrumentPageload(client: Client): void {
6767
if (routeId) {
6868
pageloadSpan.updateName(routeId);
6969
pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
70+
pageloadSpan.setAttribute('url.template', routeId);
7071
getCurrentScope().setTransactionName(routeId);
7172
}
7273
});
@@ -133,6 +134,7 @@ function _instrumentNavigations(client: Client): void {
133134
attributes: {
134135
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit',
135136
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedRouteDestination ? 'route' : 'url',
137+
...(parameterizedRouteDestination && { 'url.template': parameterizedRouteDestination }),
136138
...navigationInfo,
137139
},
138140
});

packages/vue/src/router.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export function instrumentVueRouter(
9494
transactionSource = 'route';
9595
}
9696

97+
if (transactionSource === 'route') {
98+
attributes['url.template'] = spanName;
99+
}
100+
97101
getCurrentScope().setTransactionName(spanName);
98102

99103
// Update the existing page load span with parametrized route information

packages/vue/src/tanstackrouter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
6666
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
6767
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router',
6868
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
69+
...(routeMatch && { 'url.template': routeMatch.routeId }),
6970
...routeMatchToParamSpanAttributes(routeMatch),
7071
},
7172
});
@@ -83,6 +84,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
8384
if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) {
8485
pageloadSpan.updateName(resolvedMatch.routeId);
8586
pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
87+
pageloadSpan.setAttribute('url.template', resolvedMatch.routeId);
8688
pageloadSpan.setAttributes(routeMatchToParamSpanAttributes(resolvedMatch));
8789
}
8890
});
@@ -102,6 +104,9 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
102104
): void => {
103105
span.updateName(match ? match.routeId : fallbackName);
104106
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url');
107+
if (match) {
108+
span.setAttribute('url.template', match.routeId);
109+
}
105110
span.setAttributes(routeMatchToParamSpanAttributes(match));
106111
};
107112

@@ -129,6 +134,7 @@ export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
129134
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
130135
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router',
131136
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
137+
...(routeMatch && { 'url.template': routeMatch.routeId }),
132138
...routeMatchToParamSpanAttributes(routeMatch),
133139
},
134140
});

0 commit comments

Comments
 (0)