diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts index 8a62126dbe15..ff773075f3fb 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts @@ -14,13 +14,12 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => { expect(transactionEvent.contexts?.trace?.data).toEqual( expect.objectContaining({ 'sentry.origin': 'auto.pageload.remix', - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), 'url.path': '/', + 'url.template': '/', }), ); - // no url.template because the route isn't parameterized (sentry.source: 'url') - expect(transactionEvent.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('Sends a navigation transaction to Sentry', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts index f47a001fa7c7..30188a9d4ee8 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts @@ -14,13 +14,12 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => { expect(transactionEvent.contexts?.trace?.data).toEqual( expect.objectContaining({ 'sentry.origin': 'auto.pageload.remix', - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), 'url.path': '/', + 'url.template': '/', }), ); - // no url.template because the route isn't parameterized (sentry.source: 'url') - expect(transactionEvent.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('Sends a navigation transaction to Sentry', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts index 111902c7dd3c..0f60d08876c6 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts @@ -13,13 +13,12 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => { expect(transactionEvent).toBeDefined(); expect(transactionEvent.contexts?.trace?.data).toEqual( expect.objectContaining({ - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), 'url.path': '/', + 'url.template': '/', }), ); - // no url.template because the route isn't parameterized (sentry.source: 'url') - expect(transactionEvent.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('Sends a navigation transaction to Sentry', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts index 90745d9b777b..bde21cd3673a 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts @@ -16,15 +16,14 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => { contexts: { trace: { data: { - 'sentry.source': 'url', + 'sentry.source': 'route', 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), 'url.path': '/', + 'url.template': '/', }, }, }, }); - // no url.template because the route isn't parameterized (sentry.source: 'url') - expect(transactionEvent.contexts?.trace?.data).not.toHaveProperty('url.template'); }); test('Sends a navigation transaction to Sentry', async ({ page }) => { diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index a3ef39d37776..4890eb31773b 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -62,8 +62,8 @@ function getInitPathName(): string | undefined { /** * Determines the transaction name and source for a route. * Handles three cases: - * 1. Dynamic routes with manifest (Vite apps): Use parameterized path with source 'route' - * 2. Static routes with manifest (Vite apps): Use pathname with source 'url' + * 1. Routes resolved from the manifest (Vite apps): Use the route template with source 'route' + * 2. Routes the manifest doesn't know, e.g. 404s (Vite apps): Use pathname with source 'url' * 3. Legacy apps without manifest: Use route ID with source 'route' */ function getTransactionNameAndSource( @@ -73,13 +73,11 @@ function getTransactionNameAndSource( const parameterizedRoute = pathname ? maybeParameterizeRemixRoute(pathname) : undefined; if (parameterizedRoute) { - // We have a parameterized route from the manifest (dynamic route) return { name: parameterizedRoute, source: 'route' }; } if (hasManifest()) { - // We have a manifest but no parameterization (static route) - // Use the pathname with source 'url' + // The manifest doesn't know this route, so the pathname may well be high cardinality. return { name: pathname || routeId, source: 'url' }; } diff --git a/packages/remix/src/client/remixRouteParameterization.ts b/packages/remix/src/client/remixRouteParameterization.ts index 6a587afffdd9..5e389c2e87ad 100644 --- a/packages/remix/src/client/remixRouteParameterization.ts +++ b/packages/remix/src/client/remixRouteParameterization.ts @@ -112,9 +112,9 @@ function findMatchingRoutes( ): string[] { const matches: string[] = []; - // Static routes don't need parameterization - return empty to keep source as 'url' + // Static routes don't need parameterization, return the route itself as already parameterized if (staticRoutes.some(r => r.path === route)) { - return matches; + return [route]; } // Check dynamic routes @@ -142,7 +142,7 @@ export function hasManifest(): boolean { * Parameterize a route using the route manifest. * * @param route - The route to parameterize. - * @returns The parameterized route or undefined if no parameterization is needed. + * @returns The parameterized route or undefined if not able to parameterize. */ export const maybeParameterizeRemixRoute = (route: string): string | undefined => { const manifest = getManifest(); diff --git a/packages/remix/test/client/remixRouteParameterization.test.ts b/packages/remix/test/client/remixRouteParameterization.test.ts index 59eb2c1796c1..128d50a363aa 100644 --- a/packages/remix/test/client/remixRouteParameterization.test.ts +++ b/packages/remix/test/client/remixRouteParameterization.test.ts @@ -25,17 +25,17 @@ describe('maybeParameterizeRemixRoute', () => { }); 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: '/about' }, { path: '/contact' }, { path: '/blog/posts' }], dynamicRoutes: [], }; globalWithInjectedManifest._sentryRemixRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRemixRoute('/')).toBeUndefined(); - expect(maybeParameterizeRemixRoute('/about')).toBeUndefined(); - expect(maybeParameterizeRemixRoute('/contact')).toBeUndefined(); - expect(maybeParameterizeRemixRoute('/blog/posts')).toBeUndefined(); + expect(maybeParameterizeRemixRoute('/')).toBe('/'); + expect(maybeParameterizeRemixRoute('/about')).toBe('/about'); + expect(maybeParameterizeRemixRoute('/contact')).toBe('/contact'); + expect(maybeParameterizeRemixRoute('/blog/posts')).toBe('/blog/posts'); }); }); @@ -84,8 +84,8 @@ describe('maybeParameterizeRemixRoute', () => { }; globalWithInjectedManifest._sentryRemixRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRemixRoute('/')).toBeUndefined(); - expect(maybeParameterizeRemixRoute('/about')).toBeUndefined(); + expect(maybeParameterizeRemixRoute('/')).toBe('/'); + expect(maybeParameterizeRemixRoute('/about')).toBe('/about'); }); it('should handle splat/catch-all routes', () => { @@ -222,7 +222,7 @@ describe('maybeParameterizeRemixRoute', () => { }; globalWithInjectedManifest._sentryRemixRouteManifest = JSON.stringify(manifest); - expect(maybeParameterizeRemixRoute('/')).toBeUndefined(); + expect(maybeParameterizeRemixRoute('/')).toBe('/'); }); it('should handle complex nested dynamic routes', () => { @@ -246,10 +246,10 @@ describe('maybeParameterizeRemixRoute', () => { describe('realistic Remix patterns', () => { it.each([ - ['/', undefined], - ['/about', undefined], - ['/contact', undefined], - ['/blog/posts', undefined], + ['/', '/'], + ['/about', '/about'], + ['/contact', '/contact'], + ['/blog/posts', '/blog/posts'], ['/users/123', '/users/:id'], ['/users/john-doe', '/users/:id'],