Skip to content

Support Remix 3 (v3 rewrite) in @sentry/remix #23901

Description

@mydea

Summary

Remix 3 published its first release candidate on 2026-08-31. It is a ground-up rewrite, not a version bump: it drops React entirely (its UI runtime is a fork of Preact with its own component model — no virtual DOM, explicit updates instead of hooks), drops React Router, and rebuilds around the Fetch API (Request/Response) so it runs on any Fetch runtime (Node, Deno, Bun). It ships as a new set of composable packages, none of which are the @remix-run/* packages our current SDK targets.

This issue tracks adding Remix 3 support to @sentry/remix.

Why the current SDK is fully incompatible

Everything in @sentry/remix today assumes the Remix 2 internals:

  • Server (orchestrion): our tracing channels target @remix-run/server-runtime internals — requestHandler, matchServerRoutes, callRouteLoader, callRouteAction — version-gated >=2.0.0 <3 (packages/server-utils/src/orchestrion/config/remix.ts). None of these exist in v3; there is no @remix-run/server-runtime.
  • Server (monkey-patch fallback): instrumentServer.ts fills createRequestHandler from @remix-run/server-runtime — same non-existent module.
  • Client: we are a wrapper around @sentry/react (withSentry React error boundaries, React-Router browser tracing / route parameterization). With React gone, the entire client is inapplicable.
  • Deps: peer deps are @remix-run/{node,react,server-runtime} 2.x + react 18.x; all irrelevant to v3.

Relevant Remix 3 packages

  • @remix-run/fetch-router — composable Fetch-API router with router.mount() and a middleware system
  • @remix-run/node-fetch-server — Node adapter for Fetch-API servers
  • @remix-run/ui — the new reconciler + component model (the Preact fork)
  • a suite of middleware packages (logger-middleware, session-middleware, cors-middleware, …)

No diagnostics_channel usage was found in the framework, so there is no native channel to subscribe to.

Proposed approach

Packaging — keep it in @sentry/remix

One package covers both majors. The two runtimes share nothing but never collide: orchestrion configs are gated by the module + version they patch, so the v2 config (@remix-run/server-runtime >=2 <3) and a new v3 config (@remix-run/fetch-router / @remix-run/node-fetch-server, >=3 / 0.x during RC) each no-op on the other major. End-user experience stays: install @sentry/remix, done. Package exports stay index.server / index.client; we branch internally on which framework is present. Peer deps widen so both the v2 and v3 @remix-run/* sets are optional.

Server — orchestrion auto-injects a Sentry middleware (zero config)

Reuse the existing module-injected-registration machinery (registrationOnly in packages/server-utils/src/orchestrion/config/registration-only.ts, bundler/moduleInjectedTransform.ts): orchestrion transforms @remix-run/fetch-router's router construction at load time and prepends our Sentry middleware to every router — no manual wiring by the user. Orchestrion's role is auto-registration; the span logic lives in normal SDK code (the middleware).

The middleware — (context, next) => Response, with context.request/url/typed params and the ability to run before and after await next() — handles:

  • open the http.server span, run next() inside it, set status from the returned Response, close it;
  • name the transaction from the matched route pattern (low-cardinality / span-streaming logic we already have);
  • continueTrace from incoming headers + inject sentry-trace / baggage.

Error capture: try/catch around next()captureException → rethrow. Open question (spike): whether every handler throw surfaces at the middleware boundary or gets converted to an error Response upstream first. If middleware misses some, add a second orchestrion patch at the node-fetch-server request-handler choke point (the v3 analog of wrapping createRequestHandler today).

Manual fallback: also export the middleware (e.g. Sentry.remixMiddleware()) for bundlers/runtimes where orchestrion injection doesn't fire — matches our usual "auto, with a manual escape hatch" pattern.

Sub-request spans: out of scope for v1. No loader/action split exists in v3 (routes are Fetch handlers returning Response), so the middleware request span is the whole server story initially.

Client — build on @sentry/browser

No React → @sentry/react is out. @remix-run/ui gives us:

  • Wrappable entry run(options) (returns app with app.ready()) — wrap to init Sentry and catch init/hydration errors.
  • Navigation via the standard Navigation API — the runtime intercepts same-origin links/forms through window.navigation, so pageload + navigation spans subscribe to navigate events instead of patching History. Route parameterization from the router's matched pattern. Strongest reuse point on the client.
  • Missing: no documented onError / component error boundary in run()/app. Client error capture for v1 leans on @sentry/browser global onerror/unhandledrejection handlers + the run() wrap. Whether the ui reconciler exposes a render-error hook is a spike item; if not, global handlers are the v1 answer and we document the gap.

Spike checklist (against the RC, before committing)

  1. Does a fetch-router middleware try/catch around next() see all handler throws, or do some become error Responses upstream? → decides whether we also need the node-fetch-server wrap for errors.
  2. Where to inject in fetch-router so every router (incl. router.mount() sub-routers) gets the middleware.
  3. Confirm window.navigation navigate events give clean start/end + destination URL + matched route for span naming; check pageload vs. SPA (remix/spa render()) paths.
  4. Any render-error hook in the ui reconciler for component-level capture; otherwise global-handlers-only for v1.
  5. Confirm v3 package/version identifiers for the orchestrion versionRange and optional peer deps (still 0.x RC).

v1 scope

In: server http.server span + trace propagation + route naming (auto-injected middleware); server error capture; client init + pageload/navigation tracing (Navigation API); client global error capture.

Out: sub-request spans; component-level client error boundaries (pending a hook); anything depending on APIs still moving in the RC.

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions