diff --git a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts index 14d8b8b21d65..cd67a97c1cef 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts @@ -15,6 +15,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -46,6 +52,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + }, }, }, transaction: '/user/:id', diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts index 362119d1da13..e31cba74ecec 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts @@ -31,6 +31,9 @@ test('Captures a pageload transaction', async ({ page }) => { 'sentry.origin': 'auto.pageload.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), }), op: 'pageload', span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -58,6 +61,9 @@ test('Captures a navigation transaction', async ({ page }) => { 'sentry.origin': 'auto.navigation.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), }), links: [ { @@ -106,6 +112,9 @@ test('Captures a lazy pageload transaction', async ({ page }) => { 'sentry.origin': 'auto.pageload.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/lazy-loaded-user/:id/:innerId', + 'url.path': '/lazy-loaded-user/5/foo', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/), }), op: 'pageload', span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -159,6 +168,9 @@ test('Captures a lazy navigation transaction', async ({ page }) => { 'sentry.origin': 'auto.navigation.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/lazy-loaded-user/:id/:innerId', + 'url.path': '/lazy-loaded-user/5/foo', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/), }), links: [ { diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts index 48ed51bc548b..e1086eb00959 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts @@ -26,6 +26,7 @@ test('Captures a pageload transaction', async ({ page }) => { 'performance.activationStart': expect.any(Number), 'lcp.renderTime': expect.any(Number), 'lcp.loadTime': expect.any(Number), + 'url.template': '/', 'url.full': 'http://localhost:3030/', 'url.path': '/', }, @@ -124,6 +125,9 @@ test('Captures a navigation transaction', async ({ page }) => { 'sentry.origin': 'auto.navigation.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/', + 'url.full': expect.stringMatching(/^http:\/\/localhost:3030\/#\/user\/5$/), }), links: [ { @@ -166,15 +170,22 @@ test('Captures a parameterized path pageload transaction', async ({ page }) => { const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v2/post/:post', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v2/post/:post', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v2/post/:post', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v2/post/1', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path pageload transaction for nested route', async ({ page }) => { @@ -186,15 +197,22 @@ test('Captures a parameterized path pageload transaction for nested route', asyn const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v2/post/:post/featured', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v2/post/:post/featured', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v2/post/:post/featured', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v2/post/1/featured', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path pageload transaction for deeply nested route', async ({ page }) => { @@ -206,15 +224,22 @@ test('Captures a parameterized path pageload transaction for deeply nested route const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v1/post/:post/edit', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v1/post/:post/edit', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v1/post/:post/edit', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v1/post/1/edit', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path pageload transaction for nested route with absolute path', async ({ page }) => { @@ -226,15 +251,22 @@ test('Captures a parameterized path pageload transaction for nested route with a const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v2/post/:post/related', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v2/post/:post/related', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v2/post/:post/related', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v2/post/1/related', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path navigation transaction', async ({ page }) => { @@ -248,15 +280,22 @@ test('Captures a parameterized path navigation transaction', async ({ page }) => const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v2/post/:post', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v2/post/:post', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v2/post/:post', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v2/post/1', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path navigation transaction for nested route', async ({ page }) => { @@ -270,15 +309,22 @@ test('Captures a parameterized path navigation transaction for nested route', as const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v2/post/:post/featured', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v2/post/:post/featured', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v2/post/:post/featured', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v2/post/1/featured', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path navigation transaction for deeply nested route', async ({ page }) => { @@ -292,15 +338,22 @@ test('Captures a parameterized path navigation transaction for deeply nested rou const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v1/post/:post/edit', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v1/post/:post/edit', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v1/post/:post/edit', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v1/post/1/edit', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path navigation transaction for nested route with absolute path', async ({ page }) => { @@ -314,15 +367,22 @@ test('Captures a parameterized path navigation transaction for nested route with const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/v2/post/:post/related', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/v2/post/:post/related', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/v2/post/:post/related', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/v2/post/1/related', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path pageload transaction for group route', async ({ page }) => { @@ -334,15 +394,22 @@ test('Captures a parameterized path pageload transaction for group route', async const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/group/:group/:user?', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/group/:group/:user?', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/group/1', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path navigation transaction for group route', async ({ page }) => { @@ -356,15 +423,22 @@ test('Captures a parameterized path navigation transaction for group route', asy const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/group/:group/:user?', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/group/:group/:user?', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/group/1', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path pageload transaction for nested group route', async ({ page }) => { @@ -376,15 +450,22 @@ test('Captures a parameterized path pageload transaction for nested group route' const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/group/:group/:user?', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/group/:group/:user?', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/group/1/5', + }, }, - }), - ); + }, + }); }); test('Captures a parameterized path navigation transaction for nested group route', async ({ page }) => { @@ -398,13 +479,20 @@ test('Captures a parameterized path navigation transaction for nested group rout const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: '/group/:group/:user?', + type: 'transaction', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + data: { + 'url.template': '/group/:group/:user?', + 'url.path': '/', + 'url.full': 'http://localhost:3030/#/group/1/5', + }, }, - }), - ); + }, + }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts index 5c60b704bb7a..8fff883df119 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts @@ -28,6 +28,9 @@ test('Captures a pageload transaction', async ({ page }) => { 'sentry.origin': 'auto.pageload.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), }), op: 'pageload', span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -55,6 +58,9 @@ test('Captures a navigation transaction', async ({ page }) => { 'sentry.origin': 'auto.navigation.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), }), links: [ { diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-5/tests/transactions.test.ts index 71808851a290..14c0c47ae5da 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-5/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-5/tests/transactions.test.ts @@ -15,6 +15,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v5', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -46,6 +52,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter_v5', + data: { + 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + }, }, }, transaction: '/user/:id', diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts index 2f13b7cc1eac..7c5a1bd7ac67 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts @@ -16,6 +16,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/views/:viewId/:detailId', + 'url.path': '/projects/123/views/234/567', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), + }, }, }, transaction: '/projects/:projectId/views/:viewId/:detailId', @@ -40,6 +46,12 @@ test('sends a pageload transaction with a parameterized URL - alternative route' trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', + 'url.path': '/projects/234/old-views/234/567', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), + }, }, }, transaction: '/projects/:projectId/old-views/:viewId/:detailId', @@ -66,6 +78,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -84,6 +102,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/views/:viewId/:detailId', + 'url.path': '/projects/123/views/456/789', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), + }, }, }, transaction: '/projects/:projectId/views/:viewId/:detailId', @@ -110,6 +134,12 @@ test('sends a navigation transaction with a parameterized URL - alternative rout trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -128,6 +158,12 @@ test('sends a navigation transaction with a parameterized URL - alternative rout trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', + 'url.path': '/projects/123/old-views/345/654', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), + }, }, }, transaction: '/projects/:projectId/old-views/:viewId/:detailId', diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/transactions.test.ts index 160ab8a4909f..20a126396acf 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/transactions.test.ts @@ -15,6 +15,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -46,6 +52,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + }, }, }, transaction: '/user/:id', diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts index ae9ff366abd4..ad1f99199ed2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/transactions.test.ts @@ -15,6 +15,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -46,6 +52,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter_v6', + data: { + 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + }, }, }, transaction: '/user/:id', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts index d889793aa73a..f0d82229770f 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts @@ -16,6 +16,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/views/:viewId/:detailId', + 'url.path': '/projects/123/views/234/567', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), + }, }, }, transaction: '/projects/:projectId/views/:viewId/:detailId', @@ -40,6 +46,12 @@ test('sends a pageload transaction with a parameterized URL - alternative route' trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', + 'url.path': '/projects/234/old-views/234/567', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), + }, }, }, transaction: '/projects/:projectId/old-views/:viewId/:detailId', @@ -66,6 +78,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -84,6 +102,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/views/:viewId/:detailId', + 'url.path': '/projects/123/views/456/789', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), + }, }, }, transaction: '/projects/:projectId/views/:viewId/:detailId', @@ -110,6 +134,12 @@ test('sends a navigation transaction with a parameterized URL - alternative rout trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -128,6 +158,12 @@ test('sends a navigation transaction with a parameterized URL - alternative rout trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', + 'url.path': '/projects/123/old-views/345/654', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), + }, }, }, transaction: '/projects/:projectId/old-views/:viewId/:detailId', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/tests/spans.test.ts index 6056d85f09e0..8da4f7a8ae39 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa-streaming/tests/spans.test.ts @@ -15,6 +15,9 @@ test('sends a pageload span with a parameterized URL', async ({ page }) => { expect(span.status).toBe('ok'); expect(span.attributes['sentry.origin']?.value).toBe('auto.pageload.react.reactrouter_v7'); expect(span.attributes['sentry.source']?.value).toBe('route'); + expect(span.attributes['url.template']?.value).toBe('/'); + expect(span.attributes['url.path']?.value).toBe('/'); + expect(span.attributes['url.full']?.value).toMatch(/^https?:\/\/localhost:\d+\/$/); }); test('sends a navigation span with a parameterized URL', async ({ page }) => { @@ -39,6 +42,9 @@ test('sends a navigation span with a parameterized URL', async ({ page }) => { expect(navigationSpan.status).toBe('ok'); expect(navigationSpan.attributes['sentry.origin']?.value).toBe('auto.navigation.react.reactrouter_v7'); expect(navigationSpan.attributes['sentry.source']?.value).toBe('route'); + expect(navigationSpan.attributes['url.template']?.value).toBe('/user/:id'); + expect(navigationSpan.attributes['url.path']?.value).toBe('/user/5'); + expect(navigationSpan.attributes['url.full']?.value).toMatch(/^https?:\/\/localhost:\d+\/user\/5$/); }); test('sends an INP span', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts index 27554491019f..92cf76f5e181 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts @@ -15,6 +15,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -46,6 +52,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + }, }, }, transaction: '/user/:id', diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts index a1731af3e968..3aa5f002fe11 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts @@ -16,6 +16,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/views/:viewId/:detailId', + 'url.path': '/projects/123/views/234/567', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), + }, }, }, transaction: '/projects/:projectId/views/:viewId/:detailId', @@ -40,6 +46,12 @@ test('sends a pageload transaction with a parameterized URL - alternative route' trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', + 'url.path': '/projects/234/old-views/234/567', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), + }, }, }, transaction: '/projects/:projectId/old-views/:viewId/:detailId', @@ -66,6 +78,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -84,6 +102,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/views/:viewId/:detailId', + 'url.path': '/projects/123/views/456/789', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), + }, }, }, transaction: '/projects/:projectId/views/:viewId/:detailId', @@ -110,6 +134,12 @@ test('sends a navigation transaction with a parameterized URL - alternative rout trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -128,6 +158,12 @@ test('sends a navigation transaction with a parameterized URL - alternative rout trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', + 'url.path': '/projects/123/old-views/345/654', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), + }, }, }, transaction: '/projects/:projectId/old-views/:viewId/:detailId', diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts index fcf012f4b51c..36ab03d87c3e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts @@ -15,6 +15,12 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = trace: { op: 'pageload', origin: 'auto.pageload.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -46,6 +52,12 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) trace: { op: 'navigation', origin: 'auto.navigation.react.reactrouter', + data: { + 'sentry.source': 'route', + 'url.template': '/user/:id', + 'url.path': '/user/5', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), + }, }, }, transaction: '/user/:id', diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx b/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx index 47d112136049..3c2ed2905383 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/src/main.tsx @@ -63,6 +63,11 @@ const postIdRoute = createRoute({ shouldReload() { return true; }, + beforeLoad: ({ params }) => { + if (params.postId === '999') { + throw redirect({ to: '/posts/$postId', params: { postId: '2' }, replace: true }); + } + }, loader: ({ params }) => { return Sentry.startSpan({ name: `loading-post-${params.postId}` }, async () => { await new Promise(resolve => setTimeout(resolve, 1000)); diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts index ee752f4b7cac..06708292089e 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/tests/routing-instrumentation.test.ts @@ -18,6 +18,9 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = 'sentry.origin': 'auto.pageload.react.tanstack_router', 'sentry.op': 'pageload', 'url.path.params.postId': '456', + 'url.template': '/posts/$postId', + 'url.path': '/posts/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/456$/), }, op: 'pageload', origin: 'auto.pageload.react.tanstack_router', @@ -49,6 +52,12 @@ test('sends pageload transaction with web vitals measurements', async ({ page }) trace: { op: 'pageload', origin: 'auto.pageload.react.tanstack_router', + data: { + 'sentry.source': 'route', + 'url.template': '/', + 'url.path': '/', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), + }, }, }, transaction: '/', @@ -101,6 +110,9 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) 'sentry.origin': 'auto.navigation.react.tanstack_router', 'sentry.op': 'navigation', 'url.path.params.postId': '2', + 'url.template': '/posts/$postId', + 'url.path': '/posts/2', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), }, op: 'navigation', origin: 'auto.navigation.react.tanstack_router', @@ -118,6 +130,41 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) }); }); +test('sends a pageload transaction with resolved URL attrs after same-route redirect on initial load', async ({ + page, +}) => { + const pageloadTxnPromise = waitForTransaction('tanstack-router', async transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/posts/$postId'; + }); + + // `/posts/999` matches `/posts/$postId` initially, then `beforeLoad` redirects to `/posts/2`. + await page.goto(`/posts/999`); + + const pageloadTxn = await pageloadTxnPromise; + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.react.tanstack_router', + 'sentry.op': 'pageload', + 'url.path.params.postId': '2', + 'url.template': '/posts/$postId', + 'url.path': '/posts/2', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), + }, + op: 'pageload', + origin: 'auto.pageload.react.tanstack_router', + }, + }, + transaction: '/posts/$postId', + transaction_info: { + source: 'route', + }, + }); +}); + test('sends a pageload transaction named after the resolved route when a redirect is thrown on initial load', async ({ page, }) => { @@ -139,6 +186,9 @@ test('sends a pageload transaction named after the resolved route when a redirec 'sentry.origin': 'auto.pageload.react.tanstack_router', 'sentry.op': 'pageload', 'url.path.params.postId': '1', + 'url.template': '/posts/$postId', + 'url.path': '/posts/1', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/), }, op: 'pageload', origin: 'auto.pageload.react.tanstack_router', @@ -177,6 +227,9 @@ test('sends a navigation transaction when a redirect is thrown in beforeLoad', a 'sentry.origin': 'auto.navigation.react.tanstack_router', 'sentry.op': 'navigation', 'url.path.params.postId': '1', + 'url.template': '/posts/$postId', + 'url.path': '/posts/1', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/), }, op: 'navigation', origin: 'auto.navigation.react.tanstack_router', @@ -226,6 +279,9 @@ test('sends a navigation transaction for a normal navigation that happens after 'sentry.origin': 'auto.navigation.react.tanstack_router', 'sentry.op': 'navigation', 'url.path.params.postId': '2', + 'url.template': '/posts/$postId', + 'url.path': '/posts/2', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), }, op: 'navigation', origin: 'auto.navigation.react.tanstack_router', diff --git a/packages/react/package.json b/packages/react/package.json index 8fd7107944a7..eed0a949a151 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -44,7 +44,8 @@ }, "dependencies": { "@sentry/browser": "10.64.0", - "@sentry/core": "10.64.0" + "@sentry/core": "10.64.0", + "@sentry/conventions": "^0.15.1" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index 5bc58325f963..12b1efecbb07 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -47,6 +47,7 @@ import { setNavigationContext, transactionNameHasWildcard, } from './utils'; +import { URL_TEMPLATE } from '@sentry/conventions/attributes'; let _useEffect: UseEffect; let _useLocation: UseLocation; @@ -398,6 +399,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 +1001,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 +1039,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 +1128,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 +1227,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/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 73cc390e4645..1ac4c7b265ee 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -18,6 +18,7 @@ import type { ReactElement } from 'react'; import * as React from 'react'; import { hoistNonReactStatics } from './hoist-non-react-statics'; import type { Action, Location } from './types'; +import { URL_TEMPLATE } from '@sentry/conventions/attributes'; // We need to disable eslint no-explicit-any because any is required for the // react-router typings. @@ -162,6 +163,7 @@ function instrumentReactRouter( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(source === 'route' && { [URL_TEMPLATE]: name }), }, }); } @@ -177,6 +179,7 @@ function instrumentReactRouter( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(source === 'route' && { [URL_TEMPLATE]: name }), }, }); } @@ -233,7 +236,7 @@ export function withSentryRouting
, R extends React
if (activeRootSpan) {
activeRootSpan.updateName(route);
- activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ activeRootSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [URL_TEMPLATE]: route });
}
}
diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts
index cdf97c02d03b..b08df8632f41 100644
--- a/packages/react/src/reactrouterv3.ts
+++ b/packages/react/src/reactrouterv3.ts
@@ -11,6 +11,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core/browser';
import type { Location } from './types';
+import { URL_TEMPLATE } from '@sentry/conventions/attributes';
// Many of the types below had to be mocked out to prevent typescript issues
// these types are required for correct functionality.
@@ -68,6 +69,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 +90,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..42993182cd55 100644
--- a/packages/react/src/tanstackrouter.ts
+++ b/packages/react/src/tanstackrouter.ts
@@ -1,5 +1,6 @@
import {
browserTracingIntegration as originalBrowserTracingIntegration,
+ getAbsoluteUrl,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
WINDOW,
@@ -11,6 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core/browser';
import type { VendoredTanstackRouter, VendoredTanstackRouterRouteMatch } from './vendor/tanstackrouter-types';
+import { PARAMS_KEY, URL_FULL, URL_PATH, URL_PATH_PARAMETER_KEY, URL_TEMPLATE } from '@sentry/conventions/attributes';
+
+interface TanstackRouterLocation {
+ pathname: string;
+ search: Record