From ad36553375982c6052e39db5e20953999318b433 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 31 Aug 2026 15:01:37 -0400 Subject: [PATCH 1/5] feat(remix): Register a route provider outside the tracing integration `maybeParameterizeRemixRoute` is already a pure matcher over the route manifest the Vite plugin injects at build time, it was just only reachable from the pageload and navigation instrumentation. Registered from `init()` rather than a tracing integration, because the manifest is on the global object before `Sentry.init` runs. Route parameterization no longer depends on tracing being enabled. --- packages/remix/src/client/routeProvider.ts | 13 +++++ packages/remix/src/client/sdk.ts | 12 ++++- .../remix/test/client/routeProvider.test.ts | 50 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 packages/remix/src/client/routeProvider.ts create mode 100644 packages/remix/test/client/routeProvider.test.ts diff --git a/packages/remix/src/client/routeProvider.ts b/packages/remix/src/client/routeProvider.ts new file mode 100644 index 000000000000..7adca26a4480 --- /dev/null +++ b/packages/remix/src/client/routeProvider.ts @@ -0,0 +1,13 @@ +import type { RouteProvider } from '@sentry/core'; +import { createUrlRouteProvider } from '@sentry/core'; +import { maybeParameterizeRemixRoute } from './remixRouteParameterization'; + +/** + * A route provider backed by the route manifest the Vite plugin injects at build time. + * + * The manifest is on the global object before `Sentry.init` runs, so this needs no router and no + * tracing integration: registering it is what lets anything else in the SDK name a route. + */ +export function createRemixRouteProvider(): RouteProvider { + return createUrlRouteProvider(url => maybeParameterizeRemixRoute(url.pathname)); +} diff --git a/packages/remix/src/client/sdk.ts b/packages/remix/src/client/sdk.ts index d09ec6786ce4..32a72eb9e580 100644 --- a/packages/remix/src/client/sdk.ts +++ b/packages/remix/src/client/sdk.ts @@ -1,7 +1,8 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, setRouteProvider } from '@sentry/core'; import { init as reactInit } from '@sentry/react'; import type { RemixOptions } from '../utils/remixOptions'; +import { createRemixRouteProvider } from './routeProvider'; /** * Initializes the Remix SDK. @@ -16,5 +17,12 @@ export function init(options: RemixOptions): Client | undefined { applySdkMetadata(opts, 'remix', ['remix', 'react']); - return reactInit(opts); + const client = reactInit(opts); + + // Registered here rather than from the tracing integration so route parameterization does not + // depend on tracing: the manifest is injected at build time, so anything that needs a route name + // (bfcache metrics, web vitals) can resolve one even with tracing disabled. + setRouteProvider(createRemixRouteProvider(), client); + + return client; } diff --git a/packages/remix/test/client/routeProvider.test.ts b/packages/remix/test/client/routeProvider.test.ts new file mode 100644 index 000000000000..d92edc120cba --- /dev/null +++ b/packages/remix/test/client/routeProvider.test.ts @@ -0,0 +1,50 @@ +import { GLOBAL_OBJ } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createRemixRouteProvider } from '../../src/client/routeProvider'; + +const globalWithInjectedManifest = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + _sentryRemixRouteManifest: string | undefined; +}; + +const MANIFEST = JSON.stringify({ + staticRoutes: [{ path: '/about' }], + dynamicRoutes: [{ path: '/users/:id', regex: '^/users/([^/]+)$', paramNames: ['id'] }], +}); + +let originalDocument: unknown; + +describe('createRemixRouteProvider', () => { + beforeEach(() => { + globalWithInjectedManifest._sentryRemixRouteManifest = MANIFEST; + originalDocument = (GLOBAL_OBJ as { document?: unknown }).document; + // `resolveCurrentRoute` reads `document.location.href`. + (GLOBAL_OBJ as { document?: unknown }).document = { location: { href: 'https://example.com/users/42' } }; + }); + + afterEach(() => { + globalWithInjectedManifest._sentryRemixRouteManifest = undefined; + (GLOBAL_OBJ as { document?: unknown }).document = originalDocument; + }); + + it('parameterizes a URL from the build-time manifest', () => { + expect(createRemixRouteProvider().resolveRoute(new URL('https://example.com/users/42'))).toBe('/users/:id'); + }); + + it('resolves a static route', () => { + expect(createRemixRouteProvider().resolveRoute(new URL('https://example.com/about'))).toBe('/about'); + }); + + it('resolves the current route from the document location', () => { + expect(createRemixRouteProvider().resolveCurrentRoute()).toBe('/users/:id'); + }); + + it('returns undefined for a URL the manifest does not know', () => { + expect(createRemixRouteProvider().resolveRoute(new URL('https://example.com/nope/deep'))).toBeUndefined(); + }); + + it('returns undefined when the manifest was never injected', () => { + globalWithInjectedManifest._sentryRemixRouteManifest = undefined; + + expect(createRemixRouteProvider().resolveRoute(new URL('https://example.com/users/42'))).toBeUndefined(); + }); +}); From 798a41c9ce500cde8824db7bffdbe26385a00eb4 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 24 Sep 2026 14:03:01 -0400 Subject: [PATCH 2/5] ref(remix): Import the route provider API from `@sentry/core/browser` --- packages/remix/src/client/routeProvider.ts | 4 ++-- packages/remix/src/client/sdk.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/remix/src/client/routeProvider.ts b/packages/remix/src/client/routeProvider.ts index 7adca26a4480..4ad4ed21e93f 100644 --- a/packages/remix/src/client/routeProvider.ts +++ b/packages/remix/src/client/routeProvider.ts @@ -1,5 +1,5 @@ -import type { RouteProvider } from '@sentry/core'; -import { createUrlRouteProvider } from '@sentry/core'; +import type { RouteProvider } from '@sentry/core/browser'; +import { createUrlRouteProvider } from '@sentry/core/browser'; import { maybeParameterizeRemixRoute } from './remixRouteParameterization'; /** diff --git a/packages/remix/src/client/sdk.ts b/packages/remix/src/client/sdk.ts index 32a72eb9e580..93138fac04d6 100644 --- a/packages/remix/src/client/sdk.ts +++ b/packages/remix/src/client/sdk.ts @@ -1,5 +1,6 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, setRouteProvider } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; +import { setRouteProvider } from '@sentry/core/browser'; import { init as reactInit } from '@sentry/react'; import type { RemixOptions } from '../utils/remixOptions'; import { createRemixRouteProvider } from './routeProvider'; From 4884a18d97f217fb425f153772fa0eaaadaacb78 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 24 Sep 2026 14:35:55 -0400 Subject: [PATCH 3/5] ref(remix): Import the route provider API from `@sentry/react` --- packages/remix/src/client/routeProvider.ts | 4 ++-- packages/remix/src/client/sdk.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/remix/src/client/routeProvider.ts b/packages/remix/src/client/routeProvider.ts index 4ad4ed21e93f..5cc470a9c5a0 100644 --- a/packages/remix/src/client/routeProvider.ts +++ b/packages/remix/src/client/routeProvider.ts @@ -1,5 +1,5 @@ -import type { RouteProvider } from '@sentry/core/browser'; -import { createUrlRouteProvider } from '@sentry/core/browser'; +import type { RouteProvider } from '@sentry/react'; +import { createUrlRouteProvider } from '@sentry/react'; import { maybeParameterizeRemixRoute } from './remixRouteParameterization'; /** diff --git a/packages/remix/src/client/sdk.ts b/packages/remix/src/client/sdk.ts index 93138fac04d6..40d1cd459041 100644 --- a/packages/remix/src/client/sdk.ts +++ b/packages/remix/src/client/sdk.ts @@ -1,7 +1,6 @@ import type { Client } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; -import { setRouteProvider } from '@sentry/core/browser'; -import { init as reactInit } from '@sentry/react'; +import { init as reactInit, setRouteProvider } from '@sentry/react'; import type { RemixOptions } from '../utils/remixOptions'; import { createRemixRouteProvider } from './routeProvider'; From a9e340cd3eb9daf94147906aa2fe46f695161f90 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 24 Sep 2026 15:57:24 -0400 Subject: [PATCH 4/5] ref(remix): Pass the route provider as the `routeProvider` option --- packages/remix/src/client/sdk.ts | 14 +++++--------- packages/remix/test/index.client.test.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/remix/src/client/sdk.ts b/packages/remix/src/client/sdk.ts index 40d1cd459041..46bf5a92c501 100644 --- a/packages/remix/src/client/sdk.ts +++ b/packages/remix/src/client/sdk.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; -import { init as reactInit, setRouteProvider } from '@sentry/react'; +import { init as reactInit } from '@sentry/react'; import type { RemixOptions } from '../utils/remixOptions'; import { createRemixRouteProvider } from './routeProvider'; @@ -11,18 +11,14 @@ import { createRemixRouteProvider } from './routeProvider'; */ export function init(options: RemixOptions): Client | undefined { const opts = { + // The manifest is injected at build time, so route parameterization works from `init` on, even with + // tracing disabled. + routeProvider: createRemixRouteProvider(), ...options, environment: options.environment || process.env.NODE_ENV, }; applySdkMetadata(opts, 'remix', ['remix', 'react']); - const client = reactInit(opts); - - // Registered here rather than from the tracing integration so route parameterization does not - // depend on tracing: the manifest is injected at build time, so anything that needs a route name - // (bfcache metrics, web vitals) can resolve one even with tracing disabled. - setRouteProvider(createRemixRouteProvider(), client); - - return client; + return reactInit(opts); } diff --git a/packages/remix/test/index.client.test.ts b/packages/remix/test/index.client.test.ts index 5b39e7736e29..1e0e26343494 100644 --- a/packages/remix/test/index.client.test.ts +++ b/packages/remix/test/index.client.test.ts @@ -43,6 +43,17 @@ describe('Client init()', () => { ); }); + it('passes the Remix route provider unless the user passed one', () => { + init({}); + expect(reactInit).toHaveBeenLastCalledWith( + expect.objectContaining({ routeProvider: expect.objectContaining({ resolveRoute: expect.any(Function) }) }), + ); + + const routeProvider = { resolveRoute: () => '/custom', resolveCurrentRoute: () => '/custom' }; + init({ routeProvider }); + expect(reactInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider })); + }); + it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); From 6276e2563c52f4a0a0e8333f24707e1fcff6c640 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 24 Sep 2026 17:12:02 -0400 Subject: [PATCH 5/5] test(remix): Cover route resolution through the route provider --- .../app/routes/route-provider.$id.tsx | 12 ++++++++++++ .../tests/route-provider.test.ts | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx new file mode 100644 index 000000000000..6134dbb17ab0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/remix'; +import { useEffect, useState } from 'react'; + +export default function RouteProvider() { + const [route, setRoute] = useState(); + + useEffect(() => { + setRoute(Sentry.resolveCurrentRoute() ?? 'unresolved'); + }, []); + + return
{route}
; +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts new file mode 100644 index 000000000000..a99e7ec07314 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from '@playwright/test'; + +// The route provider is backed by the manifest the Vite plugin injects into the client bundle, so this +// fails if that manifest never reaches the browser. +test('resolves the parameterized route through the route provider', async ({ page }) => { + await page.goto('/route-provider/123'); + + await expect(page.locator('#resolved-route')).toHaveText('/route-provider/:id'); +});