Implement alternative IITM/RITM wrapping utility #20171

Description

@linear-code

Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

Goal

Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


How OTel Instrumentation Works Today

Data Flow

  1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

  2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

    newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
  3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

    • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
    • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
  4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

    • Reads baseDir/package.json to get the package version
    • Checks version against supportedVersions using OTel's lightweight semver
    • If matched and enabled, calls module.patch(exports, version)
  5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

The shimmer layer

OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

  1. Saves original as obj[name]
  2. Calls wrapper(original) to get wrapped version
  3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
  4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

--> We can likely replace this with our own fill method or something similar

Key OTel dependencies involved

  • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
  • require-in-the-middle — CJS hook
  • import-in-the-middle — ESM hook
  • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
  • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

How Sentry Uses This Today

Three patterns for instrumentations

Pattern A: Third-party OTel instrumentation (direct usage)
~15+ integrations (connect, pg, mongodb, redis, etc.):

exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

Pattern B: Sentry-written class extendingInstrumentationBase
~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

Pattern C: Diagnostics channel (no real module patching)
SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

The init flow

  1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
  2. initOpenTelemetry() sets up OTel providers
  3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
  4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
  5. User code require('connect') / import 'connect' triggers hook → version check → patch()

Key Sentry files

FileRole
packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
packages/opentelemetry/src/OTel provider setup

Proposed Design

New API: registerModuleWrapper()

interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

New file structure

packages/node-core/src/module-wrapper/
index.ts — registerModuleWrapper() + public API
singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
semver.ts — vendored lightweight semver satisfies()
shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
version.ts — extractPackageVersion(baseDir) helper

What gets vendored from OTel

ComponentSourceSizeNotes
RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

What does NOT get vendored

  • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
  • InstrumentationAbstract (we write our own simpler base class)
  • registerInstrumentations() (replaced by registerModuleWrapper())
  • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

Migration Strategy

Step 1: Build the primitives

Create packages/node-core/src/module-wrapper/ with:

  1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
  2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
  3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
  4. index.tsregisterModuleWrapper() implementation:
    • Register with RITM singleton for CJS
    • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
    • On hook invocation: extract version → check semver → call patch()

Step 2: Create SentryInstrumentationBase (compatibility layer)

A drop-in replacement for OTel's InstrumentationBase that:

  • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
  • Uses registerModuleWrapper() internally instead of OTel's enable() loop
  • Provides _wrap / isWrapped from our vendored shimmer

This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

Step 3: Migrate Pattern B instrumentations

Change all ~16 Sentry-written instrumentations:

- import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

Step 4: Migrate Pattern A instrumentations (vendored third-party)

For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

// Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

Step 5: Update generateInstrumentOnce

Currently calls registerInstrumentations(). Update to:

  • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
  • Eventually remove OTel registerInstrumentations() call entirely

Step 6: Update ensureIsWrapped

Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

Step 7: Remove @opentelemetry/instrumentation dependency

Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


Potential Challenges

  1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
  2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
  3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
  4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
  5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
  6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
  7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

Order of Execution

PhaseWhatDepends On
1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
2Implement registerModuleWrapper()Phase 1
3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
4Update ensureIsWrapped to use vendored shimmerPhase 1
5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
6Vendor remaining Pattern A instrumentations (like connect)Phase 2
7Update generateInstrumentOnce to support new APIPhase 2
8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
9Remove @opentelemetry/instrumentation dependencyAll above

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Implement alternative IITM/RITM wrapping utility #20171

      Description

      @linear-code

      Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

      Goal

      Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


      How OTel Instrumentation Works Today

      Data Flow

      1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

      2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

        newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
      3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

        • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
        • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
      4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

        • Reads baseDir/package.json to get the package version
        • Checks version against supportedVersions using OTel's lightweight semver
        • If matched and enabled, calls module.patch(exports, version)
      5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

      The shimmer layer

      OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

      1. Saves original as obj[name]
      2. Calls wrapper(original) to get wrapped version
      3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
      4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

      --> We can likely replace this with our own fill method or something similar

      Key OTel dependencies involved

      • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
      • require-in-the-middle — CJS hook
      • import-in-the-middle — ESM hook
      • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
      • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

      How Sentry Uses This Today

      Three patterns for instrumentations

      Pattern A: Third-party OTel instrumentation (direct usage)
      ~15+ integrations (connect, pg, mongodb, redis, etc.):

      exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

      Pattern B: Sentry-written class extendingInstrumentationBase
      ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

      classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

      Pattern C: Diagnostics channel (no real module patching)
      SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

      The init flow

      1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
      2. initOpenTelemetry() sets up OTel providers
      3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
      4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
      5. User code require('connect') / import 'connect' triggers hook → version check → patch()

      Key Sentry files

      FileRole
      packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
      packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
      packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
      packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
      packages/opentelemetry/src/OTel provider setup

      Proposed Design

      New API: registerModuleWrapper()

      interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

      New file structure

      packages/node-core/src/module-wrapper/
      index.ts — registerModuleWrapper() + public API
      singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
      semver.ts — vendored lightweight semver satisfies()
      shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
      version.ts — extractPackageVersion(baseDir) helper
      

      What gets vendored from OTel

      ComponentSourceSizeNotes
      RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
      Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

      What does NOT get vendored

      • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
      • InstrumentationAbstract (we write our own simpler base class)
      • registerInstrumentations() (replaced by registerModuleWrapper())
      • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

      Migration Strategy

      Step 1: Build the primitives

      Create packages/node-core/src/module-wrapper/ with:

      1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
      2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
      3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
      4. index.tsregisterModuleWrapper() implementation:
        • Register with RITM singleton for CJS
        • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
        • On hook invocation: extract version → check semver → call patch()

      Step 2: Create SentryInstrumentationBase (compatibility layer)

      A drop-in replacement for OTel's InstrumentationBase that:

      • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
      • Uses registerModuleWrapper() internally instead of OTel's enable() loop
      • Provides _wrap / isWrapped from our vendored shimmer

      This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

      Step 3: Migrate Pattern B instrumentations

      Change all ~16 Sentry-written instrumentations:

      - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

      Step 4: Migrate Pattern A instrumentations (vendored third-party)

      For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

      // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

      This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

      Step 5: Update generateInstrumentOnce

      Currently calls registerInstrumentations(). Update to:

      • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
      • Eventually remove OTel registerInstrumentations() call entirely

      Step 6: Update ensureIsWrapped

      Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

      Step 7: Remove @opentelemetry/instrumentation dependency

      Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


      Potential Challenges

      1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
      2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
      3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
      4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
      5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
      6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
      7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

      Order of Execution

      PhaseWhatDepends On
      1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
      2Implement registerModuleWrapper()Phase 1
      3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
      4Update ensureIsWrapped to use vendored shimmerPhase 1
      5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
      6Vendor remaining Pattern A instrumentations (like connect)Phase 2
      7Update generateInstrumentOnce to support new APIPhase 2
      8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
      9Remove @opentelemetry/instrumentation dependencyAll above

      Metadata

      Metadata

      Assignees

      No one assigned

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Implement alternative IITM/RITM wrapping utility #20171

          Description

          @linear-code

          Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

          Goal

          Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


          How OTel Instrumentation Works Today

          Data Flow

          1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

          2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

            newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
          3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

            • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
            • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
          4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

            • Reads baseDir/package.json to get the package version
            • Checks version against supportedVersions using OTel's lightweight semver
            • If matched and enabled, calls module.patch(exports, version)
          5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

          The shimmer layer

          OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

          1. Saves original as obj[name]
          2. Calls wrapper(original) to get wrapped version
          3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
          4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

          --> We can likely replace this with our own fill method or something similar

          Key OTel dependencies involved

          • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
          • require-in-the-middle — CJS hook
          • import-in-the-middle — ESM hook
          • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
          • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

          How Sentry Uses This Today

          Three patterns for instrumentations

          Pattern A: Third-party OTel instrumentation (direct usage)
          ~15+ integrations (connect, pg, mongodb, redis, etc.):

          exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

          Pattern B: Sentry-written class extendingInstrumentationBase
          ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

          classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

          Pattern C: Diagnostics channel (no real module patching)
          SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

          The init flow

          1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
          2. initOpenTelemetry() sets up OTel providers
          3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
          4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
          5. User code require('connect') / import 'connect' triggers hook → version check → patch()

          Key Sentry files

          FileRole
          packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
          packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
          packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
          packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
          packages/opentelemetry/src/OTel provider setup

          Proposed Design

          New API: registerModuleWrapper()

          interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

          New file structure

          packages/node-core/src/module-wrapper/
          index.ts — registerModuleWrapper() + public API
          singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
          semver.ts — vendored lightweight semver satisfies()
          shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
          version.ts — extractPackageVersion(baseDir) helper
          

          What gets vendored from OTel

          ComponentSourceSizeNotes
          RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
          Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

          What does NOT get vendored

          • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
          • InstrumentationAbstract (we write our own simpler base class)
          • registerInstrumentations() (replaced by registerModuleWrapper())
          • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

          Migration Strategy

          Step 1: Build the primitives

          Create packages/node-core/src/module-wrapper/ with:

          1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
          2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
          3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
          4. index.tsregisterModuleWrapper() implementation:
            • Register with RITM singleton for CJS
            • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
            • On hook invocation: extract version → check semver → call patch()

          Step 2: Create SentryInstrumentationBase (compatibility layer)

          A drop-in replacement for OTel's InstrumentationBase that:

          • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
          • Uses registerModuleWrapper() internally instead of OTel's enable() loop
          • Provides _wrap / isWrapped from our vendored shimmer

          This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

          Step 3: Migrate Pattern B instrumentations

          Change all ~16 Sentry-written instrumentations:

          - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

          Step 4: Migrate Pattern A instrumentations (vendored third-party)

          For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

          // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

          This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

          Step 5: Update generateInstrumentOnce

          Currently calls registerInstrumentations(). Update to:

          • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
          • Eventually remove OTel registerInstrumentations() call entirely

          Step 6: Update ensureIsWrapped

          Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

          Step 7: Remove @opentelemetry/instrumentation dependency

          Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


          Potential Challenges

          1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
          2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
          3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
          4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
          5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
          6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
          7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

          Order of Execution

          PhaseWhatDepends On
          1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
          2Implement registerModuleWrapper()Phase 1
          3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
          4Update ensureIsWrapped to use vendored shimmerPhase 1
          5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
          6Vendor remaining Pattern A instrumentations (like connect)Phase 2
          7Update generateInstrumentOnce to support new APIPhase 2
          8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
          9Remove @opentelemetry/instrumentation dependencyAll above

          Metadata

          Metadata

          Assignees

          No one assigned

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Implement alternative IITM/RITM wrapping utility #20171

              Description

              @linear-code

              Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

              Goal

              Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


              How OTel Instrumentation Works Today

              Data Flow

              1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

              2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

                newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
              3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

                • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
                • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
              4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

                • Reads baseDir/package.json to get the package version
                • Checks version against supportedVersions using OTel's lightweight semver
                • If matched and enabled, calls module.patch(exports, version)
              5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

              The shimmer layer

              OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

              1. Saves original as obj[name]
              2. Calls wrapper(original) to get wrapped version
              3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
              4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

              --> We can likely replace this with our own fill method or something similar

              Key OTel dependencies involved

              • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
              • require-in-the-middle — CJS hook
              • import-in-the-middle — ESM hook
              • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
              • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

              How Sentry Uses This Today

              Three patterns for instrumentations

              Pattern A: Third-party OTel instrumentation (direct usage)
              ~15+ integrations (connect, pg, mongodb, redis, etc.):

              exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

              Pattern B: Sentry-written class extendingInstrumentationBase
              ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

              classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

              Pattern C: Diagnostics channel (no real module patching)
              SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

              The init flow

              1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
              2. initOpenTelemetry() sets up OTel providers
              3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
              4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
              5. User code require('connect') / import 'connect' triggers hook → version check → patch()

              Key Sentry files

              FileRole
              packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
              packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
              packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
              packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
              packages/opentelemetry/src/OTel provider setup

              Proposed Design

              New API: registerModuleWrapper()

              interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

              New file structure

              packages/node-core/src/module-wrapper/
              index.ts — registerModuleWrapper() + public API
              singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
              semver.ts — vendored lightweight semver satisfies()
              shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
              version.ts — extractPackageVersion(baseDir) helper
              

              What gets vendored from OTel

              ComponentSourceSizeNotes
              RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
              Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

              What does NOT get vendored

              • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
              • InstrumentationAbstract (we write our own simpler base class)
              • registerInstrumentations() (replaced by registerModuleWrapper())
              • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

              Migration Strategy

              Step 1: Build the primitives

              Create packages/node-core/src/module-wrapper/ with:

              1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
              2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
              3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
              4. index.tsregisterModuleWrapper() implementation:
                • Register with RITM singleton for CJS
                • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
                • On hook invocation: extract version → check semver → call patch()

              Step 2: Create SentryInstrumentationBase (compatibility layer)

              A drop-in replacement for OTel's InstrumentationBase that:

              • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
              • Uses registerModuleWrapper() internally instead of OTel's enable() loop
              • Provides _wrap / isWrapped from our vendored shimmer

              This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

              Step 3: Migrate Pattern B instrumentations

              Change all ~16 Sentry-written instrumentations:

              - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

              Step 4: Migrate Pattern A instrumentations (vendored third-party)

              For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

              // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

              This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

              Step 5: Update generateInstrumentOnce

              Currently calls registerInstrumentations(). Update to:

              • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
              • Eventually remove OTel registerInstrumentations() call entirely

              Step 6: Update ensureIsWrapped

              Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

              Step 7: Remove @opentelemetry/instrumentation dependency

              Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


              Potential Challenges

              1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
              2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
              3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
              4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
              5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
              6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
              7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

              Order of Execution

              PhaseWhatDepends On
              1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
              2Implement registerModuleWrapper()Phase 1
              3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
              4Update ensureIsWrapped to use vendored shimmerPhase 1
              5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
              6Vendor remaining Pattern A instrumentations (like connect)Phase 2
              7Update generateInstrumentOnce to support new APIPhase 2
              8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
              9Remove @opentelemetry/instrumentation dependencyAll above

              Metadata

              Metadata

              Assignees

              No one assigned

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Implement alternative IITM/RITM wrapping utility #20171

                  Description

                  @linear-code

                  Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

                  Goal

                  Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


                  How OTel Instrumentation Works Today

                  Data Flow

                  1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

                  2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

                    newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
                  3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

                    • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
                    • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
                  4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

                    • Reads baseDir/package.json to get the package version
                    • Checks version against supportedVersions using OTel's lightweight semver
                    • If matched and enabled, calls module.patch(exports, version)
                  5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

                  The shimmer layer

                  OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

                  1. Saves original as obj[name]
                  2. Calls wrapper(original) to get wrapped version
                  3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
                  4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

                  --> We can likely replace this with our own fill method or something similar

                  Key OTel dependencies involved

                  • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
                  • require-in-the-middle — CJS hook
                  • import-in-the-middle — ESM hook
                  • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
                  • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

                  How Sentry Uses This Today

                  Three patterns for instrumentations

                  Pattern A: Third-party OTel instrumentation (direct usage)
                  ~15+ integrations (connect, pg, mongodb, redis, etc.):

                  exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

                  Pattern B: Sentry-written class extendingInstrumentationBase
                  ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

                  classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

                  Pattern C: Diagnostics channel (no real module patching)
                  SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

                  The init flow

                  1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
                  2. initOpenTelemetry() sets up OTel providers
                  3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
                  4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
                  5. User code require('connect') / import 'connect' triggers hook → version check → patch()

                  Key Sentry files

                  FileRole
                  packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
                  packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
                  packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
                  packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
                  packages/opentelemetry/src/OTel provider setup

                  Proposed Design

                  New API: registerModuleWrapper()

                  interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

                  New file structure

                  packages/node-core/src/module-wrapper/
                  index.ts — registerModuleWrapper() + public API
                  singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
                  semver.ts — vendored lightweight semver satisfies()
                  shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
                  version.ts — extractPackageVersion(baseDir) helper
                  

                  What gets vendored from OTel

                  ComponentSourceSizeNotes
                  RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
                  Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

                  What does NOT get vendored

                  • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
                  • InstrumentationAbstract (we write our own simpler base class)
                  • registerInstrumentations() (replaced by registerModuleWrapper())
                  • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

                  Migration Strategy

                  Step 1: Build the primitives

                  Create packages/node-core/src/module-wrapper/ with:

                  1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
                  2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
                  3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
                  4. index.tsregisterModuleWrapper() implementation:
                    • Register with RITM singleton for CJS
                    • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
                    • On hook invocation: extract version → check semver → call patch()

                  Step 2: Create SentryInstrumentationBase (compatibility layer)

                  A drop-in replacement for OTel's InstrumentationBase that:

                  • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
                  • Uses registerModuleWrapper() internally instead of OTel's enable() loop
                  • Provides _wrap / isWrapped from our vendored shimmer

                  This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

                  Step 3: Migrate Pattern B instrumentations

                  Change all ~16 Sentry-written instrumentations:

                  - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

                  Step 4: Migrate Pattern A instrumentations (vendored third-party)

                  For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

                  // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

                  This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

                  Step 5: Update generateInstrumentOnce

                  Currently calls registerInstrumentations(). Update to:

                  • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
                  • Eventually remove OTel registerInstrumentations() call entirely

                  Step 6: Update ensureIsWrapped

                  Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

                  Step 7: Remove @opentelemetry/instrumentation dependency

                  Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


                  Potential Challenges

                  1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
                  2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
                  3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
                  4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
                  5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
                  6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
                  7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

                  Order of Execution

                  PhaseWhatDepends On
                  1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
                  2Implement registerModuleWrapper()Phase 1
                  3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
                  4Update ensureIsWrapped to use vendored shimmerPhase 1
                  5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
                  6Vendor remaining Pattern A instrumentations (like connect)Phase 2
                  7Update generateInstrumentOnce to support new APIPhase 2
                  8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
                  9Remove @opentelemetry/instrumentation dependencyAll above

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Implement alternative IITM/RITM wrapping utility #20171

                      Description

                      @linear-code

                      Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

                      Goal

                      Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


                      How OTel Instrumentation Works Today

                      Data Flow

                      1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

                      2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

                        newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
                      3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

                        • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
                        • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
                      4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

                        • Reads baseDir/package.json to get the package version
                        • Checks version against supportedVersions using OTel's lightweight semver
                        • If matched and enabled, calls module.patch(exports, version)
                      5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

                      The shimmer layer

                      OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

                      1. Saves original as obj[name]
                      2. Calls wrapper(original) to get wrapped version
                      3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
                      4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

                      --> We can likely replace this with our own fill method or something similar

                      Key OTel dependencies involved

                      • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
                      • require-in-the-middle — CJS hook
                      • import-in-the-middle — ESM hook
                      • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
                      • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

                      How Sentry Uses This Today

                      Three patterns for instrumentations

                      Pattern A: Third-party OTel instrumentation (direct usage)
                      ~15+ integrations (connect, pg, mongodb, redis, etc.):

                      exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

                      Pattern B: Sentry-written class extendingInstrumentationBase
                      ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

                      classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

                      Pattern C: Diagnostics channel (no real module patching)
                      SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

                      The init flow

                      1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
                      2. initOpenTelemetry() sets up OTel providers
                      3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
                      4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
                      5. User code require('connect') / import 'connect' triggers hook → version check → patch()

                      Key Sentry files

                      FileRole
                      packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
                      packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
                      packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
                      packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
                      packages/opentelemetry/src/OTel provider setup

                      Proposed Design

                      New API: registerModuleWrapper()

                      interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

                      New file structure

                      packages/node-core/src/module-wrapper/
                      index.ts — registerModuleWrapper() + public API
                      singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
                      semver.ts — vendored lightweight semver satisfies()
                      shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
                      version.ts — extractPackageVersion(baseDir) helper
                      

                      What gets vendored from OTel

                      ComponentSourceSizeNotes
                      RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
                      Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

                      What does NOT get vendored

                      • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
                      • InstrumentationAbstract (we write our own simpler base class)
                      • registerInstrumentations() (replaced by registerModuleWrapper())
                      • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

                      Migration Strategy

                      Step 1: Build the primitives

                      Create packages/node-core/src/module-wrapper/ with:

                      1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
                      2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
                      3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
                      4. index.tsregisterModuleWrapper() implementation:
                        • Register with RITM singleton for CJS
                        • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
                        • On hook invocation: extract version → check semver → call patch()

                      Step 2: Create SentryInstrumentationBase (compatibility layer)

                      A drop-in replacement for OTel's InstrumentationBase that:

                      • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
                      • Uses registerModuleWrapper() internally instead of OTel's enable() loop
                      • Provides _wrap / isWrapped from our vendored shimmer

                      This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

                      Step 3: Migrate Pattern B instrumentations

                      Change all ~16 Sentry-written instrumentations:

                      - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

                      Step 4: Migrate Pattern A instrumentations (vendored third-party)

                      For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

                      // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

                      This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

                      Step 5: Update generateInstrumentOnce

                      Currently calls registerInstrumentations(). Update to:

                      • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
                      • Eventually remove OTel registerInstrumentations() call entirely

                      Step 6: Update ensureIsWrapped

                      Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

                      Step 7: Remove @opentelemetry/instrumentation dependency

                      Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


                      Potential Challenges

                      1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
                      2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
                      3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
                      4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
                      5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
                      6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
                      7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

                      Order of Execution

                      PhaseWhatDepends On
                      1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
                      2Implement registerModuleWrapper()Phase 1
                      3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
                      4Update ensureIsWrapped to use vendored shimmerPhase 1
                      5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
                      6Vendor remaining Pattern A instrumentations (like connect)Phase 2
                      7Update generateInstrumentOnce to support new APIPhase 2
                      8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
                      9Remove @opentelemetry/instrumentation dependencyAll above

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Implement alternative IITM/RITM wrapping utility #20171

                          Description

                          @linear-code

                          Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

                          Goal

                          Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


                          How OTel Instrumentation Works Today

                          Data Flow

                          1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

                          2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

                            newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
                          3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

                            • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
                            • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
                          4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

                            • Reads baseDir/package.json to get the package version
                            • Checks version against supportedVersions using OTel's lightweight semver
                            • If matched and enabled, calls module.patch(exports, version)
                          5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

                          The shimmer layer

                          OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

                          1. Saves original as obj[name]
                          2. Calls wrapper(original) to get wrapped version
                          3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
                          4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

                          --> We can likely replace this with our own fill method or something similar

                          Key OTel dependencies involved

                          • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
                          • require-in-the-middle — CJS hook
                          • import-in-the-middle — ESM hook
                          • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
                          • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

                          How Sentry Uses This Today

                          Three patterns for instrumentations

                          Pattern A: Third-party OTel instrumentation (direct usage)
                          ~15+ integrations (connect, pg, mongodb, redis, etc.):

                          exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

                          Pattern B: Sentry-written class extendingInstrumentationBase
                          ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

                          classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

                          Pattern C: Diagnostics channel (no real module patching)
                          SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

                          The init flow

                          1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
                          2. initOpenTelemetry() sets up OTel providers
                          3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
                          4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
                          5. User code require('connect') / import 'connect' triggers hook → version check → patch()

                          Key Sentry files

                          FileRole
                          packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
                          packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
                          packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
                          packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
                          packages/opentelemetry/src/OTel provider setup

                          Proposed Design

                          New API: registerModuleWrapper()

                          interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

                          New file structure

                          packages/node-core/src/module-wrapper/
                          index.ts — registerModuleWrapper() + public API
                          singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
                          semver.ts — vendored lightweight semver satisfies()
                          shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
                          version.ts — extractPackageVersion(baseDir) helper
                          

                          What gets vendored from OTel

                          ComponentSourceSizeNotes
                          RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
                          Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

                          What does NOT get vendored

                          • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
                          • InstrumentationAbstract (we write our own simpler base class)
                          • registerInstrumentations() (replaced by registerModuleWrapper())
                          • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

                          Migration Strategy

                          Step 1: Build the primitives

                          Create packages/node-core/src/module-wrapper/ with:

                          1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
                          2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
                          3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
                          4. index.tsregisterModuleWrapper() implementation:
                            • Register with RITM singleton for CJS
                            • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
                            • On hook invocation: extract version → check semver → call patch()

                          Step 2: Create SentryInstrumentationBase (compatibility layer)

                          A drop-in replacement for OTel's InstrumentationBase that:

                          • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
                          • Uses registerModuleWrapper() internally instead of OTel's enable() loop
                          • Provides _wrap / isWrapped from our vendored shimmer

                          This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

                          Step 3: Migrate Pattern B instrumentations

                          Change all ~16 Sentry-written instrumentations:

                          - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

                          Step 4: Migrate Pattern A instrumentations (vendored third-party)

                          For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

                          // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

                          This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

                          Step 5: Update generateInstrumentOnce

                          Currently calls registerInstrumentations(). Update to:

                          • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
                          • Eventually remove OTel registerInstrumentations() call entirely

                          Step 6: Update ensureIsWrapped

                          Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

                          Step 7: Remove @opentelemetry/instrumentation dependency

                          Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


                          Potential Challenges

                          1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
                          2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
                          3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
                          4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
                          5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
                          6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
                          7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

                          Order of Execution

                          PhaseWhatDepends On
                          1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
                          2Implement registerModuleWrapper()Phase 1
                          3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
                          4Update ensureIsWrapped to use vendored shimmerPhase 1
                          5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
                          6Vendor remaining Pattern A instrumentations (like connect)Phase 2
                          7Update generateInstrumentOnce to support new APIPhase 2
                          8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
                          9Remove @opentelemetry/instrumentation dependencyAll above

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Implement alternative IITM/RITM wrapping utility #20171

                              Description

                              @linear-code

                              Today, we rely on OTELs registerInstrumentation() to register monkey patching for our Node SDK. While there will continue to be a way to use OTEL, we want to also allow a more generic way to register instrumentation without OTEL as well.

                              Goal

                              Replace @opentelemetry/instrumentation's InstrumentationBase + registerInstrumentations() with a Sentry-owned registerModuleWrapper() primitive. This decouples Sentry's module-patching mechanism from the OTel instrumentation infrastructure while keeping the same underlying IITM/RITM hooks.


                              How OTel Instrumentation Works Today

                              Data Flow

                              1. Construction: new ConnectInstrumentation() calls InstrumentationBase constructor, which calls this.init() to get InstrumentationNodeModuleDefinition[], stores them in this._modules, then calls this.enable().

                              2. init() return value: Each instrumentation defines its targets via InstrumentationNodeModuleDefinition:

                                newInstrumentationNodeModuleDefinition('connect',// module name['>=3.0.0 <4'],// supported versions (semver)(exports)=>patch(exports),// patch function(exports)=>unpatch(exports)// unpatch function (optional))
                              3. enable() sets up hooks (the critical piece, in @opentelemetry/instrumentation/build/src/platform/node/instrumentation.js):

                                • CJS: Registers with RequireInTheMiddleSingleton (a singleton require-in-the-middle Hook with trie-based module matching)
                                • ESM: Creates new import-in-the-middle.Hook([moduleName], { internals: true }, hookFn)
                              4. Hook fires on require/import: Calls _onRequire(module, exports, name, baseDir) which:

                                • Reads baseDir/package.json to get the package version
                                • Checks version against supportedVersions using OTel's lightweight semver
                                • If matched and enabled, calls module.patch(exports, version)
                              5. registerInstrumentations(): Top-level API that iterates instrumentations and calls enable() on each.

                              The shimmer layer

                              OTel has its own shimmer (~120 lines, not the npm shimmer package). _wrap(obj, name, wrapper):

                              1. Saves original as obj[name]
                              2. Calls wrapper(original) to get wrapped version
                              3. Sets markers (e.g. __original, __wrapped) so isWrapped() can detect patched exports
                              4. The Node-specific InstrumentationBase overrides _wrap to handle ESM Proxy objects

                              --> We can likely replace this with our own fill method or something similar

                              Key OTel dependencies involved

                              • @opentelemetry/instrumentationInstrumentationBase, InstrumentationNodeModuleDefinition, shimmer, registerInstrumentations()
                              • require-in-the-middle — CJS hook
                              • import-in-the-middle — ESM hook
                              • OTel's internal semver.js (~440 lines, lightweight, no npm semver dep)
                              • OTel's internal RequireInTheMiddleSingleton + ModuleNameTrie — singleton RITM with efficient module name matching

                              How Sentry Uses This Today

                              Three patterns for instrumentations

                              Pattern A: Third-party OTel instrumentation (direct usage)
                              ~15+ integrations (connect, pg, mongodb, redis, etc.):

                              exportconstinstrumentConnect=generateInstrumentOnce(INTEGRATION_NAME,()=>newConnectInstrumentation());

                              Pattern B: Sentry-written class extendingInstrumentationBase
                              ~16 instrumentations (Express, Hono, FastifyV3, OpenAI, Anthropic, Vercel AI, LangChain, PostgresJs, Firebase, NestJS, Remix, etc.):

                              classSentryExpressInstrumentationextendsInstrumentationBase{init(): InstrumentationNodeModuleDefinition[]{ ... }}

                              Pattern C: Diagnostics channel (no real module patching)
                              SentryNodeFetchInstrumentation, SentryHttpInstrumentation — extend InstrumentationBase but return undefined from init() and use diagnostics_channel directly.

                              The init flow

                              1. Sentry.init()initNodeCore()initializeEsmLoader() (registers IITM loader hook via module.register())
                              2. initOpenTelemetry() sets up OTel providers
                              3. Integrations' setupOnce() calls instrumentXxx() (created via generateInstrumentOnce)
                              4. That calls registerInstrumentations()enable() → creates RITM + IITM hooks
                              5. User code require('connect') / import 'connect' triggers hook → version check → patch()

                              Key Sentry files

                              FileRole
                              packages/node-core/src/otel/instrument.tsgenerateInstrumentOnce() + registerInstrumentations() call site
                              packages/node-core/src/sdk/esmLoader.tsIITM loader hook registration (module.register())
                              packages/node-core/src/utils/ensureIsWrapped.tsUses isWrapped from @opentelemetry/instrumentation
                              packages/node/src/integrations/tracing/*.tsAll Pattern A/B/C instrumentations
                              packages/opentelemetry/src/OTel provider setup

                              Proposed Design

                              New API: registerModuleWrapper()

                              interfaceModuleWrapperOptions{moduleName: string;supportedVersions: string[];// semver ranges, e.g. ['>=3.0.0 <4']patch: (moduleExports: any,version?: string)=>any;files?: Array<{name: string;// relative path within the packagesupportedVersions: string[];patch: (exports: any,version?: string)=>any;}>;}functionregisterModuleWrapper(options: ModuleWrapperOptions): void;

                              New file structure

                              packages/node-core/src/module-wrapper/
                              index.ts — registerModuleWrapper() + public API
                              singleton.ts — RITM singleton (like OTel's RequireInTheMiddleSingleton + ModuleNameTrie)
                              semver.ts — vendored lightweight semver satisfies()
                              shimmer.ts — wrap + isWrapped utilities (with ESM Proxy awareness)
                              version.ts — extractPackageVersion(baseDir) helper
                              

                              What gets vendored from OTel

                              ComponentSourceSizeNotes
                              RITM singletonRequireInTheMiddleSingleton + ModuleNameTrie~150 linesSingle RITM hook with trie-based matching
                              Module version extractionInstrumentationBase._onRequire (partial)~30 linesRead package.json version from baseDir

                              What does NOT get vendored

                              • OTel tracer/meter/logger provider plumbing (not needed for module wrapping)
                              • InstrumentationAbstract (we write our own simpler base class)
                              • registerInstrumentations() (replaced by registerModuleWrapper())
                              • The diagnostics channel integration (Pattern C instrumentations don't need module wrapping)

                              Migration Strategy

                              Step 1: Build the primitives

                              Create packages/node-core/src/module-wrapper/ with:

                              1. shimmer.ts — Vendor OTel's shimmer with Proxy-awareness. Must set __wrapped / __original (or equivalent) so ensureIsWrapped() and isWrapped() keep working.
                              2. semver.ts — Vendor OTel's lightweight semver satisfies() or use something comparable
                              3. singleton.ts — RITM singleton with ModuleNameTrie. One global RITM hook, register module names into it.
                              4. index.tsregisterModuleWrapper() implementation:
                                • Register with RITM singleton for CJS
                                • Create new iitm.Hook() for ESM (leveraging Sentry's already-registered loader from initializeEsmLoader())
                                • On hook invocation: extract version → check semver → call patch()

                              Step 2: Create SentryInstrumentationBase (compatibility layer)

                              A drop-in replacement for OTel's InstrumentationBase that:

                              • Keeps the same init()InstrumentationNodeModuleDefinition[] pattern
                              • Uses registerModuleWrapper() internally instead of OTel's enable() loop
                              • Provides _wrap / isWrapped from our vendored shimmer

                              This minimizes migration effort for Pattern B instrumentations — they just change extends InstrumentationBase to extends SentryInstrumentationBase.

                              Step 3: Migrate Pattern B instrumentations

                              Change all ~16 Sentry-written instrumentations:

                              - import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';+ import { SentryInstrumentationBase, InstrumentationNodeModuleDefinition } from '@sentry/node-core';- class SentryExpressInstrumentation extends InstrumentationBase {+ class SentryExpressInstrumentation extends SentryInstrumentationBase {

                              Step 4: Migrate Pattern A instrumentations (vendored third-party)

                              For each vendored instrumentation (like connect, which we've already started), the patch logic moves out of the OTel class and into a direct registerModuleWrapper() call:

                              // Before (using OTel InstrumentationBase)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>newConnectInstrumentation());// After (using registerModuleWrapper directly)exportconstinstrumentConnect=generateInstrumentOnce('Connect',()=>registerModuleWrapper({moduleName: 'connect',supportedVersions: ['>=3.0.0 <4'],patch: (moduleExports)=>patchConnect(moduleExports),}));

                              This requires generateInstrumentOnce to accept registrations that use registerModuleWrapper() (not only OTel Instrumentation objects).

                              Step 5: Update generateInstrumentOnce

                              Currently calls registerInstrumentations(). Update to:

                              • Accept both legacy Instrumentation objects (for any remaining OTel instrumentations during transition) and registerModuleWrapper()-based registrations
                              • Eventually remove OTel registerInstrumentations() call entirely

                              Step 6: Update ensureIsWrapped

                              Replace import of isWrapped from @opentelemetry/instrumentation with our own shimmer's isWrapped.

                              Step 7: Remove @opentelemetry/instrumentation dependency

                              Once all instrumentations are migrated, remove from @sentry/node-core and @sentry/node package.json.


                              Potential Challenges

                              1. ESM loader hook sharing: IITM's loader hook (registered by initializeEsmLoader()) must be active before any new iitm.Hook() is created. Current code already handles this ordering.
                              2. Dual RITM instances: If users load their own OTel instrumentations alongside Sentry, there will be two RITM hooks. This is unavoidable but acceptable — both will fire independently.
                              3. isWrapped() compatibility: Our shimmer must expose the same isWrapped() behavior (and compatible markers) as OTel's shimmer, so ensureIsWrapped() and any code that checks isWrapped() still works.
                              4. Pattern C instrumentations: These don't use module wrapping at all (init() returns undefined). They just need a simpler base class that provides a tracer — or they can be refactored to not extend any base class.
                              5. InstrumentationNodeModuleDefinition: All Pattern B instrumentations use this class. We should either vendor it (trivial — it's a simple data holder) or create our own equivalent with the same shape.
                              6. import-in-the-middle version coupling: IITM is already a direct Sentry dependency. No issue here.
                              7. require-in-the-middle: Currently a transitive dependency via @opentelemetry/instrumentation. After removing that dep, we'll need to add require-in-the-middle as a direct dependency of @sentry/node-core.

                              Order of Execution

                              PhaseWhatDepends On
                              1Vendor shimmer, semver, RITM singleton into module-wrapper/Nothing
                              2Implement registerModuleWrapper()Phase 1
                              3Create SentryInstrumentationBase using registerModuleWrapper()Phase 2
                              4Update ensureIsWrapped to use vendored shimmerPhase 1
                              5Migrate Pattern B instrumentations to SentryInstrumentationBasePhase 3
                              6Vendor remaining Pattern A instrumentations (like connect)Phase 2
                              7Update generateInstrumentOnce to support new APIPhase 2
                              8Migrate Pattern C instrumentations (simplify, drop base class)Phase 3
                              9Remove @opentelemetry/instrumentation dependencyAll above

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions