Skip to content

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

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

ref(server-utils): Move ServerRuntimeClient, node stack parser and server-only utils out of core#23833
mydea wants to merge 8 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 📦

Path Size % Change Change
@sentry/browser 28.71 kB - -
@sentry/browser - with treeshaking flags 27.02 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.92 kB - -
@sentry/browser (incl. Tracing) 49.1 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 49.1 kB - -
@sentry/browser (incl. Tracing, Profiling) 52.01 kB - -
@sentry/browser (incl. Tracing, Replay) 88.58 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.88 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 93.26 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 106.21 kB - -
@sentry/browser (incl. Feedback) 46.2 kB - -
@sentry/browser (incl. sendFeedback) 33.78 kB - -
@sentry/browser (incl. FeedbackAsync) 38.87 kB - -
@sentry/browser (incl. Metrics) 29.68 kB - -
@sentry/browser (incl. Logs) 29.97 kB - -
@sentry/browser (incl. Metrics & Logs) 30.61 kB - -
@sentry/react 30.47 kB - -
@sentry/react (incl. Tracing) 51.3 kB - -
@sentry/vue 35.95 kB - -
@sentry/vue (incl. Tracing) 51.37 kB - -
@sentry/svelte 28.74 kB - -
CDN Bundle 30.45 kB - -
CDN Bundle (incl. Tracing) 49.64 kB - -
CDN Bundle (incl. Logs, Metrics) 32.69 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.56 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.28 kB - -
CDN Bundle (incl. Tracing, Replay) 87.13 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 89 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 93.06 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 95.01 kB - -
CDN Bundle - uncompressed 90.19 kB - -
CDN Bundle (incl. Tracing) - uncompressed 147.87 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.58 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 153.65 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.75 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 267.37 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 273.13 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 281.07 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 286.82 kB - -
@sentry/nextjs (client) 53.92 kB - -
@sentry/sveltekit (client) 49.53 kB - -
@sentry/core/server 28.74 kB -29.83% -12.21 kB 🔽
@sentry/core/browser 13.53 kB - -
@sentry/node 124.66 kB +0.03% +25 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.51 kB - -
@sentry/node - without tracing 88.5 kB +0.03% +25 B 🔺
@sentry/node - without channel injection 103.96 kB +0.03% +28 B 🔺
@sentry/aws-serverless 96.87 kB +0.03% +28 B 🔺
@sentry/cloudflare (withSentry) - minified 201.64 kB +0.04% +80 B 🔺
@sentry/cloudflare (withSentry) 501.63 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 from 194f1bd to 6396adc Compare September 2, 2026 14:53
mydea and others added 8 commits September 3, 2026 13:03
…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 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.

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 a6bdaf3. Configure here.

@@ -1,5 +1,5 @@
import { captureException, getClient, getCurrentScope } from '@sentry/core';
import { flushIfServerless } from '@sentry/core/server';
import { flushIfServerless } from '@sentry/server-utils';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nuxt pulls the full server-utils barrel

Medium Severity

flushIfServerless and trpcMiddleware now come from the full @sentry/server-utils barrel instead of @sentry/core/server or @sentry/server-utils/no-diagnostic-channels. That barrel still statically re-exports Node integrations (node:net, diagnostic channels). This is flagged because the review rules forbid unguarded Node builtins on edge-capable SDK paths, the same failure this PR fixed for Next.js and vercel-edge.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a6bdaf3. Configure here.

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