From bd02ab2330be65fa66d71e5d58d0bf6882985447 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 6 Jul 2026 15:17:42 +0200 Subject: [PATCH 1/2] feat(angular): Set url attributes on pageload and navigation spans Add url.full, url.path, and url.template to Angular tracing spans, resolve relative router URLs against the current origin, and extend e2e performance tests to assert the new attributes. Co-Authored-By: Cursor --- .../angular-17/tests/performance.test.ts | 50 ++++++++++++++ .../angular-18/tests/performance.test.ts | 50 ++++++++++++++ .../angular-19/tests/performance.test.ts | 50 ++++++++++++++ .../angular-20/tests/performance.test.ts | 50 ++++++++++++++ .../angular-21/tests/performance.test.ts | 50 ++++++++++++++ .../angular-22/tests/performance.test.ts | 50 ++++++++++++++ packages/angular/ng-package.json | 8 ++- packages/angular/package.json | 1 + packages/angular/src/tracing.ts | 68 +++++++++++++++---- packages/angular/test/tracing.test.ts | 20 ++++-- 10 files changed, 378 insertions(+), 19 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts index 59749ff3cc78..e2238d42a17a 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -17,6 +17,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -46,6 +53,14 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { op: 'navigation', + origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -78,6 +93,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -91,6 +113,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -115,6 +144,13 @@ test('groups redirects within one navigation root span', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, }, }, transaction: '/users/:id/', @@ -145,6 +181,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/cancel', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/cancel$/), + // url.template is not set because the navigation was cancelled before Angular fully resolved the route + }, }, }, transaction: '/cancel', @@ -176,6 +219,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/non-existent', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/non-existent$/), + // url.template is not set because the navigation failed before Angular fully resolved the route + }, }, }, transaction: nonExistentRoute, diff --git a/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts index a44b454b10ef..66146b4b49ae 100644 --- a/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts @@ -17,6 +17,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -46,6 +53,14 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { op: 'navigation', + origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -78,6 +93,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -91,6 +113,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -115,6 +144,13 @@ test('groups redirects within one navigation root span', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, }, }, transaction: '/users/:id/', @@ -145,6 +181,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/cancel', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/cancel$/), + // url.template is not set because the navigation was cancelled before Angular fully resolved the route + }, }, }, transaction: '/cancel', @@ -176,6 +219,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/non-existent', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/non-existent$/), + // url.template is not set because the navigation failed before Angular fully resolved the route + }, }, }, transaction: nonExistentRoute, diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts index c41aa5859f82..826959def7b2 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -17,6 +17,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -46,6 +53,14 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { op: 'navigation', + origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -78,6 +93,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -91,6 +113,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -115,6 +144,13 @@ test('groups redirects within one navigation root span', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, }, }, transaction: '/users/:id/', @@ -145,6 +181,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/cancel', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/cancel$/), + // url.template is not set because the navigation was cancelled before Angular fully resolved the route + }, }, }, transaction: '/cancel', @@ -176,6 +219,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/non-existent', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/non-existent$/), + // url.template is not set because the navigation failed before Angular fully resolved the route + }, }, }, transaction: nonExistentRoute, diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts index cfdd2d95c575..18dbd47eb540 100644 --- a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts @@ -17,6 +17,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -46,6 +53,14 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { op: 'navigation', + origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -78,6 +93,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -91,6 +113,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -115,6 +144,13 @@ test('groups redirects within one navigation root span', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, }, }, transaction: '/users/:id/', @@ -145,6 +181,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/cancel', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/cancel$/), + // url.template is not set because the navigation was cancelled before Angular fully resolved the route + }, }, }, transaction: '/cancel', @@ -176,6 +219,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/non-existent', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/non-existent$/), + // url.template is not set because the navigation failed before Angular fully resolved the route + }, }, }, transaction: nonExistentRoute, diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts index cee1f939c4c6..d2617be9c653 100644 --- a/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts @@ -17,6 +17,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -46,6 +53,14 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { op: 'navigation', + origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -78,6 +93,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -91,6 +113,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -115,6 +144,13 @@ test('groups redirects within one navigation root span', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, }, }, transaction: '/users/:id/', @@ -145,6 +181,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/cancel', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/cancel$/), + // url.template is not set because the navigation was cancelled before Angular fully resolved the route + }, }, }, transaction: '/cancel', @@ -176,6 +219,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/non-existent', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/non-existent$/), + // url.template is not set because the navigation failed before Angular fully resolved the route + }, }, }, transaction: nonExistentRoute, diff --git a/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts index f7a9be7cae56..4eca907379e4 100644 --- a/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts @@ -17,6 +17,13 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -46,6 +53,14 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { op: 'navigation', + origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -78,6 +93,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'pageload', origin: 'auto.pageload.angular', + data: { + 'sentry.origin': 'auto.pageload.angular', + 'sentry.source': 'route', + 'url.template': '/home/', + 'url.path': '/home', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/home$/), + }, }, }, transaction: '/home/', @@ -91,6 +113,13 @@ test('sends a navigation transaction even if the pageload span is still active', trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, }, }, transaction: '/users/:id/', @@ -115,6 +144,13 @@ test('groups redirects within one navigation root span', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'route', + 'url.template': '/users/:id/', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, }, }, transaction: '/users/:id/', @@ -145,6 +181,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/cancel', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/cancel$/), + // url.template is not set because the navigation was cancelled before Angular fully resolved the route + }, }, }, transaction: '/cancel', @@ -176,6 +219,13 @@ test.describe('finish routing span', () => { trace: { op: 'navigation', origin: 'auto.navigation.angular', + data: { + 'sentry.origin': 'auto.navigation.angular', + 'sentry.source': 'url', + 'url.path': '/non-existent', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/non-existent$/), + // url.template is not set because the navigation failed before Angular fully resolved the route + }, }, }, transaction: nonExistentRoute, diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 4a2d2eac3db3..7c460db175aa 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -4,6 +4,12 @@ "lib": { "entryFile": "src/index.ts" }, - "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/types", "tslib"], + "allowedNonPeerDependencies": [ + "@sentry/browser", + "@sentry/core", + "@sentry/types", + "@sentry/conventions", + "tslib" + ], "assets": [] } diff --git a/packages/angular/package.json b/packages/angular/package.json index 162d6bd565a4..c347457081b1 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -23,6 +23,7 @@ "dependencies": { "@sentry/browser": "10.64.0", "@sentry/core": "10.64.0", + "@sentry/conventions": "^0.15.1", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 86d7a4771c9c..f720d5c8803c 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -19,6 +19,7 @@ import { spanToJSON, startBrowserTracingNavigationSpan, startInactiveSpan, + WINDOW, } from '@sentry/browser'; import type { Integration, Span } from '@sentry/core'; import { debug, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/core'; @@ -28,6 +29,7 @@ import { filter, tap } from 'rxjs/operators'; import { ANGULAR_INIT_OP, ANGULAR_OP, ANGULAR_ROUTING_OP } from './constants'; import { IS_DEBUG_BUILD } from './flags'; import { runOutsideAngular } from './zone'; +import { URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; let instrumentationInitialized: boolean; @@ -54,13 +56,38 @@ export function browserTracingIntegration( /** * This function is extracted to make unit testing easier. */ -export function _updateSpanAttributesForParametrizedUrl(route: string, span?: Span): void { - const attributes = (span && spanToJSON(span).data) || {}; +export function _updateSpanAttributesForParametrizedUrl(route: string, url: string, span?: Span): void { + if (!span) { + return; + } + + const { data: attributes, op } = spanToJSON(span); - if (span && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { + if (!attributes || attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { span.updateName(route); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, `auto.${spanToJSON(span).op}.angular`); + + // Angular router gives us relative paths (e.g. `/users/123`). Resolve against the + // current origin so that `url.full` contains the absolute URL including protocol and host. + const locationOrigin = WINDOW.location?.origin; + let urlFull = url; + let urlPath = url; + if (locationOrigin) { + try { + const parsed = new URL(url, locationOrigin); + urlFull = parsed.href; + urlPath = parsed.pathname; + } catch { + // fall back to the raw string + } + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.${op}.angular`, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [URL_FULL]: urlFull, + [URL_PATH]: urlPath, + [URL_TEMPLATE]: route, + }); } } @@ -91,13 +118,30 @@ export class TraceService implements OnDestroy { // see comment in `_isPageloadOngoing` for rationale if (!this._isPageloadOngoing()) { runOutsideAngular(() => { - startBrowserTracingNavigationSpan(client, { - name: strippedUrl, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.angular', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + // Angular router gives us a relative path; resolve it against the current origin + // so the browser tracing integration can set url.full correctly from the start. + const locationOrigin = WINDOW.location?.origin; + let absoluteUrl: string = navigationEvent.url; + if (locationOrigin) { + try { + absoluteUrl = new URL(navigationEvent.url, locationOrigin).href; + } catch { + // fall back to relative path + } + } + startBrowserTracingNavigationSpan( + client, + { + name: strippedUrl, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.angular', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }, + { + url: absoluteUrl, }, - }); + ); }); } else { // The first time we end up here, we set the pageload flag to false @@ -149,7 +193,7 @@ export class TraceService implements OnDestroy { const activeSpan = getActiveSpan(); const rootSpan = activeSpan && getRootSpan(activeSpan); - _updateSpanAttributesForParametrizedUrl(route, rootSpan); + _updateSpanAttributesForParametrizedUrl(route, event.urlAfterRedirects, rootSpan); }), ); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 872fc9c67505..9d34fba938eb 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -8,6 +8,8 @@ import { import { describe, it } from 'vitest'; import { browserTracingIntegration, init, TraceDirective } from '../src/index'; import { _updateSpanAttributesForParametrizedUrl, getParameterizedRouteFromSnapshot } from '../src/tracing'; +import { URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { expect } from 'vitest'; describe('browserTracingIntegration', () => { it('implements required hooks', () => { @@ -73,18 +75,23 @@ describe('Angular Tracing', () => { it('change the span name to route name if the the source is `url`', async () => { init({ integrations: [browserTracingIntegration()] }); - const route = 'sample-route'; + const route = '/users/:id/'; + const url = '/users/123/'; const span = new SentrySpan({ name: 'initial-span-name' }); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); - _updateSpanAttributesForParametrizedUrl(route, span); + _updateSpanAttributesForParametrizedUrl(route, url, span); expect(spanToJSON(span)).toEqual( expect.objectContaining({ - data: { + data: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.undefined.angular', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, + [URL_TEMPLATE]: route, + // URL_FULL is resolved against jsdom's http://localhost origin + [URL_FULL]: expect.stringContaining('/users/123/'), + [URL_PATH]: '/users/123/', + }), description: route, }), ); @@ -93,11 +100,12 @@ describe('Angular Tracing', () => { it('does not change the span name if the source is something other than `url`', async () => { init({ integrations: [browserTracingIntegration()] }); - const route = 'sample-route'; + const route = '/users/:id/'; + const url = '/users/123/'; const span = new SentrySpan({ name: 'initial-span-name' }); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'sample-source'); - _updateSpanAttributesForParametrizedUrl(route, span); + _updateSpanAttributesForParametrizedUrl(route, url, span); expect(spanToJSON(span)).toEqual( expect.objectContaining({ From ed9174985e7073af74b9b3530399864fd703ceaa Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 6 Jul 2026 15:23:49 +0200 Subject: [PATCH 2/2] format --- packages/angular/ng-package.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 7c460db175aa..2f41a87c930f 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -4,12 +4,6 @@ "lib": { "entryFile": "src/index.ts" }, - "allowedNonPeerDependencies": [ - "@sentry/browser", - "@sentry/core", - "@sentry/types", - "@sentry/conventions", - "tslib" - ], + "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/types", "@sentry/conventions", "tslib"], "assets": [] }