Skip to content

diagnostics_channel: add opt-in subscriber suppression via bypassBy and bypassed() - #63651

Open
DivyanshuX9 wants to merge 43 commits into
nodejs:mainfrom
DivyanshuX9:diag/suppression-als
Open

diagnostics_channel: add opt-in subscriber suppression via bypassBy and bypassed()#63651
DivyanshuX9 wants to merge 43 commits into
nodejs:mainfrom
DivyanshuX9:diag/suppression-als

Conversation

@DivyanshuX9

@DivyanshuX9 DivyanshuX9 commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds opt-in subscriber suppression to diagnostics_channel so APM
agents can prevent recursive instrumentation when their own internal
code calls into instrumented libraries.

What Changed

lib/diagnostics_channel.js

  • Added suppressed(key, fn, thisArg, ...args) - runs fn with
    the given key active in the current async context
  • subscribe() and bindStore() accept { subscriberId } option
  • Separate lazy storage for bypassed subscribers/stores - normal
    path has zero overhead, ALS is only hit when bypass subscribers exist
  • Lazy AsyncLocalStorage initialization via getSuppressionsStorage()
    • required because diagnostics_channel loads during bootstrap before
      async_hooks is available
  • Unified validateBypassKey() helper for all key validation

test/parallel/test-diagnostics-channel-suppression.js

  • 10 test scenarios covering sync, async, Promise, timer,
    store, nested, and type validation cases

Why

APM agents currently implement suppression themselves using a
custom ALS noop marker - every vendor reimplements the same
pattern differently. Moving it into core:

  • Eliminates per-handler overhead on the hot path
  • Gives vendors a standard API
  • Correctly propagates across Promise/timer boundaries via ALS

Backward Compatibility

  • Fully opt-in : existing code unaffected
  • Normal subscribers pay zero extra cost
  • ALS only initialized and queried when bypass subscribers exist

Naming

As per @bengl final name is "Bypass"

Addresses:#63623

DivyanshuX9 and others added 2 commits May 24, 2026 21:33
Defer non-critical warnings to the next event loop iteration when
can_call_into_js() returns false. This prevents crashes when V8
emits warnings during REPL preview evaluation or other contexts
where JavaScript execution is temporarily forbidden.

When a warning is emitted inside DisallowJavascriptExecutionScope,
ProcessEmitWarningGeneric cannot be called immediately. Instead,
use env->SetImmediate() to queue the warning emission for after
the scope exits. This preserves full warning formatting, deprecation
codes, and --redirect-warnings routing.

Signed-off-by: Divyanshu Sharma <Divyanshu88999@gmail.com>
Copilot AI review requested due to automatic review settings May 29, 2026 21:31
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. diagnostics_channel Issues and PRs related to diagnostics channel needs-ci PRs that need a full CI run. labels May 29, 2026
DivyanshuX9 added a commit to DivyanshuX9/node that referenced this pull request May 29, 2026
Refs: nodejs#63623

Refs: nodejs#63651
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 5b4110a to 8b122c2 Compare May 29, 2026 21:49

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@DivyanshuX9

DivyanshuX9 commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@BridgeAR @bengl @Qard , I am opening this as a implementation
of #63623. Test file is included. Happy to adjust the API shape
or implementation based on your feedback before CI runs.

Refs: nodejs#63623

Refs: nodejs#63651
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Refs: nodejs#63623

Refs: nodejs#63651
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 8b122c2 to e4aea85 Compare May 29, 2026 22:37
@codecov

codecov Bot commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.43590% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.13%. Comparing base (58795e9) to head (a86df32).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
lib/diagnostics_channel.js 97.43% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #63651      +/-   ##
==========================================
- Coverage   90.16%   90.13%   -0.03%     
==========================================
  Files         744      744              
  Lines      242518   242667     +149     
  Branches    45692    45723      +31     
==========================================
+ Hits       218663   218729      +66     
- Misses      15349    15443      +94     
+ Partials     8506     8495      -11     
Files with missing lines Coverage Δ
lib/diagnostics_channel.js 97.28% <97.43%> (+<0.01%) ⬆️

... and 33 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Tests are missing a lot of necessary common.mustCall(fn) wrappers.

Also, it seems like this was just vibe-coded without reviewing the output before submitting the PR. Please ensure it is in a good state and that the test suite and lint passes before submitting.

Comment thread lib/diagnostics_channel.js Outdated
Comment thread src/node_errors.cc
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Comment thread lib/diagnostics_channel.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9

DivyanshuX9 commented May 30, 2026

Copy link
Copy Markdown
Contributor Author

@Qard , thanks for the detailed review. So far the checklist of what i have fixed in the latest push:

[] Removed the impossible ALS try/catch fallback
[] Removed node_errors.cc changes as you suggested
[] Replaced all IIFE wrappers with plain blocks in tests
[] Added common.mustCall() / common.mustNotCall() to all handlers
[] Ran lint and tests locally , both passing

Ready for re-review and any changes if you want, when you have time please review it.

Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
Comment thread test/parallel/test-diagnostics-channel-suppression.js Outdated
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 0f6e780 to 8fa6fd8 Compare May 30, 2026 12:27
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 8fa6fd8 to b8194af Compare May 30, 2026 12:38
@Qard

Qard commented May 30, 2026

Copy link
Copy Markdown
Member

The node_errors.cc change still seems to be present.

…ove SetImmediate deferral)

Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9

DivyanshuX9 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@BridgeAR @bengl @rochdev
I opened the docs PR: #64250

Covers bypass(), bypassId on subscribe/bindStore/tracingChannel,the APM recursion use case, and multi-vendor coordination.
Both MJS and CJS examples throughout.

Tell if i'm missing somethin' or I need to add anything else

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

Docs are missing

Comment thread lib/diagnostics_channel.js Outdated
Comment thread lib/diagnostics_channel.js
Comment thread lib/diagnostics_channel.js Outdated
Comment thread lib/diagnostics_channel.js
Comment thread lib/diagnostics_channel.js
@DivyanshuX9

Copy link
Copy Markdown
Contributor Author

@Flarna docs are in PR: #64250
since it is going to be a continuation of this PR , after all the changes and reviews i'll update them.

@Flarna

Flarna commented Jul 3, 2026

Copy link
Copy Markdown
Member

I think implementation + tests + docs of a new public API should come in one PR.

@DivyanshuX9

Copy link
Copy Markdown
Contributor Author

@Flarna Well once BridgeAR also mentioned about doc

We definitely also need documentation for the feature.

at that time was not sure how far reviews will go ,also @rochdev wanted some examples rightaway, So made a seperate PR for that to keep this one clean , yes if i get a green signal to land this one will will update and merge that pr doc in this one.

Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9

Copy link
Copy Markdown
Contributor Author

@Flarna i have moved all the docs here

Comment thread doc/api/diagnostics_channel.md
Comment thread doc/api/diagnostics_channel.md Outdated
Comment thread doc/api/diagnostics_channel.md Outdated
Comment thread doc/api/diagnostics_channel.md Outdated
Comment thread doc/api/diagnostics_channel.md Outdated
Comment thread doc/api/diagnostics_channel.md Outdated
Comment thread doc/api/diagnostics_channel.md Outdated
Comment thread doc/api/diagnostics_channel.md Outdated
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 2ba3a7a to f7f593e Compare July 8, 2026 13:44
@DivyanshuX9

Copy link
Copy Markdown
Contributor Author

@Flarna , lowkey I have addressed all feedback in latest push:

  1. Added Stability: 1.1 - Active development to bypass()
  2. Removed bypass example from Overview (not a typical usage pattern)
  3. Rewrote key parameter description to clarify no single match required
  4. Rewrote description paragraph to focus on behavior, not internals
  5. Fixed channel name to http.client.request.start throughout
  6. Replaced repeated subscribe/bindStore bypass examples with link to bypass() docs
  7. Removed all the unrelated REPLACEME changes entry from tracingChannel.subscribe

Thank you for the thorough review.

@DivyanshuX9

DivyanshuX9 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Stability: 1.1 - Active development

also @Flarna at 251 line is this correct or should i just remove REPLACE ME with the stability:1.1 right away

@Flarna

Flarna commented Jul 8, 2026

Copy link
Copy Markdown
Member

No, the REPLACEME is changed by the release tooling when a release is made, see https://github.com/nodejs/node/blob/main/doc/contributing/api-documentation.md#writing-documentation

The stability index is just documentation and independent of that.

Comment thread doc/api/diagnostics_channel.md Outdated
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 2fccc37 to 58785bb Compare July 13, 2026 13:31
@DivyanshuX9

Copy link
Copy Markdown
Contributor Author

@BridgeAR following up on your performance concern, ran a microbenchmark on the normal-subscriber hot path (1,000,000 iterations, no bypass() active):

main branch delta
normal-only (no bypass sub) 5.77ms 7.97ms +2.2ms (+38%)
normal + bypass sub (bypass idle) 14.9ms 19.6ms +4.7ms (+32%)

Test 1 is the pure normal path with no bypass subscriber registered at all, the overhead is just the _bypassSubscribers !== null null-check plus the extra fields on the channel object, comes out to around 2.2ns per call.
Test 2 is when an APM tool has opted in but bypass() isn't currently active, getBypassStorage().getStore() runs on every publish anyway, that's where the bigger cost comes from (~4.7ns/call over baseline).
Before I touch anything wanted to ask, is the 2.2ns on the pure normal path within acceptable range for this feature, or do you want me to look at avoiding the hidden-class shape change? For test 2 I could try caching the getStore() result per microtask turn but that adds complexity, not sure if its worth it.
Happy to dig into either if you think it matters.

image image

Signed-off-by: DivyanshuX9 <divyanshu88999@gmail.com>
@DivyanshuX9
DivyanshuX9 force-pushed the diag/suppression-als branch from 81685dd to aa285c8 Compare July 24, 2026 16:24
DivyanshuX9 and others added 7 commits July 24, 2026 22:00
Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Three bad-merge fragments were committed without conflict markers:

- unsubscribe: spurious decRef/maybeMarkInactive before return false

- bindStore: stale bindStore(store, transform) fragment injected inside

  the else block of bindStore(store, transform, options), with missing

  closing braces for the else block and the method

- unbindStore: spurious delete/decRef/maybeMarkInactive before return false

All three were remnants of the pre-bypass single-path implementation

that were not removed during the bypass-feature merge.

Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
…s entries

Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
The subscriber buffer (dc_binding.subscribers) is replaced with a new

larger TypedArray when native channel storage grows past its initial

capacity. The old code destructured it once at module load:

  const { subscribers: subscriberCounts } = dc_binding;

After growth, dc_binding.subscribers points to the new array but

subscriberCounts still holds a reference to the old one. Any

subscribe/unsubscribe/bindStore/unbindStore call after growth would

increment/decrement the stale array, leaving the live array at zero.

Channel::HasSubscribers() reads from the live array and would return

false even with active subscribers, breaking the C++ test

NativeChannelsGrowSubscriberStorage.

Fix: remove the cached destructuring and access dc_binding.subscribers

directly at every call site. The comment above the binding already

stated this requirement; the code just did not follow it.

Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
ActiveChannel.publish() accessed this._subscribers with optional

chaining (subscribers?.length || 0). This is unnecessary because:

- _subscribers is always initialized to [] by markActive() before any

  ActiveChannel method can be called

- maybeMarkInactive() sets _subscribers back to undefined only after

  switching the prototype back to Channel.prototype, so ActiveChannel

  methods are never reachable with a nullish _subscribers

Use subscribers.length directly.

Signed-off-by: Divyanshu Sharma <divyanshu88999@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. diagnostics_channel Issues and PRs related to diagnostics channel needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants