Skip to content

Commit afa64b0

Browse files
Lms24cursoragent
andcommitted
feat(ember): Set url.template, url.path and url.full on router spans
Emit standard URL attributes on Ember pageload and navigation root spans, updating navigation attributes at routeDidChange once the destination URL is finalized. Co-Authored-By: Cursor <cursoragent@cursor.com>
1 parent ffde4cb commit afa64b0

4 files changed

Lines changed: 167 additions & 3 deletions

File tree

dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
1515
trace: {
1616
op: 'pageload',
1717
origin: 'auto.pageload.ember',
18+
data: {
19+
'sentry.origin': 'auto.pageload.ember',
20+
'sentry.source': 'route',
21+
'url.template': '/',
22+
'url.path': '/',
23+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
24+
},
1825
},
1926
},
2027
transaction: 'route:index',
@@ -43,6 +50,13 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
4350
trace: {
4451
op: 'navigation',
4552
origin: 'auto.navigation.ember',
53+
data: {
54+
'sentry.origin': 'auto.navigation.ember',
55+
'sentry.source': 'route',
56+
'url.template': '/tracing',
57+
'url.path': '/tracing',
58+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/),
59+
},
4660
},
4761
},
4862
transaction: 'route:tracing',
@@ -75,6 +89,13 @@ test('sends a navigation transaction even if the pageload span is still active',
7589
trace: {
7690
op: 'pageload',
7791
origin: 'auto.pageload.ember',
92+
data: {
93+
'sentry.origin': 'auto.pageload.ember',
94+
'sentry.source': 'route',
95+
'url.template': '/',
96+
'url.path': '/',
97+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
98+
},
7899
},
79100
},
80101
transaction: 'route:index',
@@ -88,6 +109,13 @@ test('sends a navigation transaction even if the pageload span is still active',
88109
trace: {
89110
op: 'navigation',
90111
origin: 'auto.navigation.ember',
112+
data: {
113+
'sentry.origin': 'auto.navigation.ember',
114+
'sentry.source': 'route',
115+
'url.template': '/tracing',
116+
'url.path': '/tracing',
117+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/),
118+
},
91119
},
92120
},
93121
transaction: 'route:tracing',
@@ -124,6 +152,13 @@ test('captures correct spans for navigation', async ({ page }) => {
124152
trace: {
125153
op: 'navigation',
126154
origin: 'auto.navigation.ember',
155+
data: {
156+
'sentry.origin': 'auto.navigation.ember',
157+
'sentry.source': 'route',
158+
'url.template': '/slow-loading-route',
159+
'url.path': '/slow-loading-route',
160+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/slow-loading-route$/),
161+
},
127162
},
128163
},
129164
transaction: 'route:slow-loading-route.index',

dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
1515
trace: {
1616
op: 'pageload',
1717
origin: 'auto.pageload.ember',
18+
data: {
19+
'sentry.origin': 'auto.pageload.ember',
20+
'sentry.source': 'route',
21+
'url.template': '/',
22+
'url.path': '/',
23+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
24+
},
1825
},
1926
},
2027
transaction: 'route:index',
@@ -43,6 +50,13 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
4350
trace: {
4451
op: 'navigation',
4552
origin: 'auto.navigation.ember',
53+
data: {
54+
'sentry.origin': 'auto.navigation.ember',
55+
'sentry.source': 'route',
56+
'url.template': '/tracing',
57+
'url.path': '/tracing',
58+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/),
59+
},
4660
},
4761
},
4862
transaction: 'route:tracing',
@@ -75,6 +89,13 @@ test('sends a navigation transaction even if the pageload span is still active',
7589
trace: {
7690
op: 'pageload',
7791
origin: 'auto.pageload.ember',
92+
data: {
93+
'sentry.origin': 'auto.pageload.ember',
94+
'sentry.source': 'route',
95+
'url.template': '/',
96+
'url.path': '/',
97+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
98+
},
7899
},
79100
},
80101
transaction: 'route:index',
@@ -88,6 +109,13 @@ test('sends a navigation transaction even if the pageload span is still active',
88109
trace: {
89110
op: 'navigation',
90111
origin: 'auto.navigation.ember',
112+
data: {
113+
'sentry.origin': 'auto.navigation.ember',
114+
'sentry.source': 'route',
115+
'url.template': '/tracing',
116+
'url.path': '/tracing',
117+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/),
118+
},
91119
},
92120
},
93121
transaction: 'route:tracing',
@@ -124,6 +152,13 @@ test('captures correct spans for navigation', async ({ page }) => {
124152
trace: {
125153
op: 'navigation',
126154
origin: 'auto.navigation.ember',
155+
data: {
156+
'sentry.origin': 'auto.navigation.ember',
157+
'sentry.source': 'route',
158+
'url.template': '/slow-loading-route',
159+
'url.path': '/slow-loading-route',
160+
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/slow-loading-route$/),
161+
},
127162
},
128163
},
129164
transaction: 'route:slow-loading-route.index',

packages/ember/addon/utils/instrumentEmberAppInstanceForPerformance.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@ import type {
55
startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType,
66
startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType,
77
} from '@sentry/browser';
8-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startInactiveSpan } from '@sentry/browser';
8+
import {
9+
getAbsoluteUrl,
10+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
11+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
12+
startInactiveSpan,
13+
} from '@sentry/browser';
914
import type { Client, Span } from '@sentry/core';
1015
import type { EmberRouterMain } from '../types';
1116
import { getBackburner } from './performance';
1217

18+
const URL_FULL = 'url.full';
19+
const URL_PATH = 'url.path';
20+
const URL_TEMPLATE = 'url.template';
21+
22+
type TransitionWithIntent = Transition & { intent?: { url?: string } };
23+
1324
export function instrumentEmberAppInstanceForPerformance(
1425
client: Client,
1526
appInstance: ApplicationInstance,
@@ -22,6 +33,7 @@ export function instrumentEmberAppInstanceForPerformance(
2233
let routerService = appInstance.lookup('service:router') as RouterService & {
2334
externalRouter?: RouterService;
2435
_hasMountedSentryPerformanceRouting?: boolean;
36+
currentURL?: string;
2537
};
2638

2739
if (routerService.externalRouter) {
@@ -60,6 +72,49 @@ function getTransitionInformation(
6072
};
6173
}
6274

75+
function getUrlPathFromEmberLocation(url: string): string {
76+
if (!url) {
77+
return '/';
78+
}
79+
80+
const withoutQuery = url.split('?')[0] ?? url;
81+
82+
if (withoutQuery.includes('#')) {
83+
const hashPart = withoutQuery.substring(withoutQuery.indexOf('#') + 1);
84+
return hashPart.startsWith('/') ? hashPart : `/${hashPart}`;
85+
}
86+
87+
return withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`;
88+
}
89+
90+
function buildUrlTemplate(path: string, params: Record<string, unknown> = {}): string {
91+
let template = path;
92+
93+
const paramEntries = Object.entries(params)
94+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0)
95+
.sort(([, a], [, b]) => b.length - a.length);
96+
97+
for (const [key, value] of paramEntries) {
98+
template = template.replace(`/${value}`, `/:${key}`);
99+
}
100+
101+
return template;
102+
}
103+
104+
// Only exported for testing
105+
export function _getRouteUrlAttributes(
106+
url: string,
107+
params: Record<string, unknown> = {},
108+
): Record<string, string> {
109+
const path = getUrlPathFromEmberLocation(url);
110+
111+
return {
112+
[URL_PATH]: path,
113+
[URL_FULL]: getAbsoluteUrl(path),
114+
[URL_TEMPLATE]: buildUrlTemplate(path, params),
115+
};
116+
}
117+
63118
// Only exported for testing
64119
export function _getLocationURL(location: EmberRouterMain['location']): string {
65120
if (!location?.getURL || !location?.formatURL) {
@@ -76,7 +131,7 @@ export function _getLocationURL(location: EmberRouterMain['location']): string {
76131

77132
function _instrumentEmberRouter(
78133
client: Client,
79-
routerService: RouterService,
134+
routerService: RouterService & { currentURL?: string },
80135
routerMain: EmberRouterMain,
81136
config: { disableRunloopPerformance?: boolean; instrumentPageLoad?: boolean; instrumentNavigation?: boolean },
82137
startBrowserTracingPageLoadSpan: typeof startBrowserTracingPageLoadSpanType,
@@ -96,6 +151,7 @@ function _instrumentEmberRouter(
96151
attributes: {
97152
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
98153
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember',
154+
..._getRouteUrlAttributes(url, routeInfo.params),
99155
url,
100156
toRoute: routeInfo.name,
101157
},
@@ -124,11 +180,16 @@ function _instrumentEmberRouter(
124180

125181
activeRootSpan?.end();
126182

183+
const targetUrl =
184+
(transition as TransitionWithIntent).intent?.url ?? _getLocationURL(location);
185+
const urlAttributes = _getRouteUrlAttributes(targetUrl, transition.to?.params);
186+
127187
activeRootSpan = startBrowserTracingNavigationSpan(client, {
128188
name: `route:${toRoute}`,
129189
attributes: {
130190
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
131191
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember',
192+
...urlAttributes,
132193
fromRoute,
133194
toRoute,
134195
},
@@ -150,6 +211,12 @@ function _instrumentEmberRouter(
150211
}
151212
transitionSpan.end();
152213

214+
const url = routerService.currentURL ?? _getLocationURL(location);
215+
if (url) {
216+
const routeInfo = routerService.recognize(url);
217+
activeRootSpan.setAttributes(_getRouteUrlAttributes(url, routeInfo.params ?? transition.to?.params));
218+
}
219+
153220
if (disableRunloopPerformance) {
154221
activeRootSpan.end();
155222
return;

packages/ember/tests/unit/instrument-router-location-test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { EmberRouterMain } from '@sentry/ember/addon/types';
2-
import { _getLocationURL } from '@sentry/ember/utils/instrumentEmberAppInstanceForPerformance';
2+
import {
3+
_getLocationURL,
4+
_getRouteUrlAttributes,
5+
} from '@sentry/ember/utils/instrumentEmberAppInstanceForPerformance';
36
import { setupTest } from 'ember-qunit';
47
import { module, test } from 'qunit';
58
import type { SentryTestContext } from '../helpers/setup-sentry';
@@ -97,4 +100,28 @@ module('Unit | Utility | instrument-router-location', function (hooks) {
97100
const result = _getLocationURL(mockLocation);
98101
assert.strictEqual(result, '', 'Should return empty string when formatURL is not available');
99102
});
103+
104+
test('_getRouteUrlAttributes handles history location paths', function (this: SentryTestContext, assert) {
105+
const result = _getRouteUrlAttributes('/tracing');
106+
107+
assert.strictEqual(result['url.path'], '/tracing');
108+
assert.strictEqual(result['url.template'], '/tracing');
109+
assert.ok(result['url.full']?.includes('/tracing'), 'url.full includes the path');
110+
});
111+
112+
test('_getRouteUrlAttributes parameterizes dynamic segments', function (this: SentryTestContext, assert) {
113+
const result = _getRouteUrlAttributes('/users/123', { user_id: '123' });
114+
115+
assert.strictEqual(result['url.path'], '/users/123');
116+
assert.strictEqual(result['url.template'], '/users/:user_id');
117+
assert.ok(result['url.full']?.includes('/users/123'), 'url.full includes the path');
118+
});
119+
120+
test('_getRouteUrlAttributes handles hash location paths', function (this: SentryTestContext, assert) {
121+
const result = _getRouteUrlAttributes('/#/tracing');
122+
123+
assert.strictEqual(result['url.path'], '/tracing');
124+
assert.strictEqual(result['url.template'], '/tracing');
125+
assert.ok(result['url.full']?.includes('/tracing'), 'url.full includes the path');
126+
});
100127
});

0 commit comments

Comments
 (0)