Skip to content

feat(react): Add @sentry/react/react-router entry with automatic React Router hooks - #24014

Merged
mydea merged 16 commits into
developfrom
fn/react-router-fox
Sep 7, 2026
Merged

feat(react): Add @sentry/react/react-router entry with automatic React Router hooks#24014
mydea merged 16 commits into
developfrom
fn/react-router-fox

Conversation

@mydea

@mydea mydea commented Sep 3, 2026

Copy link
Copy Markdown
Member

Adds a new @sentry/react/react-router entry point so React Router users no longer have to thread useLocation/useNavigationType/matchRoutes/createRoutesFromChildren into reactRouterBrowserTracingIntegration — the entry pulls them from react-router and supplies them as defaults. react-router is declared as an optional peer dependency (v6/v7/v8); the import lives only in the separate entry, so the main @sentry/react barrel stays free of react-router for plain-React and CJS/SSR consumers.

Under the hood, the React Router instrumentation was reworked to stop sharing state through ambient module-scope variables. All integration configuration (the router hooks, stripBasename, enableAsyncRouteHandlers, lazyRouteTimeout, lazyRouteManifest, basename, and the navigation-instrumentation flag) now lives in a single per-client ReactRouterConfig held in a WeakMap<Client, …> and threaded explicitly through the call chain.

A direct consequence, for every @sentry/react routing setup — not just the new entry — is that the order in which you add the browser tracing integration and wrap your routes no longer matters. Previously Sentry.init() had to run before your routes were wrapped (withSentryReactRouterV6Routing, wrapReactRouterRouting, wrapUseRoutes, wrapCreateBrowserRouter, …), and wrapping earlier silently produced uninstrumented routes. Wrapping now reads its config when the router renders (or is created), so wrapping at module-evaluation time before Sentry.init() still instruments correctly.

Root cause

The pre-existing design shared the integration's config with the routing wrappers through module-scope globals, which were per-module rather than per-client (so multiple Sentry clients clobbered each other) and coupled wrap-order to init-order (wrapping routes before Sentry.init() produced uninstrumented components).

Decisions

  • A separate entry point, not a main-barrel auto-import. @sentry/react ships CJS and is consumed server-side (Next.js, SSR, Jest). A static import from 'react-router' in the main barrel would crash every consumer that doesn't have react-router installed; a dynamic import would violate the repo's static-analyzability policy. A dedicated entry (the same pattern @sentry/solid/solidrouter already uses) keeps the main barrel import-free.
  • Per-client config instead of module globals. Moving config onto the client (following the existing WeakMap<Client> pattern in the same file) makes it per-client-correct, makes basename per-router via a shallow copy instead of a last-writer-wins global, and — because the wrappers read config at render/creation time via a Rules-of-Hooks-safe outer/inner split — makes wrapping order-independent w.r.t. Sentry.init() for all routing APIs.
  • initializeRouterUtils/_stripBasename and the CLIENTS_WITH_INSTRUMENT_NAVIGATION WeakSet were folded into the threaded config; the integration is composed with extendIntegration.
  • useEffect is no longer used internally (React's own effect hook is used); it stays an optional, ignored option for backwards compatibility.

The existing @sentry/react unit suite passes unchanged; new e2e apps for React Router v6, v7 and v8 (react-router-{6,7,8}-router-entry) exercise the zero-config setup end-to-end and type-check the @sentry/react/react-router API against each major via tsc --noEmit; a v11 MIGRATION.md entry documents the new entry point and the order-independence.

@mydea
mydea force-pushed the fn/react-router-fox branch from 905593c to 028914a Compare September 3, 2026 13:02

@s1gr1d s1gr1d left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only confusing thing is the v6 import stuff. This might be tricky for users.

Comment thread packages/react/src/router.ts Outdated
Comment thread MIGRATION.md Outdated
Comment thread MIGRATION.md Outdated
The existing `@sentry/react` API is unchanged and keeps working; passing the hooks there is now optional too (`useEffect` in particular is no longer used and can be omitted).

- `@sentry/core` now exports only isomorphic code. Browser-only exports live on `@sentry/core/browser` and server-only exports on `@sentry/core/server`, and neither subpath re-exports the shared surface any more. This keeps server-only code (HTTP instrumentation, ANR, postgres and sql helpers) out of browser bundles. Most of these APIs are also re-exported by the platform SDKs (`@sentry/node`, `@sentry/browser`, ...), which is unchanged, so this only affects code importing straight from `@sentry/core`. TypeScript reports it as `has no exported member`.
Additionally — for **every** `@sentry/react` routing setup, not just the new entry — the order in which you add the browser tracing integration and wrap your routes no longer matters. Previously `Sentry.init()` had to run before your routes were wrapped (e.g. `withSentryReactRouterV6Routing`, `wrapReactRouterRouting`, `wrapUseRoutes`, `wrapCreateBrowserRouter`); wrapping earlier silently produced uninstrumented routes. Wrapping now reads its configuration when the router renders (or is created), so wrapping at module-evaluation time — before `Sentry.init()` — still instruments correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part can be shortened, it's a bit too detailed for users. Ideally, just say the order does not matter anymore and it does not need to be wrapped after init.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shortended it!

Comment thread MIGRATION.md
The `wrapReactRouterRouting`, `wrapUseRoutes`, `wrapCreateBrowserRouter` and `wrapCreateMemoryRouter` helpers are re-exported from `@sentry/react/router` as well.

## 3. Removed APIs
This entry requires `react-router` to be resolvable — it is declared as an optional peer dependency and supports React Router v6, v7 and v8. If you are on React Router v6 with only `react-router-dom` installed, either add `react-router` as a dependency or keep importing `reactRouterBrowserTracingIntegration` from `@sentry/react` and pass the hooks explicitly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react-router is a dependency of react-router-dom (source), so people might not need to install it (it's installed anyway), but pnpm might be stricter with that.

But react-router in v6 also mentions that nothing should be directly imported from there (but I guess thi only applies to users): https://github.com/remix-run/react-router/tree/v6/packages/react-router

From v7, react-router is the package to import from: https://reactrouter.com/7.18.3/upgrading/v6#upgrade-to-v7

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s1gr1d added e2e tests for react router v6 and v8 as well. in v6 I on purpose did not add the react-router v6 dependency directly and it seems to work, though this may or may not work depending on package manager etc. I think it's fine to recommend people to add this dependency (does not hurt at least) but it should mostly also work without!

@mydea
mydea force-pushed the fn/react-router-fox branch from 28ab460 to be4a22d Compare September 4, 2026 07:51
Comment thread dev-packages/e2e-tests/test-applications/react-router-7-router-entry/package.json Outdated
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 28.8 kB - -
@sentry/browser - with treeshaking flags 27.11 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27 kB - -
@sentry/browser (incl. Tracing) 49.22 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 49.22 kB - -
@sentry/browser (incl. Tracing, Profiling) 52.12 kB - -
@sentry/browser (incl. Tracing, Replay) 88.76 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.95 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 93.44 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 106.37 kB - -
@sentry/browser (incl. Feedback) 46.3 kB - -
@sentry/browser (incl. sendFeedback) 33.86 kB - -
@sentry/browser (incl. FeedbackAsync) 38.97 kB - -
@sentry/browser (incl. Metrics) 29.82 kB - -
@sentry/browser (incl. Logs) 30.09 kB - -
@sentry/browser (incl. Metrics & Logs) 30.75 kB - -
@sentry/react 30.55 kB - -
@sentry/react (incl. Tracing) 51.56 kB +0.27% +138 B 🔺
@sentry/vue 36.05 kB - -
@sentry/vue (incl. Tracing) 51.48 kB - -
@sentry/svelte 28.83 kB - -
CDN Bundle 30.55 kB - -
CDN Bundle (incl. Tracing) 49.74 kB - -
CDN Bundle (incl. Logs, Metrics) 32.82 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.7 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.48 kB - -
CDN Bundle (incl. Tracing, Replay) 87.29 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 89.18 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 93.22 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 95.21 kB - -
CDN Bundle - uncompressed 90.46 kB - -
CDN Bundle (incl. Tracing) - uncompressed 148.17 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.03 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 154.14 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.29 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 267.76 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 273.71 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 281.46 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 287.4 kB - -
@sentry/nextjs (client) 54.03 kB - -
@sentry/sveltekit (client) 49.65 kB - -
@sentry/core/server 38.63 kB - -
@sentry/core/browser 13.55 kB - -
@sentry/node 127.63 kB +0.02% +20 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.58 kB - -
@sentry/node - without tracing 88.71 kB +0.03% +20 B 🔺
@sentry/node - without channel injection 106.9 kB +0.02% +18 B 🔺
@sentry/aws-serverless 97.1 kB +0.03% +25 B 🔺
@sentry/cloudflare (withSentry) - minified 201.91 kB - -
@sentry/cloudflare (withSentry) 502.4 kB - -

View base workflow run

@mydea
mydea force-pushed the fn/react-router-fox branch from 81f5f97 to 751e21f Compare September 4, 2026 10:56

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/react/test/react-router.test.tsx
@mydea
mydea force-pushed the fn/react-router-fox branch 2 times, most recently from cdf0bcf to b597f2c Compare September 4, 2026 11:11
@mydea
mydea marked this pull request as ready for review September 4, 2026 11:56
@mydea
mydea requested a review from a team as a code owner September 4, 2026 11:56
@mydea
mydea requested review from chargome and s1gr1d and removed request for a team September 4, 2026 11:56
Comment thread packages/react/package.json Outdated
Comment thread packages/react/package.json
Comment thread packages/react/package.json
Comment thread packages/react/package.json Outdated
Comment on lines +815 to +819
if (!getRouterConfig(getClient())) {
return <UninstrumentedRoutes routes={routes} locationArg={locationArg} />;
}
return <SentryRoutes routes={routes} locationArg={locationArg} />;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think if someone call initat a later point than we recommend, then this will entirely wipe the full component tree?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we avoid this? I would tend to say that's "fine" (meaning, what else should we do/should happen?), but will def. defer to your POV on this as you 've got way more react experience xD

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually changed this to hopefully fix this better

@mydea
mydea force-pushed the fn/react-router-fox branch from 2223ba8 to 46ca2f9 Compare September 7, 2026 08:15
@mydea mydea changed the title feat(react): Add @sentry/react/router entry with automatic React Router hooks feat(react): Add @sentry/react/react-router entry with automatic React Router hooks Sep 7, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 46ca2f9. Configure here.

Comment thread packages/react/src/reactrouter-compat-utils/instrumentation.tsx Outdated
Comment thread packages/react/src/reactrouter-compat-utils/instrumentation.tsx Outdated

@chargome chargome left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the changes!

Comment thread packages/react/package.json Outdated
@mydea
mydea enabled auto-merge (squash) September 7, 2026 11:48
mydea and others added 9 commits September 7, 2026 14:26
…er hooks

Add a new `@sentry/react/router` subpath export that statically imports the
required hooks from `react-router` and exposes a
`reactRouterBrowserTracingIntegration()` variant that supplies them as
defaults, so consumers no longer have to pass `useLocation`,
`useNavigationType`, `createRoutesFromChildren` and `matchRoutes` themselves
(they can still override via options). `react-router` is declared as an
optional peer dependency (6.x || 7.x || 8.x); the import lives only in the
separate entry so the main barrel stays free of `react-router` for
plain-React / CJS consumers.

Also tighten the router instrumentation to only require the hooks each
function actually uses, and make `useEffect` optional: it was never used
internally (React's own effect hook is used instead), so it is no longer part
of any readiness guard. The option is kept for backwards compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjLdAGt9CHRbbJyBSCnduV
Make the router `matchRoutes` (and the render-time hooks) flow as explicit
parameters instead of being read from module scope everywhere. Only the four
entrypoint wrapper factories now read the module-scope hook variables (as a
fallback), resolving `hooks?.X ?? _X`; every downstream helper receives
`matchRoutes` as a required argument. The module-scope `_matchRoutes` in
`utils.ts` is removed entirely.

Each public wrapper (`wrapReactRouterRouting`, `wrapUseRoutes`,
`wrapCreateBrowserRouter`, `wrapCreateMemoryRouter` and their v6/v7 aliases)
gains an optional `hooks` argument to pass the hooks directly. The
`@sentry/react/router` entry uses this to expose defaulted wrapper variants
that bake in the `react-router` hooks, matching its zero-config integration.

Pure refactor - no behavior change; the existing test suite passes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjLdAGt9CHRbbJyBSCnduV
…odule scope

Replace all the ambient module-scope variables the React Router
instrumentation used to share between the browser tracing integration and the
routing wrappers with a single per-client `ReactRouterConfig`, held in a
`WeakMap<Client, ReactRouterConfig>` and set once in the integration's
`setup()`. A single `config` object is then threaded through the call chain
(captured in closures for the lazy/async paths), replacing the previous
per-hook threading.

This removes `_matchRoutes`, `_useLocation`, `_useNavigationType`,
`_createRoutesFromChildren`, `_enableAsyncRouteHandlers`, `_lazyRouteTimeout`,
`_lazyRouteManifest`, `_basename`, `_stripBasename`/`initializeRouterUtils`,
and the `CLIENTS_WITH_INSTRUMENT_NAVIGATION` WeakSet. Config is now per-client
(correct for multiple clients) rather than last-writer-wins global state, and
`basename` is copied per router so one router's basename can't leak into
another sharing the same client.

The wrappers read the config from the client at render/creation time via a
small outer/inner split, which is Rules-of-Hooks safe and makes wrapping
order-independent: wrapping routes before `Sentry.init()` runs now still
instruments once the app renders. The `@sentry/react/router` wrappers become
plain re-exports again (they read the hooks the entry's integration stores on
the client). The integration is composed with `extendIntegration`, and the
now-unused per-hook `hooks?` params are dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjLdAGt9CHRbbJyBSCnduV
Add the v11 MIGRATION.md entry for the new `@sentry/react/router` entry point,
and `react-router-7-router-entry`, a React Router v7 SPA that configures tracing
purely through it - `reactRouterBrowserTracingIntegration()` is called with no
arguments and `wrapReactRouterRouting` comes from the same entry. The span tests
assert pageload and navigation transactions still get parameterized route names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjLdAGt9CHRbbJyBSCnduV
Co-authored-by: Sigrid <32902192+s1gr1d@users.noreply.github.com>
…d v8

Add `react-router-6-router-entry` (React 18) and `react-router-8-router-entry`
(React 19) alongside the existing v7 app, all configured purely through
`@sentry/react/router` (zero-arg integration + `wrapReactRouterRouting`). Each
has the full span/error/navigation-trace-propagation suite and runs
`tsc --noEmit` in `test:assert` so CI verifies the `@sentry/react/router` types
match against each React Router major (tsconfig uses `moduleResolution: bundler`
so the subpath's exports-map types resolve).

The v6 app depends only on `react-router-dom` (not `react-router` directly) -
this is the common real-world v6 setup and exercises that the entry's
`react-router` import still resolves via the copy `react-router-dom` pulls in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjLdAGt9CHRbbJyBSCnduV
mydea and others added 7 commits September 7, 2026 14:26
The v7 router-entry app only ran Playwright and its tsconfig used
`moduleResolution: "node"`, which ignores the package `exports` map and so
cannot resolve the `@sentry/react/router` subpath types - meaning it never
type-checked the new API, unlike the v6/v8 apps. Switch it to
`moduleResolution: "bundler"` and run `tsc --noEmit` in `test:assert`, matching
the other two. Verified locally that all three (v6/v7/v8) type-check against
their respective React Router major.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the `wrapReactRouterRouting(Routes)` call into a separate `sentry-routes.tsx`
module that `main.tsx` imports, so it runs at module-evaluation time BEFORE
`Sentry.init()` is called. This exercises the order-independence of the
`@sentry/react/router` setup end-to-end: the existing pageload/navigation span
tests still expect parameterized route names, which only holds if wrapping
before init still instruments (the wrapper reads its config at render time,
after init). v6 and v8 keep the normal init-first order, so both orders are
covered across the apps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the `@sentry/react/router` subpath export to `@sentry/react/react-router`
(source `src/router.ts` -> `src/react-router.ts`) and adds a `typesVersions`
mapping so the subpath's types resolve under classic `moduleResolution: node`,
which ignores the `exports` map.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Charly Gomez <charly.gomez1310@gmail.com>
@mydea
mydea force-pushed the fn/react-router-fox branch from 3a0fc3c to bca9fd7 Compare September 7, 2026 12:26
@mydea
mydea merged commit 93b61c8 into develop Sep 7, 2026
307 of 308 checks passed
@mydea
mydea deleted the fn/react-router-fox branch September 7, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants