Skip to content

feat(bun): Instrument outgoing fetch requests - #22869

Merged
JPeer264 merged 8 commits into
fn/native-fetch-checkfrom
fn/bun-outgoing-fetch-instrumentation
Aug 4, 2026
Merged

feat(bun): Instrument outgoing fetch requests#22869
JPeer264 merged 8 commits into
fn/native-fetch-checkfrom
fn/bun-outgoing-fetch-instrumentation

Conversation

@mydea

@mydea mydea commented Jul 30, 2026

Copy link
Copy Markdown
Member

Adds outgoing fetch instrumentation to @sentry/bun via a new fetchIntegration.

Why the Bun SDK needed this

@sentry/bun shipped nativeNodeFetchIntegration (from @sentry/node) in its defaults, but that integration works exclusively by subscribing to undici's diagnostics_channel events (undici:request:create, etc.). Bun implements fetch natively (in Zig, with its own HTTP client) and never emits those channels, so the integration was registered but completely inert — no http.client spans, no breadcrumbs, and no trace propagation on outgoing requests.

Approach

The new fetchIntegration patches the global fetch (via core's addFetchInstrumentationHandler + instrumentFetchRequest), mirroring the Cloudflare SDK, which has the same "no undici / no diagnostics_channel" constraint. It creates http.client spans (origin auto.http.fetch), records fetch breadcrumbs, and injects sentry-trace/baggage headers gated by tracePropagationTargets. It replaces nativeNodeFetchIntegration in the Bun default integrations to avoid shipping a dead integration and any risk of double instrumentation.

Because @sentry/elysia builds on @sentry/bun's defaults, it inherits this automatically.

Preserving Bun's non-standard fetch properties

Core's fetch instrumentation now wraps the global fetch in a Proxy rather than a plain wrapper function. Bun hangs non-standard APIs off the fetch global (most notably fetch.preconnect), and a plain wrapper closure would drop those own properties, breaking code that relies on them. The Proxy transparently forwards property access (and toString()) to the native implementation, so fetch.preconnect and friends keep working after instrumentation is installed.

Tests

  • New dev-packages/bun-integration-tests/suites/fetch/ — asserts http.client span creation, header propagation to allowed targets, no propagation to disallowed targets, and breadcrumb recording. Verified against a real Bun runtime.
  • elysia-bun e2e — un-fixme'd the two outgoing-fetch propagation tests, which now pass.
  • nextjs-16-bun e2e — un-skipped; asserts propagation through the Next.js AppRouteRouteHandlers.runHandler span (see below).

Known limitation: Next.js on Bun

For apps running @sentry/nextjs (→ @sentry/node) on the Bun runtime (not @sentry/bun), the fetchIntegration can be added manually, but outgoing-fetch propagation is owned by the active OTel context, whose active span during a route handler is Next's executing api route span — not our inactive http.client span. So the downstream request becomes a sibling of our http.client span rather than its child. The nextjs-16-bun test asserts this actual behavior. A proper fix (a Bun-runtime fetch path inside @sentry/node that participates in the active context) is out of scope here.

Comment thread packages/core/src/instrument/fetch.ts Outdated

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

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 225988a. Configure here.

Comment thread packages/bun/test/integrations/bunserver.test.ts Outdated
@mydea
mydea force-pushed the fn/bun-outgoing-fetch-instrumentation branch from 225988a to 98d00aa Compare July 30, 2026 12:58
@mydea
mydea marked this pull request as ready for review July 30, 2026 12:58
@mydea
mydea requested a review from a team as a code owner July 30, 2026 12:58
@mydea
mydea requested review from a team, JPeer264 and isaacs and removed request for a team July 30, 2026 12:58
Comment thread packages/core/src/instrument/fetch.ts Outdated
shouldCreateSpanForRequest?: (url: string) => boolean;
}

const _fetchIntegration = ((options: FetchOptions = {}) => {

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.

l: Seems like it is an almost 1:1 copy from the one from Cloudflare. Would be nice if this could get moved to server-utils and named somehoe nativeFetchIntegration or so? Then we can reuse it 😍

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

The only change that I'd gate on here is the issue with the confusing native fetch detection flags. I recommend leaving that aside in this PR, and addressing later in a cleanup that can be more careful about making that less footgunny.

Comment thread packages/core/src/instrument/fetch.ts Outdated
Comment thread packages/bun/test/integrations/bunserver.test.ts Outdated
Comment thread packages/core/src/instrument/fetch.ts Outdated
Comment thread packages/bun/src/index.ts Outdated
Comment thread packages/bun/src/integrations/fetch.ts Outdated
Comment thread packages/bun/src/integrations/fetch.ts Outdated
// `skipNativeFetchCheck: true` — the native-fetch check is browser-only (it probes for
// `[native code]` and falls back to an iframe DOM check). On Bun `fetch` may already be
// wrapped by the host (e.g. Next.js) and there is no DOM, so we always patch the global.
}, true);

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 drops fetch.preconnect (a bun-specific non-standard addition).

Would probably be worth adding ownProps back on:

const globalFetch = GLOBAL_OBJ.fetch;
// ... instrument fetch call
for (const k of Object.getOwnPropertyNames(globalFetch)) {
  Object.defineProperty(fetch, k, Object.getOwnPropertyDescriptor(globalFetch, k));
}

Or something to that effect.

@mydea
mydea force-pushed the fn/bun-outgoing-fetch-instrumentation branch from 98d00aa to 23639a5 Compare July 31, 2026 07:55
@mydea
mydea requested a review from a team as a code owner July 31, 2026 07:55
@mydea
mydea requested review from logaretm and msonnb and removed request for a team July 31, 2026 07:55
@mydea
mydea changed the base branch from develop to fn/native-fetch-check July 31, 2026 07:55
@mydea
mydea requested a review from a team as a code owner July 31, 2026 07:59
@mydea
mydea requested review from isaacs, nicohrubec and s1gr1d and removed request for a team July 31, 2026 07:59
@mydea

mydea commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

I refactored the skipNativeFetch check out into a separate change/PR (this is overall not ideal IMHO).

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.93 kB +0.46% +137 B 🔺
@sentry/browser - with treeshaking flags 28.13 kB +0.4% +110 B 🔺
@sentry/browser (incl. Tracing) 47.38 kB +0.26% +120 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.38 kB +0.24% +110 B 🔺
@sentry/browser (incl. Tracing, Profiling) 52.15 kB +0.23% +116 B 🔺
@sentry/browser (incl. Tracing, Replay) 86.71 kB +0.17% +140 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 76.14 kB +0.17% +128 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 91.43 kB +0.15% +133 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 104.06 kB +0.12% +121 B 🔺
@sentry/browser (incl. Feedback) 47.24 kB +0.27% +126 B 🔺
@sentry/browser (incl. sendFeedback) 34.78 kB +0.4% +138 B 🔺
@sentry/browser (incl. FeedbackAsync) 39.88 kB +0.35% +136 B 🔺
@sentry/browser (incl. Metrics) 31.01 kB +0.45% +138 B 🔺
@sentry/browser (incl. Logs) 31.24 kB +0.43% +132 B 🔺
@sentry/browser (incl. Metrics & Logs) 31.9 kB +0.37% +117 B 🔺
@sentry/react 31.74 kB +0.44% +139 B 🔺
@sentry/react (incl. Tracing) 49.62 kB +0.27% +132 B 🔺
@sentry/vue 35 kB +0.45% +154 B 🔺
@sentry/vue (incl. Tracing) 49.36 kB +0.24% +116 B 🔺
@sentry/svelte 29.96 kB +0.5% +147 B 🔺
CDN Bundle 32.01 kB +0.37% +116 B 🔺
CDN Bundle (incl. Tracing) 47.74 kB +0.36% +167 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.56 kB +0.33% +109 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 49.09 kB +0.23% +108 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 72.91 kB +0.15% +109 B 🔺
CDN Bundle (incl. Tracing, Replay) 85.36 kB +0.16% +132 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.66 kB +0.16% +133 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.16 kB +0.13% +117 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.47 kB +0.12% +110 B 🔺
CDN Bundle - uncompressed 95.52 kB +0.77% +727 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 143.11 kB +0.52% +735 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.23 kB +0.74% +727 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 147.09 kB +0.51% +735 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 224.99 kB +0.33% +727 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 262.37 kB +0.29% +735 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 266.33 kB +0.28% +735 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 276.07 kB +0.27% +735 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 280.03 kB +0.27% +735 B 🔺
@sentry/nextjs (client) 52.22 kB +0.28% +145 B 🔺
@sentry/sveltekit (client) 47.82 kB +0.27% +126 B 🔺
@sentry/core/server 80 kB +0.15% +117 B 🔺
@sentry/core/browser 51.96 kB +0.24% +123 B 🔺
@sentry/node 120.89 kB +0.2% +231 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 0 B added added
@sentry/node - without tracing 84.26 kB +0.32% +264 B 🔺
@sentry/aws-serverless 92.94 kB +0.3% +271 B 🔺
@sentry/cloudflare (withSentry) - minified 219.37 kB +0.39% +835 B 🔺
@sentry/cloudflare (withSentry) 540.55 kB +0.41% +2.21 kB 🔺

View base workflow run

@isaacs
isaacs force-pushed the fn/bun-outgoing-fetch-instrumentation branch from 3ba0b22 to 272b05c Compare July 31, 2026 17:02
@JPeer264
JPeer264 force-pushed the fn/bun-outgoing-fetch-instrumentation branch from 272b05c to 8732c1e Compare August 4, 2026 07:11
@JPeer264
JPeer264 requested a review from a team as a code owner August 4, 2026 07:11
mydea and others added 7 commits August 4, 2026 13:30
Add a `fetchIntegration` to `@sentry/bun` that patches the global `fetch` to
create `http.client` spans, record breadcrumbs, and inject trace-propagation
headers. Bun implements `fetch` natively rather than through undici, so the
Node SDK's `nativeNodeFetchIntegration` (which subscribes to undici's
`diagnostics_channel` events) never fires. The new integration mirrors the
Cloudflare approach and replaces `nativeNodeFetchIntegration` in the Bun
default integrations.

Also fix an inverted guard in core's `instrumentFetch`: `skipNativeFetchCheck`
did the opposite of its name — passing `true` performed the browser-only native
check, while the default skipped it. Only Cloudflare/Bun passed `true`, so they
were the only callers that ran the check. The guard now reads
`!skipNativeFetchCheck && !supportsNativeFetch()` so the parameter matches its
name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: isaacs <i@izs.me>
@JPeer264
JPeer264 force-pushed the fn/bun-outgoing-fetch-instrumentation branch from 6638540 to 386c63d Compare August 4, 2026 10:30
@JPeer264
JPeer264 merged commit c0f8510 into develop Aug 4, 2026
517 of 519 checks passed
@JPeer264
JPeer264 deleted the fn/bun-outgoing-fetch-instrumentation branch August 4, 2026 11:58
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