Skip to content

refactor(instrumentation): add browser-native InstrumentationBase and migrate instrumentations - #278

Draft
overbalance wants to merge 1 commit into
open-telemetry:mainfrom
embrace-io:overbalance/unify-instrumentation-enable-disable
Draft

refactor(instrumentation): add browser-native InstrumentationBase and migrate instrumentations#278
overbalance wants to merge 1 commit into
open-telemetry:mainfrom
embrace-io:overbalance/unify-instrumentation-enable-disable

Conversation

@overbalance

@overbalance overbalance commented May 14, 2026

Copy link
Copy Markdown
Contributor

Which problem is this PR solving?

Every instrumentation in this package extends InstrumentationBase from @opentelemetry/instrumentation. That base class is built for Node.js. It patches modules through require-in-the-middle, and it asks each instrumentation for module definitions in an init() method.

A browser has no module system to patch. So all of that code does nothing here. It only adds weight.

There are two more problems:

  1. OpenTelemetry plans to make @opentelemetry/instrumentation Node-only. The browser shim is a temporary workaround. See the noop-normalize note that points to [Instrumentation] Cannot build web project using webpack due to reference to nodejs core package (path) opentelemetry-js#4373.
  2. Under jsdom, the test suite loaded the Node version of the base class. Browser instrumentations were tested against Node behavior.

This PR gives the package its own base class for browsers.

Short description of the changes

Add src/InstrumentationBase/. The new class implements the public Instrumentation interface, so registerInstrumentations still accepts these objects. At runtime it needs only @opentelemetry/api and @opentelemetry/api-logs.

It keeps what the instrumentations use:

  • config, diag, tracer, meter and logger setup
  • the provider setters
  • _wrap

It drops all Node module patching: init(), getModuleDefinitions and require-in-the-middle. enable() and disable() stay abstract.

Wrap only, never unwrap

The class has _wrap but no _unwrap and no _massUnwrap.

Restoring a patched browser API is not safe. Other code may have wrapped the same method after us, and there is no reliable way to tear that down. So this package does not allow it. Leaving the method out enforces the rule by construction.

This changes what disable() means. It stops emission. It does not restore the global. An instrumentation can still remove its own listeners, its own PerformanceObserver and its own AbortController. That is unrelated to unwrapping.

Other changes

  • Migrate all nine instrumentations to the new base: console, errors, fetch, navigation, navigation-timing, resource-timing, user-action, web-vitals and xhr. Each one swaps the import and drops the dead init() override.
  • Copy safeExecuteInTheMiddle into src/utils/. It was the only runtime helper the instrumentations still took from @opentelemetry/instrumentation.
  • Shipped code now imports only types from @opentelemetry/instrumentation, Instrumentation and InstrumentationConfig. No runtime use of its InstrumentationBase is left. One test still imports registerInstrumentations from it.
  • Add #instrumentation-base to the imports map in package.json.

xhr: make a retry safe

XhrInstrumentation.enable() wraps open first, then send. If send fails because another script locked it, open stays wrapped. The old code called _unwrap here, and that method is now gone.

Leaving the patch in place is safe. enable() returns before it sets _isEnabled, so the wrapper passes every call straight through.

But a retry was not safe. _isXhrPatched is set only after both methods are wrapped, so a second enable() call wrapped open again. Each retry added another layer, and no layer could be removed. A new _isOpenPatched flag now guards the open wrap, so a retry does not add a layer.

Public API

No change. Constructors, config types and behavior stay the same.

Type of change

  • New feature (non-breaking change which adds functionality). It adds an internal base class for browsers. The public API does not change.

How Has This Been Tested?

  • npm run check (tsc and eslint) is clean across the workspace. This includes the examples-* and sandbox consumers, which confirms the public API did not change.

  • biome check is clean.

  • The full vitest suite passes: 324 tests in browser-instrumentation and 64 in browser-sdk. Every instrumentation now runs against the new base. fetch, xhr and web-vitals run in real Chromium.

  • New InstrumentationBase.test.ts covers:

    • the constructor enables by default
    • the constructor does not enable when enabled is false
    • setConfig applies the default
    • _wrap installs in place and keeps the original
    • setTracerProvider swaps the tracer
    • the class exposes no _unwrap
  • New xhr tests cover the case where open is wrapped and send then fails. They check that:

    • the open patch stays installed
    • the request still works and emits nothing
    • a retry does not add a second layer

    To make send fail for real, the test defines it as writable: false and keeps it configurable. The test can then undo it afterward.

  • Fixed a test isolation leak in the console test. Patches are install only, so one shared mock console collected a wrapper from every test and leaked records between them. Each test now starts from a fresh console.

Checklist:

  • Followed the style guidelines of this project
  • Unit tests have been added
  • Documentation has been updated

Comment thread packages/instrumentation/src/utils/InstrumentationBase.ts Outdated
*/
export abstract class InstrumentationBase<
ConfigType extends InstrumentationConfig = InstrumentationConfig,
> extends CoreInstrumentationBase<ConfigType> {

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.

I wonder if we could get rid of this core dependency altogether. (Maybe a separate PR)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I refactored entirely to remove it. See the description for more details re: Node.

Comment thread packages/instrumentation/src/utils/InstrumentationBase.ts Outdated
@overbalance
overbalance force-pushed the overbalance/unify-instrumentation-enable-disable branch 2 times, most recently from cb0c9fd to 4782d17 Compare June 5, 2026 00:15
@overbalance overbalance changed the title refactor(instrumentation): unify enable/disable lifecycle and default to disabled refactor(instrumentation): add browser-native InstrumentationBase and migrate instrumentations Jun 5, 2026
@overbalance
overbalance force-pushed the overbalance/unify-instrumentation-enable-disable branch from 4782d17 to 32bf3f8 Compare June 5, 2026 16:29
@overbalance
overbalance force-pushed the overbalance/unify-instrumentation-enable-disable branch from 32bf3f8 to f799326 Compare August 25, 2026 06:40
… migrate instrumentations

Signed-off-by: overbalance <overbalance@users.noreply.github.com>
@overbalance
overbalance force-pushed the overbalance/unify-instrumentation-enable-disable branch from f799326 to 33f8251 Compare September 4, 2026 05:29
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.

2 participants