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 8d813e0f2774..f674e9f7279d 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 @@ -43,9 +43,7 @@ test('should create a parameterized transaction when the `app` directory is used }); }); -test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({ - page, -}) => { +test('should create a transaction named after the static route when the `app` directory is used', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload' @@ -63,9 +61,10 @@ test('should create a static transaction when the `app` directory is used and th data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/static$/), 'url.path': '/parameterized/static', + 'url.template': '/parameterized/static', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -81,11 +80,9 @@ test('should create a static transaction when the `app` directory is used and th start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: '/parameterized/static', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); - - expect(transaction.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('should create a partially parameterized transaction when the `app` directory is used', 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 b9ea5801d1cc..ecaeb9747ddd 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 @@ -43,9 +43,7 @@ test('should create a parameterized transaction when the `app` directory is used }); }); -test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({ - page, -}) => { +test('should create a transaction named after the static route when the `app` directory is used', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return ( transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload' @@ -63,9 +61,10 @@ test('should create a static transaction when the `app` directory is used and th data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/static$/), 'url.path': '/parameterized/static', + 'url.template': '/parameterized/static', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -81,11 +80,9 @@ test('should create a static transaction when the `app` directory is used and th start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: '/parameterized/static', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); - - expect(transaction.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('should create a partially parameterized transaction when the `app` directory is used', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts index 07e5f007efad..98ac069d5b3f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts @@ -40,9 +40,7 @@ test('should create a parameterized transaction when the `app` directory is used }); }); -test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({ - page, -}) => { +test('should create a transaction named after the static route when the `app` directory is used', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => { return ( transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload' @@ -60,7 +58,8 @@ test('should create a static transaction when the `app` directory is used and th data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', + 'url.template': '/parameterized/static', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -76,7 +75,7 @@ test('should create a static transaction when the `app` directory is used and th start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: '/parameterized/static', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts index b3ba64bb55c8..7f903d044ee2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts @@ -40,9 +40,7 @@ test('should create a parameterized transaction when the `app` directory is used }); }); -test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({ - page, -}) => { +test('should create a transaction named after the static route when the `app` directory is used', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => { return ( transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload' @@ -60,7 +58,8 @@ test('should create a static transaction when the `app` directory is used and th data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', + 'url.template': '/parameterized/static', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -76,7 +75,7 @@ test('should create a static transaction when the `app` directory is used and th start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: '/parameterized/static', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts index 457d87db3dc0..106e430c8177 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts @@ -71,7 +71,7 @@ test('Prerendered shell does not stitch the pageload onto a stale trace', async }); const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-streaming-cacheComponents', span => { - return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/pageload-tracing' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto('/pageload-tracing'); @@ -81,8 +81,9 @@ test('Prerendered shell does not stitch the pageload onto a stale trace', async const [serverSpan, pageloadSpan] = await Promise.all([serverSpanPromise, pageloadSpanPromise]); expect(pageloadSpan.attributes).toMatchObject({ - ['sentry.segment.name.source']: { value: 'url', type: 'string' }, + ['sentry.segment.name.source']: { value: 'route', type: 'string' }, ['url.path']: { value: '/pageload-tracing', type: 'string' }, + ['url.template']: { value: '/pageload-tracing', type: 'string' }, }); // Under Cache Components the can be prerendered and rendered in a context detached from the // runtime server request, so a `sentry-trace` meta tag would carry a stale/unrelated trace. The diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/pageload-tracing.test.ts index 70539d3d95c5..a4a1785042d9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/pageload-tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/pageload-tracing.test.ts @@ -7,7 +7,7 @@ test('Server and client pageload spans should share the same trace', async ({ pa }); const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-streaming', span => { - return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/pageload-tracing' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/pageload-tracing`); @@ -17,7 +17,7 @@ test('Server and client pageload spans should share the same trace', async ({ pa expect(pageloadSpan.trace_id).toBeTruthy(); expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id); expect(pageloadSpan.attributes).toMatchObject({ - ['sentry.segment.name.source']: { value: 'url', type: 'string' }, + ['sentry.segment.name.source']: { value: 'route', type: 'string' }, ['url.path']: { value: '/pageload-tracing', type: 'string' }, }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/parameterized-routes.test.ts index 502d7fdced2a..f7951bc3a355 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/parameterized-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/parameterized-routes.test.ts @@ -15,22 +15,40 @@ test('should create a parameterized streamed span when the `app` directory is us expect(span.attributes['sentry.source']?.value).toBe('route'); }); -test('should create a static streamed span when the `app` directory is used and the route is not parameterized', async ({ +test('should create a streamed span named after the static route when the `app` directory is used', async ({ page, }) => { const spanPromise = waitForStreamedSpan('nextjs-16-streaming', span => { - return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/parameterized/static' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/parameterized/static`); const span = await spanPromise; + expect(span.name).toBe('/parameterized/static'); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); + expect(span.attributes).toMatchObject({ + ['sentry.segment.name.source']: { value: 'route', type: 'string' }, + ['url.template']: { value: '/parameterized/static', type: 'string' }, + ['url.path']: { value: '/parameterized/static', type: 'string' }, + }); +}); + +test('should fall back to a low cardinality span name for routes the manifest does not know', async ({ page }) => { + const spanPromise = waitForStreamedSpan('nextjs-16-streaming', span => { + return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/this-route-does-not-exist'); + + const span = await spanPromise; + expect(span.name).toBe('Pageload'); expect(span.trace_id).toMatch(/[a-f0-9]{32}/); expect(span.attributes).toMatchObject({ ['sentry.segment.name.source']: { value: 'url', type: 'string' }, - ['url.path']: { value: '/parameterized/static', type: 'string' }, + ['url.path']: { value: '/this-route-does-not-exist', type: 'string' }, }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts index cfdfe12d0c27..cb0c6e76f7e5 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/tests/trailing-slash-parameterization.test.ts @@ -20,14 +20,15 @@ test('should create a correctly named pageload transaction for a static route', data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', + 'url.template': '/static-page', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', }, }, transaction: '/static-page', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); }); @@ -80,14 +81,15 @@ test('should create a correctly named pageload transaction for a static nested r data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', + 'url.template': '/parameterized/static', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', }, }, transaction: '/parameterized/static', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); }); @@ -134,14 +136,15 @@ test('should create a correctly named pageload transaction for the home page', a data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', + 'url.template': '/', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', }, }, transaction: '/', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); }); 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 38772dadc092..afaf6c2a5601 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 @@ -43,9 +43,7 @@ test('should create a parameterized transaction when the `app` directory is used }); }); -test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({ - page, -}) => { +test('should create a transaction named after the static route when the `app` directory is used', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-16', async transactionEvent => { return ( transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload' @@ -63,9 +61,10 @@ test('should create a static transaction when the `app` directory is used and th data: { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/parameterized\/static$/), 'url.path': '/parameterized/static', + 'url.template': '/parameterized/static', }, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -81,11 +80,9 @@ test('should create a static transaction when the `app` directory is used and th start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: '/parameterized/static', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', }); - - expect(transaction.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('should create a partially parameterized transaction when the `app` directory is used', async ({ page }) => { 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 ec0babf7df77..8d89217eb395 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 @@ -19,7 +19,7 @@ test('Sends a pageload transaction', async ({ page }) => { expect(transactionEvent).toEqual( expect.objectContaining({ transaction: '/', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, type: 'transaction', contexts: expect.objectContaining({ react: { @@ -37,9 +37,10 @@ test('Sends a pageload transaction', async ({ page }) => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), 'url.path': '/', + 'url.template': '/', }), }, }), @@ -51,8 +52,6 @@ test('Sends a pageload transaction', async ({ page }) => { }, }), ); - - expect(transactionEvent.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('Should send a transaction for instrumented server actions', async ({ page }) => { diff --git a/packages/nextjs/src/client/routing/parameterization.ts b/packages/nextjs/src/client/routing/parameterization.ts index e80720beb262..a45f0faab8fd 100644 --- a/packages/nextjs/src/client/routing/parameterization.ts +++ b/packages/nextjs/src/client/routing/parameterization.ts @@ -130,9 +130,9 @@ function findMatchingRoutes( ): string[] { const matches: string[] = []; - // Static path: no parameterization needed, return empty array + // Static path: no parameterization needed, return the route itself as already parameterized if (staticRoutes.some(r => r.path === route)) { - return matches; + return [route]; } // Dynamic path: find the route pattern that matches the concrete route diff --git a/packages/nextjs/test/client/parameterization.test.ts b/packages/nextjs/test/client/parameterization.test.ts index dd7abff0bcd0..18bd61bee210 100644 --- a/packages/nextjs/test/client/parameterization.test.ts +++ b/packages/nextjs/test/client/parameterization.test.ts @@ -25,17 +25,17 @@ describe('maybeParameterizeRoute', () => { }); describe('when manifest has static routes', () => { - it('should return undefined for static routes', () => { + it('returns the route itself for static routes', () => { const manifest: RouteManifest = { staticRoutes: [{ path: '/' }, { path: '/some/nested' }, { path: '/user' }, { path: '/users' }], dynamicRoutes: [], }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRoute('/')).toBeUndefined(); - expect(maybeParameterizeRoute('/some/nested')).toBeUndefined(); - expect(maybeParameterizeRoute('/user')).toBeUndefined(); - expect(maybeParameterizeRoute('/users')).toBeUndefined(); + expect(maybeParameterizeRoute('/')).toBe('/'); + expect(maybeParameterizeRoute('/some/nested')).toBe('/some/nested'); + expect(maybeParameterizeRoute('/user')).toBe('/user'); + expect(maybeParameterizeRoute('/users')).toBe('/users'); }); }); @@ -78,7 +78,7 @@ describe('maybeParameterizeRoute', () => { expect(maybeParameterizeRoute('/users/john-doe/settings')).toBe('/users/:id/settings'); }); - it('should return undefined for static routes even when dynamic routes exist', () => { + it('returns the route itself for static routes even when dynamic routes exist', () => { const manifest: RouteManifest = { staticRoutes: [{ path: '/' }, { path: '/dynamic/static' }, { path: '/static/nested' }], dynamicRoutes: [ @@ -91,9 +91,9 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRoute('/')).toBeUndefined(); - expect(maybeParameterizeRoute('/dynamic/static')).toBeUndefined(); - expect(maybeParameterizeRoute('/static/nested')).toBeUndefined(); + expect(maybeParameterizeRoute('/')).toBe('/'); + expect(maybeParameterizeRoute('/dynamic/static')).toBe('/dynamic/static'); + expect(maybeParameterizeRoute('/static/nested')).toBe('/static/nested'); }); it('should handle catchall routes', () => { @@ -137,11 +137,11 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRoute('/(auth)/login')).toBeUndefined(); - expect(maybeParameterizeRoute('/(auth)/signup')).toBeUndefined(); - expect(maybeParameterizeRoute('/(dashboard)/dashboard')).toBeUndefined(); - expect(maybeParameterizeRoute('/(dashboard)/settings/profile')).toBeUndefined(); - expect(maybeParameterizeRoute('/(marketing)/public/about')).toBeUndefined(); + expect(maybeParameterizeRoute('/(auth)/login')).toBe('/(auth)/login'); + expect(maybeParameterizeRoute('/(auth)/signup')).toBe('/(auth)/signup'); + expect(maybeParameterizeRoute('/(dashboard)/dashboard')).toBe('/(dashboard)/dashboard'); + expect(maybeParameterizeRoute('/(dashboard)/settings/profile')).toBe('/(dashboard)/settings/profile'); + expect(maybeParameterizeRoute('/(marketing)/public/about')).toBe('/(marketing)/public/about'); expect(maybeParameterizeRoute('/(dashboard)/dashboard/123')).toBe('/(dashboard)/dashboard/:id'); }); @@ -165,11 +165,11 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRoute('/login')).toBeUndefined(); - expect(maybeParameterizeRoute('/signup')).toBeUndefined(); - expect(maybeParameterizeRoute('/dashboard')).toBeUndefined(); - expect(maybeParameterizeRoute('/settings/profile')).toBeUndefined(); - expect(maybeParameterizeRoute('/public/about')).toBeUndefined(); + expect(maybeParameterizeRoute('/login')).toBe('/login'); + expect(maybeParameterizeRoute('/signup')).toBe('/signup'); + expect(maybeParameterizeRoute('/dashboard')).toBe('/dashboard'); + expect(maybeParameterizeRoute('/settings/profile')).toBe('/settings/profile'); + expect(maybeParameterizeRoute('/public/about')).toBe('/public/about'); expect(maybeParameterizeRoute('/dashboard/123')).toBe('/dashboard/:id'); }); @@ -283,7 +283,7 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRoute('/')).toBeUndefined(); + expect(maybeParameterizeRoute('/')).toBe('/'); }); it('should handle complex nested dynamic routes', () => { @@ -307,17 +307,17 @@ describe('maybeParameterizeRoute', () => { describe('realistic Next.js App Router patterns', () => { it.each([ - ['/', undefined], - ['/some/nested', undefined], - ['/user', undefined], - ['/users', undefined], - ['/dynamic/static', undefined], - ['/static/nested', undefined], - ['/login', undefined], - ['/signup', undefined], - ['/dashboard', undefined], - ['/settings/profile', undefined], - ['/public/about', undefined], + ['/', '/'], + ['/some/nested', '/some/nested'], + ['/user', '/user'], + ['/users', '/users'], + ['/dynamic/static', '/dynamic/static'], + ['/static/nested', '/static/nested'], + ['/login', '/login'], + ['/signup', '/signup'], + ['/dashboard', '/dashboard'], + ['/settings/profile', '/settings/profile'], + ['/public/about', '/public/about'], ['/dynamic/123', '/dynamic/:id'], ['/dynamic/abc', '/dynamic/:id'], @@ -472,8 +472,8 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - // Static route should take precedence (no parameterization) - expect(maybeParameterizeRoute('/static')).toBeUndefined(); + // Static route should take precedence over the catch-alls + expect(maybeParameterizeRoute('/static')).toBe('/static'); // Single segment should match regular dynamic route expect(maybeParameterizeRoute('/dynamic')).toBe('/:param'); @@ -515,10 +515,10 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - // Static routes should not be parameterized - expect(maybeParameterizeRoute('/')).toBeUndefined(); - expect(maybeParameterizeRoute('/about')).toBeUndefined(); - expect(maybeParameterizeRoute('/contact')).toBeUndefined(); + // Static routes should resolve to themselves rather than falling through to a catch-all + expect(maybeParameterizeRoute('/')).toBe('/'); + expect(maybeParameterizeRoute('/about')).toBe('/about'); + expect(maybeParameterizeRoute('/contact')).toBe('/contact'); // Specific dynamic routes should take precedence over catch-all expect(maybeParameterizeRoute('/blog/my-post')).toBe('/blog/:slug'); @@ -827,8 +827,8 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - // Root should not be parameterized (it's a static route) - expect(maybeParameterizeRoute('/')).toBeUndefined(); + // Root is a static route, so it wins over the locale-prefixed patterns + expect(maybeParameterizeRoute('/')).toBe('/'); // Default locale (English, no prefix) - this was the bug expect(maybeParameterizeRoute('/hola')).toBe('/:locale/hola'); @@ -942,10 +942,10 @@ describe('maybeParameterizeRoute', () => { }; globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRoute('/about/')).toBeUndefined(); - expect(maybeParameterizeRoute('/settings/profile/')).toBeUndefined(); + expect(maybeParameterizeRoute('/about/')).toBe('/about'); + expect(maybeParameterizeRoute('/settings/profile/')).toBe('/settings/profile'); // Root path should still work - expect(maybeParameterizeRoute('/')).toBeUndefined(); + expect(maybeParameterizeRoute('/')).toBe('/'); }); it('should match dynamic routes when path has a trailing slash', () => { @@ -994,8 +994,8 @@ describe('maybeParameterizeRoute', () => { globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); // Static routes with trailing slash should NOT fall through to catch-all - expect(maybeParameterizeRoute('/about/')).toBeUndefined(); - expect(maybeParameterizeRoute('/contact/')).toBeUndefined(); + expect(maybeParameterizeRoute('/about/')).toBe('/about'); + expect(maybeParameterizeRoute('/contact/')).toBe('/contact'); // Dynamic routes with trailing slash should match correctly, not catch-all expect(maybeParameterizeRoute('/blog/my-post/')).toBe('/blog/:slug'); @@ -1025,13 +1025,13 @@ describe('maybeParameterizeRoute', () => { globalWithInjectedManifest._sentryRouteManifest = JSON.stringify(manifest); // Static route with trailing slash should not match the optional catch-all - expect(maybeParameterizeRoute('/static-page/')).toBeUndefined(); + expect(maybeParameterizeRoute('/static-page/')).toBe('/static-page'); // Dynamic route with trailing slash should not match the optional catch-all expect(maybeParameterizeRoute('/parameterized/value/')).toBe('/parameterized/:param'); // Root with trailing slash is just '/' - should match static - expect(maybeParameterizeRoute('/')).toBeUndefined(); + expect(maybeParameterizeRoute('/')).toBe('/'); }); it('should produce the same result for paths with and without trailing slashes', () => {