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/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/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 5ffdb3fd8860..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,6 +49,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..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,6 +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 }), }, }); } @@ -116,18 +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 }), }); 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}`, + 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 }, + ); } }; @@ -140,6 +148,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 +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 }), }, }); } @@ -240,13 +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', + 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 0b9146b26f5b..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,6 +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 }), ...(params && { ...params }), }, }, @@ -160,14 +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, + // 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 }, + ); }); } 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), }, });