Which SDK are you using?
@sentry/react-router
SDK Version
10.70.0
Problem
The Node entry of @sentry/react-router statically re-exports the Vite plugin and its build-time helpers:
// build/esm/index.server.js:10-12
export { sentryReactRouter } from './vite/plugin.js';
export { sentryOnBuildEnd } from './vite/buildEnd/handleOnBuildEnd.js';
export { makeConfigInjectorPlugin } from './vite/makeConfigInjectorPlugin.js';
Because these are static ESM re-exports, a plain server-side import * as Sentry from "@sentry/react-router" — the exact thing the docs tell you to put in instrument.server.mjs and load with --import — eagerly pulls @sentry/vite-plugin, @sentry/cli and glob into the runtime import graph of the production server.
These packages are only meaningful at build time (source map upload). For deployments that don't upload source maps they are pure dead weight, and even for those that do, they have no business being resolved inside the running server process.
Measurements
Node v22.22.3, same machine, warm FS cache, n=3 each:
| import |
time |
@sentry/react-router (node entry) |
251 / 263 / 272 ms |
@sentry/node |
230 / 237 / 253 ms |
So the marginal cost is roughly 20 ms of cold start. Small, but it is paid on every container start, and it is entirely avoidable.
Inspecting module._cache after await import('@sentry/react-router') confirms the build-time graph is actually loaded — 5 files / ~32 KB from @sentry/cli alone, e.g.:
node_modules/@sentry/cli/js/index.js
node_modules/@sentry/cli/js/helper.js
node_modules/@sentry/cli/js/releases/index.js
Why it can't be worked around
The package exports map only has two entries:
{
"./package.json": "./package.json",
".": { "node": { "import": "./build/esm/index.server.js", ... } },
"./cloudflare": { ... }
}
There is no ./vite-style subpath, so a consumer cannot opt out by importing a narrower entry point. Bundling isn't an option either: bundling the SDK breaks OpenTelemetry's auto-instrumentation, since the patch targets become copies inside the bundle. Reaching for @sentry/node directly loses the React Router flavored init from ./server/sdk.js.
The same graph is also pulled in through wrapSentryHandleRequest, which is imported from application code (entry.server.tsx), so it is not limited to the instrumentation file.
Suggested fix
Move the Vite/build-time exports behind a dedicated subpath, e.g. @sentry/react-router/vite, so vite.config.ts imports sentryReactRouter / sentryOnBuildEnd from there and the server entry stops re-exporting them. This mirrors what several other Sentry meta-packages already do and would be a small, mechanical change.
If keeping them on the root entry is required for backwards compatibility, making the re-exports lazy (or marking the subgraph sideEffects: false so bundlers can drop it) would at least help the bundled case — though it would not help the --import case, which is the documented setup.
Expected Result
A server-side import * as Sentry from "@sentry/react-router" should not resolve @sentry/vite-plugin, @sentry/cli or glob.
Actual Result
They are eagerly loaded into the running server's module graph.
Which SDK are you using?
@sentry/react-routerSDK Version
10.70.0
Problem
The Node entry of
@sentry/react-routerstatically re-exports the Vite plugin and its build-time helpers:Because these are static ESM re-exports, a plain server-side
import * as Sentry from "@sentry/react-router"— the exact thing the docs tell you to put ininstrument.server.mjsand load with--import— eagerly pulls@sentry/vite-plugin,@sentry/cliandglobinto the runtime import graph of the production server.These packages are only meaningful at build time (source map upload). For deployments that don't upload source maps they are pure dead weight, and even for those that do, they have no business being resolved inside the running server process.
Measurements
Node v22.22.3, same machine, warm FS cache, n=3 each:
@sentry/react-router(node entry)@sentry/nodeSo the marginal cost is roughly 20 ms of cold start. Small, but it is paid on every container start, and it is entirely avoidable.
Inspecting
module._cacheafterawait import('@sentry/react-router')confirms the build-time graph is actually loaded — 5 files / ~32 KB from@sentry/clialone, e.g.:Why it can't be worked around
The package
exportsmap only has two entries:{ "./package.json": "./package.json", ".": { "node": { "import": "./build/esm/index.server.js", ... } }, "./cloudflare": { ... } }There is no
./vite-style subpath, so a consumer cannot opt out by importing a narrower entry point. Bundling isn't an option either: bundling the SDK breaks OpenTelemetry's auto-instrumentation, since the patch targets become copies inside the bundle. Reaching for@sentry/nodedirectly loses the React Router flavoredinitfrom./server/sdk.js.The same graph is also pulled in through
wrapSentryHandleRequest, which is imported from application code (entry.server.tsx), so it is not limited to the instrumentation file.Suggested fix
Move the Vite/build-time exports behind a dedicated subpath, e.g.
@sentry/react-router/vite, sovite.config.tsimportssentryReactRouter/sentryOnBuildEndfrom there and the server entry stops re-exporting them. This mirrors what several other Sentry meta-packages already do and would be a small, mechanical change.If keeping them on the root entry is required for backwards compatibility, making the re-exports lazy (or marking the subgraph
sideEffects: falseso bundlers can drop it) would at least help the bundled case — though it would not help the--importcase, which is the documented setup.Expected Result
A server-side
import * as Sentry from "@sentry/react-router"should not resolve@sentry/vite-plugin,@sentry/cliorglob.Actual Result
They are eagerly loaded into the running server's module graph.