Skip to content

ref(nitro): Move channel-based instrumentation into @sentry/server-utils - #24861

Merged
mydea merged 14 commits into
developfrom
feat/nitro-move-to-server-utils
Oct 19, 2026
Merged

mydea merged 14 commits into
developfrom
feat/nitro-move-to-server-utils

Conversation

@mydea

@mydea mydea commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Moves Nitro's node:diagnostics_channel-based instrumentation out of @sentry/nitro and into @sentry/server-utils, so it is no longer tied to the Nitro package and can be provided by any server SDK. This mirrors the Hono → server-utils move.

The runtime instrumentation splits into two integrations:

  • nitroIntegration — h3/srvx HTTP + middleware spans and unstorage cache spans. It only creates spans, so it is a tracing integration, registered via getTracingIntegrations(). That makes it available (and on by default) in node, deno and bun.
  • nitroServerTimingIntegration — sets the Server-Timing response headers that carry sentry-trace/baggage so the browser SDK can connect a pageload to the backend trace. This must work even when tracing is disabled (trace-without-performance), so it is a default (non-tracing) integration, registered via getErrorIntegrations(). It is opt-out-able independently.

Both integrations subscribe to h3's tracing channels, so they are inert unless a Nitro app (the only real emitter of those channels) is running.

Cloudflare (and other bundler-only SDKs) get both integrations auto-injected via orchestrion registration-only configs: h3 and unstorage are transformed at build time to register the matching factory, which @sentry/cloudflare instantiates at init(). These are registration-only (no channels injected — the libraries publish their own), so they are excluded from the runtime loader and are a no-op on node/deno/bun, where the integrations are registered statically.

Non-breaking: @sentry/nitro's init() goes through @sentry/node's default integrations, which now include both of the above, so existing Nitro-on-Node apps get exactly the same spans and headers as before — just sourced from the integrations rather than the runtime plugin. @sentry/nitro no longer depends on @sentry/server-utils.

Why error capture stays in @sentry/nitro

Error capture is intentionally not moved. Nitro's error hook is a broad boundary (request/response-hook, plugin-init, cache and process-level unhandled* errors, plus contexts.nitro tags/method/path). The tracing channels only see errors thrown through a traced handler, so folding error capture into a channel-based integration would lose coverage and force an h3 coupling into the framework-neutral package. It remains in the Nitro runtime plugin's error hook.

E2E coverage

A new cloudflare-nitro e2e app (raw Nitro 3 on the cloudflare_module preset) exercises the Cloudflare auto-injection end to end — asserting auto.http.nitro.* spans, auto.cache.nitro cache spans, and the Server-Timing header, which also confirms the tracing channels bind under Cloudflare's async-context strategy. Since raw Nitro has no first-class Cloudflare init helper yet, the app wires init by hand (a Nitro server plugin wrapping nitroApp.fetch with wrapRequestHandler, mirroring @sentry/nuxt); a supported helper could be a good follow-up.

🤖 Generated with Claude Code

@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/server-utils/src/integrations/index.ts
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.88 kB - -
@sentry/browser - with treeshaking flags 28.02 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.92 kB - -
@sentry/browser (incl. Tracing) 52.07 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.07 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.02 kB - -
@sentry/browser (incl. Tracing, Replay) 91.82 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.68 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.55 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.49 kB - -
@sentry/browser (incl. Feedback) 47.4 kB - -
@sentry/browser (incl. sendFeedback) 34.94 kB - -
@sentry/browser (incl. FeedbackAsync) 40.05 kB - -
@sentry/browser (incl. Metrics) 30.9 kB - -
@sentry/browser (incl. Logs) 31.18 kB - -
@sentry/browser (incl. Metrics & Logs) 31.81 kB - -
@sentry/react 31.7 kB - -
@sentry/react (incl. Tracing) 54.39 kB - -
@sentry/vue 38.04 kB - -
@sentry/vue (incl. Tracing) 55.01 kB - -
@sentry/svelte 29.9 kB - -
@sentry/remix (Remix 3 client bundle) 57.08 kB - -
CDN Bundle 31.64 kB - -
CDN Bundle (incl. Tracing) 52.58 kB - -
CDN Bundle (incl. Logs, Metrics) 33.84 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.52 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.77 kB - -
CDN Bundle (incl. Tracing, Replay) 90.25 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.18 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.34 kB - -
CDN Bundle - uncompressed 93.18 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.03 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.72 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.76 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276.2 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 282.14 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.9 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.84 kB - -
@sentry/nextjs (client) 56.8 kB - -
@sentry/sveltekit (client) 52.45 kB - -
@sentry/core/server 41.12 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 154.79 kB +1.12% +1.71 kB 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 84.07 kB +0.04% +29 B 🔺
@sentry/node - without tracing 95.18 kB +0.33% +311 B 🔺
@sentry/node - without channel injection 132.81 kB +1.27% +1.66 kB 🔺
@sentry/aws-serverless 103.32 kB +0.28% +282 B 🔺
@sentry/cloudflare (withSentry) - minified 210.6 kB - -
@sentry/cloudflare (withSentry) 522.53 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 228.27 kB - -

View base workflow run

@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/server-utils/src/orchestrion/config/nitro.ts
@mydea
mydea added this pull request to stack #24871 September 30, 2026 08:41
@mydea
mydea force-pushed the feat/nitro-move-to-server-utils branch 2 times, most recently from e8a310d to 810fa6c Compare September 30, 2026 12:26
@mydea
mydea marked this pull request as ready for review September 30, 2026 12:30
@mydea
mydea requested review from a team as code owners September 30, 2026 12:30
@mydea
mydea requested review from a team, JPeer264, isaacs, logaretm, nicohrubec and s1gr1d and removed request for a team September 30, 2026 12:30
{ exportName: 'redisIntegration', modules: ['redis', '@redis/client', 'ioredis'] },
{ exportName: 'dataloaderIntegration', modules: ['dataloader'] },
{ exportName: 'nitroIntegration', modules: ['h3'] },
{ exportName: 'nitroServerTimingIntegration', modules: ['unstorage'] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The nitroServerTimingIntegration may silently fail to register on Cloudflare if unstorage is tree-shaken from a minimal Nitro app, breaking trace propagation.
Severity: MEDIUM

Suggested Fix

To ensure robust registration, the module dependency for nitroServerTimingIntegration should be changed from ['unstorage'] to ['h3']. Since h3 is a guaranteed dependency in any Nitro application, this change ensures the integration is always registered when needed. The configuration should be updated to { exportName: 'nitroServerTimingIntegration', modules: ['h3'] }.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts#L64

Potential issue: The registration of `nitroServerTimingIntegration` for Cloudflare
Workers is configured to trigger upon detection of the `unstorage` module. This relies
on the assumption that `unstorage` is a core, non-tree-shakable dependency in all Nitro
applications. However, if a minimal Nitro application can be built without `unstorage`,
the integration will silently fail to register. This would result in the `sentry-trace`
and `baggage` headers not being set, breaking distributed tracing for affected
applications without any error or warning.

Did we get this right? 👍 / 👎 to inform future reviews.

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 added a fix for this in a separate PR on this stack here: #24897

@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.

@logaretm logaretm 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.

Should we merge both nitro integrations into one integration that enables either instrumentation with options?

nitroIntegration({
  // defaults, or flip em to `disabeXXX`
  serverTiming: true,
  storageInstrumentation: true,
   // other instrumentations
})

@mydea
mydea force-pushed the feat/nitro-move-to-server-utils branch from d93c0f9 to a6ecc88 Compare October 1, 2026 10:59
@logaretm

logaretm commented Oct 1, 2026

Copy link
Copy Markdown
Member

Currently they are added independently as tracing and error integration within Nitro

Yea, that was never the intention of them I think. This happened as a side effect of other changes. I think rarely anyone will use one without the other.

Comment on lines 112 to 113
const span = startInactiveSpan({
name: isUnparameterizedStreamedServerSpan

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 PR moves code, so this is not blocking. But nitroIntegration is now a default in every Node-based SDK.

httpServerSpansIntegration from our Node package already creates the http.server root span. srvx.request adds a second http.server child span, and h3.request adds a third. Maybe you can check what happens there.

Suggestion: if an http.server span is already active, don't start a new one, but set the route/status/etc. on that span.

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.

FWIW I also had this with Next.js on Cloudflare (there would have been 3 http.server spans). I solved it by simply ignoring the specific spans:

options.ignoreSpans = [
...(options.ignoreSpans || []),
...NEXTJS_SERVER_IGNORE_SPANS,
// The request spans of Next.js repeat the request span of `withSentry`. An ignored span does not become active,
// so the other Next.js spans become children of the request span of `withSentry`, which then gets the route
// from their `next.route` and its status from the response.
{ attributes: { [ATTR_NEXT_SPAN_TYPE]: 'BaseServer.handleRequest' } },
];

Not sure if this would be applicable here too.

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.

#25295 --> good point, added a follow up here. it does change structure though, but I think it's worth it...

Comment thread packages/nitro/src/sdk.ts
return [...getDefaultNodeIntegrations(options)];
// `nitroIntegration` is also part of the Node tracing integrations, but add it explicitly so it is
// always present for the Nitro SDK regardless of that gating (deduped by name when both are added).
return [...getDefaultNodeIntegrations(options), nitroIntegration()];

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 adds nitroIntegration also when tracing is off, no? We should gate this.

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.

this is fine here, as we know nitro is relevant when using the nitro sdk I'd say (it is also the same behavior as before, where we always added this)?

@mydea
mydea force-pushed the feat/nitro-move-to-server-utils branch from 73ba6de to eb9579a Compare October 12, 2026 13:18

@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 dev-packages/node-integration-tests/suites/tracing/nitro/test.ts
@mydea
mydea removed this pull request from stack #24913 October 14, 2026 07:32
@mydea
mydea added this pull request to stack #25350 October 14, 2026 07:32
@mydea
mydea force-pushed the feat/nitro-move-to-server-utils branch from 69a9819 to d900bc4 Compare October 16, 2026 06:57

@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.

mydea and others added 14 commits October 19, 2026 09:55
Extract Nitro's diagnostics-channel instrumentation out of `@sentry/nitro`
into `@sentry/server-utils`, so any server SDK can provide it:

- `nitroIntegration` (h3/srvx HTTP + middleware spans, unstorage cache spans)
  is a tracing integration, registered via `getTracingIntegrations()` and so
  available in node, deno and bun.
- `nitroServerTimingIntegration` sets the `Server-Timing` trace-propagation
  headers. It is a default (non-tracing) integration so it also runs in
  tracing-without-performance mode, and can be opted out separately.

Error capture stays in `@sentry/nitro`'s runtime plugin (its Nitro `error`
hook), which a channel-based integration cannot replace without losing
coverage. `@sentry/nitro` keeps getting all of this through the underlying
`@sentry/node` defaults, so nothing changes for existing Nitro-on-Node apps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orchestrion

Add registration-only orchestrion configs so the Nitro integrations are
auto-registered on bundler-only SDKs like `@sentry/cloudflare`, which discover
a bundled module via the module-injected event and instantiate its factory.

The snippet maps one integration per module, so two anchors are used, both core
Nitro dependencies evaluated at startup: `h3` (dist/h3.mjs) registers
`nitroIntegration` (spans) and `unstorage` (dist/index.mjs) registers
`nitroServerTimingIntegration`. These are registration-only (no channels are
injected, the libraries publish their own), so they are excluded from the
runtime loader and are a no-op on node/deno/bun, where the integrations are
registered statically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trion config

Document why the h3/unstorage registration-only ranges use the `-0` suffix: the
orchestrion matcher is the vendored `semifies` (not node-semver), where a range
carrying a prerelease tag matches prereleases across patch tuples — so
`>=2.0.0-0` matches the `2.0.1-rc.*` / `2.0.0-alpha.*` versions Nitro 3 ships.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a raw Nitro 3 app deployed to Cloudflare (Workers, `cloudflare_module`
preset) that exercises the orchestrion registration-only path on a bundler-only
SDK: the app imports no Nitro instrumentation, and `nitroIntegration` /
`nitroServerTimingIntegration` are auto-injected via the orchestrion transform
over h3/unstorage and instantiated by `@sentry/cloudflare` at init.

Tests assert `auto.http.nitro.*` spans, `auto.cache.nitro` cache spans, and the
`Server-Timing` trace-propagation header — i.e. the whole auto-injection path,
including that the tracing channels bind under Cloudflare's async-context
strategy.

Init mirrors what @sentry/nuxt does for Nitro-on-Cloudflare (no first-class
raw-Nitro Cloudflare helper exists): a Nitro server plugin wraps `nitroApp.fetch`
with `wrapRequestHandler`, and the orchestrion rollup/rolldown plugin is wired
into the Nitro build config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `mod.test.ts` snapshots captured the full `sdk.integrations` list, so any
change to the default integrations (such as the new nitro integrations) broke
them even though the list is incidental to what these tests verify. Normalize
`sdk.integrations` to a placeholder, like the other volatile fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stacked on develop's hono size bump: the nitro integrations add on top, so
@sentry/node needs 146 KB and the no-channel-injection variant 125 KB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`nitroIntegration` is part of the Node tracing integrations, but the Nitro SDK
should always include it regardless of that gating. Add it explicitly to the
Nitro SDK's default integrations (deduped by name when both are added), and
re-export `nitroIntegration`/`nitroServerTimingIntegration` from `@sentry/node`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nitroIntegration and nitroServerTimingIntegration are exported from
@sentry/node, so every SDK with an explicit re-export list needs them too
(caught by the node-exports-test-app consistency check). Deno and Bun also
enable them by default via getTracingIntegrations().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jan Peer Stöcklmair <jan.oster94@gmail.com>
Drives h3 v2, srvx and unstorage with their tracing plugins enabled, which
publish the same diagnostics channels as a Nitro app, so the integrations
are covered on Node without a Nitro build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ite`

Bun 1.3 drops the newline of a `console.log` call when the string is exactly
4096 bytes, so an envelope of that size gets merged with the next one into a
single line that the integration test runner can't parse. The runner then waits
for an envelope it already received until the test times out, which made the
Bun 1.3 integration tests flaky (e.g. `captureMessage/with_level`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mydea
mydea force-pushed the feat/nitro-move-to-server-utils branch from f7da752 to 78f185c Compare October 19, 2026 07:58

@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 78f185c. Configure here.

setupOnce() {
captureTracingEvents();
captureStorageEvents();
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Span setup skips tracing-channel wait

Medium Severity

nitroIntegration calls captureTracingEvents() and captureStorageEvents() directly from setupOnce, without waitForTracingChannelBinding. Sibling native-channel integrations (dataloader, knex, native GraphQL) wait so bindTracingChannelToSpan can parent spans. If the async-context binding is not ready yet — common with a custom OpenTelemetry setup — the bind no-ops, __SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__ is still set, and Nitro HTTP/cache spans are never created. The old runtime plugin ran after init(), so this race is new.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 78f185c. Configure here.


diagnosticsChannel.tracingChannel<H3TracingRequestEvent>('h3.request').start.subscribe(message => {
setServerTimingHeaders((message as H3TracingRequestEvent).event);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server-Timing subscriber can throw into h3

Low Severity

The h3.request start subscriber calls setServerTimingHeaders with no safeChannelCallback (or other try/catch). A throw from getTraceData(), a missing event, or headers.append on immutable headers propagates through diagnostics_channel into h3’s request path and can fail the user request. This integration is now a default on every Node-based SDK, so the blast radius is larger than the old Nitro plugin.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 78f185c. Configure here.

@mydea
mydea merged commit 17e81ca into develop Oct 19, 2026
375 checks passed
@mydea
mydea deleted the feat/nitro-move-to-server-utils branch October 19, 2026 08:20
mydea added a commit that referenced this pull request Oct 19, 2026
…tion` (#25349)

Stacked on #24861. Replaces #24897.

`nitroServerTimingIntegration` only existed as a separate integration
because `nitroIntegration` was tracing-gated, while Server-Timing trace
propagation must work in tracing-without-performance mode. That split
meant Cloudflare auto-injection needed a second anchor module
(`unstorage`), since a module registers a single integration, and a
Nitro app that never loaded `unstorage` would silently lose the headers.
#24897 tried to fix that by letting one module register several
integrations, which complicated the global marker shape.

Instead, there is now a single `nitroIntegration` with options to opt
out of parts of it, both enabled by default:

```ts
nitroIntegration({
  serverTiming: false, // no Server-Timing sentry-trace/baggage headers
  storage: false, // no unstorage cache spans
});
```

It's registered as a default integration rather than a tracing one, like
express/hono/koa: without tracing, the spans it starts are
non-recording, and the Server-Timing headers still propagate. It's
anchored on `h3` alone, which every HTTP-serving Nitro app loads, so the
marker keeps its single-factory shape.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

4 participants