Skip to content

feat(core): Add bindScopeToEmitter to bind a scope to an event emitter#21594

Open
mydea wants to merge 5 commits into
developfrom
feat/bind-scope-to-emitter
Open

feat(core): Add bindScopeToEmitter to bind a scope to an event emitter#21594
mydea wants to merge 5 commits into
developfrom
feat/bind-scope-to-emitter

Conversation

@mydea

@mydea mydea commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Adds a new bindScopeToEmitter(emitter, scope?) API to @sentry/core. It captures a scope (defaulting to the current scope) and patches the emitter's listener-registration methods so that any listener added afterwards runs with that scope — and therefore the active span — active, even when it fires later in a different async context.

This is useful when instrumenting APIs that hand back an event emitter (e.g. a streamed database query) whose 'data' / 'error' / 'end' listeners would otherwise lose the trace context. It mirrors the event-emitter behavior of OpenTelemetry's ContextManager.bind, scoped to emitters only (the function-binding path of OTel's bind is intentionally omitted — at any such call site you already hold the callback and can wrap it with withScope directly).

Behavior

  • Works with both Node.js EventEmitters (on, addListener, once, prependListener, prependOnceListener) and DOM EventTargets (addEventListener).
  • Removal methods (removeListener / off / removeEventListener) are patched so removals via the original listener reference find the wrapped listener; the addEventListener options argument is forwarded; non-function (EventListener-object) listeners pass through untouched.
  • Objects exposing none of these methods are returned untouched (browser-safe — no node:events import).
  • The isolation scope is intentionally not captured — it rides along with the active async context. Only the current scope (with its active span) is bound.

Surface

  • Exported from all SDKs (@sentry/core + node / node-core / browser, and the platform/framework re-exporters).

Note: I opted to not include this in CDN bundles right now, as this is really not that critical to browser stuff. We can always add it later if it seems necessary.

Tests

  • Unit (@sentry/core): 19 cases incl. explicit-scope arg and the DOM EventTarget / addEventListener path.
  • Node integration (public-api/bindScopeToEmitter): a listener firing in a fresh async context nests under the bound parent, with an unbound control on a separate trace.
  • Browser integration (tracing/bindScopeToEmitter): a real EventTarget + dispatchEvent, verified on the ESM build

The mysql instrumentation rewiring that consumes this API is kept on a separate branch and will follow once this lands.

🤖 Generated with Claude Code

Adds a new `bindScopeToEmitter(emitter, scope?)` API to core. It captures a
scope (defaulting to the current scope) and patches the emitter's listener
registration methods so that any listener added afterwards runs with that scope
— and therefore the active span — active, even when it fires later in a
different async context.

This is useful when instrumenting APIs that hand back an event emitter (e.g. a
streamed database query) whose `'data'`/`'error'`/`'end'` listeners would
otherwise lose the trace context. It mirrors the event-emitter behavior of
OpenTelemetry's `ContextManager.bind`, scoped to emitters only.

Works with both Node.js `EventEmitter`s (`on`, `addListener`, ...) and DOM
`EventTarget`s (`addEventListener`); the isolation scope is intentionally not
captured as it is carried along by the active async context.

Exported from all SDKs and covered by unit, node-integration and
browser-integration tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/core/src/tracing/bindScopeToEmitter.ts
@mydea mydea self-assigned this Jun 17, 2026
@mydea mydea marked this pull request as ready for review June 17, 2026 07:57
@mydea mydea requested review from a team as code owners June 17, 2026 07:57
@mydea mydea requested review from JPeer264, Lms24, andreiborza, chargome, logaretm and s1gr1d and removed request for a team June 17, 2026 07:57
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.4 kB - -
@sentry/browser - with treeshaking flags 25.84 kB - -
@sentry/browser (incl. Tracing) 45.7 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 47.94 kB - -
@sentry/browser (incl. Tracing, Profiling) 50.5 kB - -
@sentry/browser (incl. Tracing, Replay) 84.92 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 74.53 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 89.61 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 102.3 kB - -
@sentry/browser (incl. Feedback) 44.56 kB - -
@sentry/browser (incl. sendFeedback) 32.2 kB - -
@sentry/browser (incl. FeedbackAsync) 37.31 kB - -
@sentry/browser (incl. Metrics) 28.47 kB - -
@sentry/browser (incl. Logs) 28.71 kB - -
@sentry/browser (incl. Metrics & Logs) 29.4 kB - -
@sentry/react 29.2 kB - -
@sentry/react (incl. Tracing) 48 kB - -
@sentry/vue 32.42 kB - -
@sentry/vue (incl. Tracing) 47.59 kB - -
@sentry/svelte 27.42 kB - -
CDN Bundle 29.79 kB - -
CDN Bundle (incl. Tracing) 48.2 kB - -
CDN Bundle (incl. Logs, Metrics) 31.33 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.49 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.62 kB - -
CDN Bundle (incl. Tracing, Replay) 85.52 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 86.77 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 91.37 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.62 kB - -
CDN Bundle - uncompressed 88.59 kB - -
CDN Bundle (incl. Tracing) - uncompressed 145.8 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 93.29 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 149.77 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.12 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 264.67 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 268.63 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 278.37 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 282.31 kB - -
@sentry/nextjs (client) 50.45 kB - -
@sentry/sveltekit (client) 46.12 kB - -
@sentry/core/server 76.54 kB +0.62% +466 B 🔺
@sentry/core/browser 63.68 kB +0.73% +457 B 🔺
@sentry/node-core 61.73 kB +0.01% +1 B 🔺
@sentry/node 128.22 kB +0.01% +2 B 🔺
@sentry/node - without tracing 74.11 kB +0.01% +2 B 🔺
@sentry/aws-serverless 85.36 kB +0.01% +1 B 🔺
@sentry/cloudflare (withSentry) - minified 174.19 kB - -
@sentry/cloudflare (withSentry) 435.54 kB - -

View base workflow run

Comment thread packages/core/src/tracing/bindScopeToEmitter.ts
Keep the API on the npm packages only; CDN bundle size should not grow for a
Node-oriented helper. Skip the browser integration test in bundle mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/core/src/tracing/bindScopeToEmitter.ts Outdated
mydea and others added 2 commits June 17, 2026 11:34
…copeToEmitter

A listener can be registered for the same event more than once; each is an
independent registration in Node's EventEmitter. Storing a single wrapper per
(event, listener) meant later registrations overwrote earlier ones, so only the
most recent wrapper could be removed via the original reference and earlier ones
were orphaned. Keep a stack of wrappers and remove one per removeListener call.

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

@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 5f01351. Configure here.

Comment thread packages/core/src/tracing/bindScopeToEmitter.ts
Minting a fresh wrapper on every registration broke DOM EventTarget semantics:
addEventListener dedupes by (type, callback, capture), so distinct wrapper refs
defeated that idempotency (the listener fired once per call) and capture-aware
removal could drop the wrong wrapper. Reuse a single stable wrapper per listener
and forward the caller's options unchanged: the DOM dedupes correctly and Node's
EventEmitter still counts duplicate registrations (removable one-per-call). This
also subsumes the earlier per-event wrapper stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

1 participant