Skip to content

ref(server-utils): Move ServerRuntimeClient, node stack parser and server-only utils out of core - #23833

Draft
mydea wants to merge 11 commits into
developfrom
fn/move-server-runtime-client-to-server-utils
Draft

mydea wants to merge 11 commits into
developfrom
fn/move-server-runtime-client-to-server-utils

Conversation

@mydea

@mydea mydea commented Sep 1, 2026

Copy link
Copy Markdown
Member

Continues slimming @sentry/core down to its isomorphic surface by moving the remaining server-only APIs into @sentry/server-utils.

Moved out of core:

  • flushIfServerless + vercelWaitUntil (used by the meta-framework SDKs)
  • trpcMiddleware
  • callFrameToStackFrame / watchdogTimer (the anr worker helpers)
  • loadModule (split out of utils/node; isNodeEnv stays, since core depends on it via isBrowser)
  • the base ServerRuntimeClient (with ServerRuntimeClientOptions / ServerRuntimeOptions) and the node stack-trace parser (nodeStackLineParser, node, filenameIsInApp)

@sentry/server-utils is added as a dependency to @sentry/node-native and @sentry/bundler-plugins, the only two consumers that didn't already have it. Every other SDK already depended on server-utils and keeps re-exporting these under the same names, so there is no user-facing change. mcp-server and the integrations/http/* subtree stay in core.

ServerRuntimeClient and the stack parser reach for a few core building blocks that aren't public. Rather than route them through the semi-internal @sentry/core/server entry, the three that are genuinely needed are exported from the public @sentry/core entry — getTraceInfoFromScope, addUserAgentToTransportHeaders, normalizeStackTracePath — and the other two dependencies are dropped: the transport buffer size becomes a local constant, and the span-streaming integration name is read off the integration instance. No metric or transport internals are exposed. ServerRuntimeOptions was extracted from the shared types/options.ts (which stays in core) into its own server-utils file.

Keeping the moved code out of edge/client bundles

Relocating this into @sentry/server-utils surfaced Next.js bundling regressions — every next build e2e app failed with UnhandledSchemeError on node:async_hooks / node:net.

Root cause: server-only @sentry/server-utils code (which statically imports node:async_hooks via the async-context strategy, and node:net via the firebase integration) was reaching the Next.js edge and browser bundles, which can't resolve node: builtins. This was latent before the move — the same code paths pulled these helpers from browser-safe @sentry/core. Fixed at each layer:

  • Barrel tree-shaking. The @sentry/server-utils barrel re-exported attachHapiErrorHandler via a const binding (export const x = _x) — a module-level statement that pinned the whole barrel graph against tree-shaking, so importing any one symbol dragged in every Node integration. It's now a plain re-export with the @deprecated marker moved onto the source function (matching attachKoaErrorHandler).
  • @sentry/vercel-edge now imports ServerRuntimeClient / nodeStackLineParser / trpcMiddleware from the lean @sentry/server-utils/no-diagnostic-channels entry (as it already did for its AI/OTLP integrations), so the heavy barrel never reaches the Next.js edge bundle.
  • Next.js client/common split. The client entry re-exports all of common, which pulled the server-only App-Router wrappers (wrapServerComponent, wrapRouteHandler, wrapMiddleware, wrapGenerationFunction, withServerActionInstrumentation) and captureRequestError into the browser bundle. These moved to common/serverOnlyExports, re-exported only from the server and edge entrypoints. And because the pages-router _error instrumentation (captureUnderscoreErrorException) is legitimately dual-bundled and uses responseEnd's waitUntil, responseEnd now inlines a client-safe vercelWaitUntil instead of importing it from server-utils (vercelWaitUntil stays in server-utils for flushIfServerless).

Tests

The node-stack-parsing tests move to @sentry/server-utils alongside the parser. Core unit tests that only used the parser as a realistic fixture (metadata, debug-ids, third-party-errors-filter) stay in core with a local node-stack-parser fixture, so core keeps that coverage without depending on server-utils.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ASNdTRtxNEjNMEBGCxENT7

@mydea
mydea force-pushed the fn/move-server-only-apis-to-server-utils branch from 172fba6 to d52ac53 Compare September 2, 2026 11:39
@mydea
mydea force-pushed the fn/move-server-runtime-client-to-server-utils branch from b8c658c to 0f1963a Compare September 2, 2026 11:39
Comment thread packages/core/src/server.ts
@mydea
mydea force-pushed the fn/move-server-runtime-client-to-server-utils branch 3 times, most recently from 6a3662a to d85e36e Compare September 2, 2026 12:01
@github-actions

github-actions Bot commented Sep 2, 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 - -
@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 24.63 kB -33.41% -12.36 kB 🔽
@sentry/core/browser 13.55 kB - -
@sentry/node 127.75 kB +0.02% +23 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.61 kB - -
@sentry/node - without tracing 88.73 kB +0.03% +22 B 🔺
@sentry/node - without channel injection 107 kB +0.03% +24 B 🔺
@sentry/aws-serverless 97.13 kB +0.03% +29 B 🔺
@sentry/cloudflare (withSentry) - minified 202.03 kB +0.04% +80 B 🔺
@sentry/cloudflare (withSentry) 502.92 kB +0.06% +256 B 🔺

View base workflow run

@mydea mydea changed the title ref(server-utils): Move ServerRuntimeClient and node stack parser out of core ref(server-utils): Move ServerRuntimeClient, node stack parser and server-only utils out of core Sep 2, 2026
@mydea
mydea changed the base branch from fn/move-server-only-apis-to-server-utils to develop September 2, 2026 12:06
@mydea
mydea force-pushed the fn/move-server-runtime-client-to-server-utils branch from d85e36e to 194f1bd Compare September 2, 2026 12:17
@mydea
mydea requested a review from chargome September 2, 2026 12:18
@mydea
mydea force-pushed the fn/move-server-runtime-client-to-server-utils branch 2 times, most recently from 6396adc to a6bdaf3 Compare September 3, 2026 11:04

@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/nuxt/src/runtime/hooks/captureErrorHook.ts Outdated
@mydea
mydea force-pushed the fn/move-server-runtime-client-to-server-utils branch from a6bdaf3 to a3c0438 Compare September 7, 2026 12:39

@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 1 potential issue.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit a3c0438. Configure here.

Comment thread packages/astro/src/server/middleware.ts Outdated
mydea and others added 11 commits September 8, 2026 12:53
…rver-only utils out of core

Continues slimming `@sentry/core` down to its isomorphic surface by moving the
remaining server-only APIs into `@sentry/server-utils`.

Moved out of core:
- `flushIfServerless` + `vercelWaitUntil` (used by the meta-framework SDKs)
- `trpcMiddleware`
- `callFrameToStackFrame` / `watchdogTimer` (the anr worker helpers)
- `loadModule` (split out of `utils/node`; `isNodeEnv` stays, since core depends
  on it via `isBrowser`)
- the base `ServerRuntimeClient` (with `ServerRuntimeClientOptions` /
  `ServerRuntimeOptions`) and the node stack-trace parser (`nodeStackLineParser`,
  `node`, `filenameIsInApp`)

`@sentry/server-utils` is added as a dependency to `@sentry/node-native` and
`@sentry/bundler-plugins`, the only two consumers that didn't already have it.
Every other SDK already depended on server-utils and keeps re-exporting these
under the same names, so there is no user-facing change. `mcp-server` and the
`integrations/http/*` subtree stay in core.

`ServerRuntimeClient` and the stack parser reach for a few core building blocks
that aren't public. Rather than route them through the semi-internal
`@sentry/core/server` entry, the three that are genuinely needed are exported
from the public `@sentry/core` entry (`getTraceInfoFromScope`,
`addUserAgentToTransportHeaders`, `normalizeStackTracePath`); the other two are
dropped — the transport buffer size becomes a local constant, and the
span-streaming integration name is read off the integration instance. No metric
or transport internals are exposed. `ServerRuntimeOptions` was extracted from the
shared `types/options.ts` (which stays in core) into its own server-utils file.

Keeping the moved code out of edge/client bundles: relocating this into
`@sentry/server-utils` surfaced Next.js bundling regressions (every `next build`
e2e app failed with `UnhandledSchemeError` on `node:async_hooks` / `node:net`),
because server-only server-utils code was reaching the edge and browser bundles,
which can't resolve `node:` builtins. This was latent before the move (the same
paths pulled these helpers from browser-safe `@sentry/core`).

- Make the `@sentry/server-utils` barrel tree-shakeable: re-export the deprecated
  `attachHapiErrorHandler` normally with the deprecation moved onto the source
  function (matching `attachKoaErrorHandler`) instead of a non-shakeable `const`
  re-export that pinned the whole barrel graph.
- Point `@sentry/vercel-edge` at `@sentry/server-utils/no-diagnostic-channels`
  for `ServerRuntimeClient` / `nodeStackLineParser` / `trpcMiddleware`, so the
  heavy barrel (Node integrations, `node:net`) never reaches the Next.js edge
  bundle.
- Split the server-only App-Router wrappers and `captureRequestError` out of the
  client-reachable `common` barrel into `common/serverOnlyExports`, re-exported
  only from the server and edge entrypoints. Make `responseEnd` client-safe by
  inlining `vercelWaitUntil`, so the dual-bundled pages-router `_error` path no
  longer pulls `node:async_hooks` into the browser bundle. `vercelWaitUntil`
  stays in server-utils for `flushIfServerless`.

Relocates the node-stack-parsing tests to `@sentry/server-utils`; keeps the
core-internal unit tests (`metadata`, `debug-ids`, `third-party-errors-filter`)
in core, fed by a local node stack parser fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASNdTRtxNEjNMEBGCxENT7
After `ServerRuntimeClient` and `nodeStackLineParser` moved from the edge-safe
`@sentry/core/server` entry into `@sentry/server-utils`, `client.ts` and
`vendor/stacktrace.ts` pulled the full `@sentry/server-utils` barrel (which
subscribes to `node:diagnostics_channel`) into the `wrapRequestHandler` graph,
breaking runtimes without `nodejs_compat` (e.g. Shopify Oxygen / the
remix-hydrogen app).

Route those two imports through `@sentry/server-utils/no-diagnostic-channels` —
the edge-safe subset that carries `ServerRuntimeClient` and the node stack
parser without the channel subscription — matching how `@sentry/vercel-edge`
already consumes them. Update the `requestModuleGraph` guard to allow that one
edge-safe entry while still forbidding the heavy barrel and `@sentry/node`.

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

- node `httpServerIntegration` test imported `ServerRuntimeClient` from
  `@sentry/core/server`, which no longer exports it — import it from
  `@sentry/server-utils`.
- tanstackstart-react `wrapFetchWithSentry` test mocked `@sentry/server-utils`
  without spreading the original module, so `nodeStackLineParser` (now pulled
  from server-utils by the `@sentry/node` init path) was undefined and the mock
  factory threw. Spread `importOriginal()` like the sibling mocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7JDQBD9J2okCe1hkWCanU
`nodeStackLineParser` moved out of `@sentry/core/server` into
`@sentry/server-utils`. Update the deno unit tests (`mod`,
`deno-runtime-metrics`) and the deno integration `direct-client-acs` scenario
to import it from `@sentry/server-utils`; the deno SDK src already does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7JDQBD9J2okCe1hkWCanU
`@sentry/bundler-plugins` now externalizes `@sentry/server-utils` (it `require`s
it at build time for `ServerRuntimeClient` / `nodeStackLineParser`). The
fixtures pinned `@sentry/core` and `@sentry/bundler-plugins` to local tarballs
but not server-utils, so pnpm pulled a published `@sentry/server-utils` from the
registry that mismatched the local `@sentry/core` build (`SPAN_KIND` export
error), failing every bundler run.

Pack `@sentry/server-utils` in `setup.mjs` and add a `pnpm.overrides` entry for
it to every fixture, pointing at the local tarball like core and bundler-plugins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7JDQBD9J2okCe1hkWCanU
The nuxt-3 app imported `flushIfServerless` from `@sentry/core/server`, which no
longer exports it after the move to `@sentry/server-utils`. Use the public
`Sentry.flush()` (already imported as `SentryNode`) instead of reaching into an
internal entry point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7JDQBD9J2okCe1hkWCanU
The Hydrogen (Oxygen) e2e app `remix-hydrogen` failed to build: its client bundle
pulled `@sentry/server-utils/async-context` (`import { AsyncLocalStorage } from
'node:async_hooks'`), which Vite externalizes into an empty stub, throwing
`"AsyncLocalStorage" is not exported by "__vite-browser-external"`.

`async-context` is the only module in the shared `exports.ts` surface that
statically imports a `node:` builtin, yet it was re-exported from both the `index`
and `no-diagnostic-channels` barrels — so any browser/edge bundle importing *any*
helper from those barrels dragged `node:async_hooks` into the graph.

- Move `setAsyncLocalStorageAsyncContextStrategy` out of the shared `exports.ts`
  into a dedicated `@sentry/server-utils/async-context` entry, so `index` /
  `no-diagnostic-channels` are free of `node:` builtins. Update its consumers
  (Node/Deno/Cloudflare SDKs + tests), all of which run where `node:async_hooks`
  resolves.
- Point the two `@sentry/remix/cloudflare`-reachable imports (`instrumentServer`'s
  `loadModule`, `cloudflare/index`'s `trpcMiddleware`) at the lean
  `no-diagnostic-channels` barrel instead of the full `@sentry/server-utils`
  barrel, whose Node-only integrations (`tedious` → `node:events`, …) otherwise
  reach the bundled Hydrogen client build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7JDQBD9J2okCe1hkWCanU
@mydea
mydea force-pushed the fn/move-server-runtime-client-to-server-utils branch from 5972456 to b9c6beb Compare September 8, 2026 11:20

This branch has not been deployed

No deployments
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.

1 participant