You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[instrumentation] Consider IITM's include list (createAddHookMessageChannel) as the default for ESM, since wrapping every module breaks bundled apps #6984
This issue was researched and written by Claude (an AI agent) on behalf of @segevfiner, who reviewed it before filing. Findings below were verified against the installed packages on a real project — nothing is inferred from documentation alone.
Important
Overlap disclosed up front. This touches #4933 (which already shows createAddHookMessageChannel in a proposed bootstrap), shares a root cause with #6489 (IITM proxy holding stale snapshots), and is adjacent to #4845. I believe the ask here is distinct — default behaviour rather than documented bootstrap — but please close as a duplicate or fold it into #4933 if you disagree.
The problem
Registering @opentelemetry/instrumentation/hook.mjs rewrites every module in the graph. For bundled ESM applications that is not merely wasteful, it is a hard crash — and it happens with the hook alone, before any SDK is started.
import-in-the-middle replaces ESM live bindings with a snapshot taken when its wrapper evaluates. Bundlers emit lazily initialized bindings (esbuild's __esm helper, which rolldown also emits): var X; declared undefined and assigned later inside an init_*() function, relying on the live binding across a chunk boundary. The wrapper snapshots undefined, and a synchronous consumer reads that:
TypeError: Class extends value undefined is not a constructor or null
IITM does retry still-undefined exports, but only on a microtask and then at 0/10/50 ms, so a class X extends Y evaluated during module evaluation always loses that race. Filed upstream with a three-file reproduction that involves no OpenTelemetry and no bundler: nodejs/import-in-the-middle#280.
That crash is arguably IITM's to fix. What is in scope here is that OpenTelemetry chooses the configuration that triggers it, and doesn't have to.
Why the include list is a reasonable default for OTel specifically
In the common path, OpenTelemetry already knows the complete module set at sdk.start() — it is the code calling Hook() for each instrumentation. That is precisely the precondition IITM's include list requires. So the information needed to scope the hook is already in hand and simply isn't used:
@opentelemetry/auto-instrumentations-node/build/src/register.js (0.79.0) constructs NodeSDK and calls sdk.start() with no include channel.
The documented ESM bootstrap registers the hook bare, so everything is wrapped.
Concretely, on a bundled Fastify service this was the difference between crash-on-boot and working:
Dropping the hook instead is not an alternative: the service then boots but emits only dns.lookup spans, losing every request span.
Proposal
Default to the include list where OTel controls the bootstrap — @opentelemetry/auto-instrumentations-node/register first, and the recommended ESM bootstrap in the docs — with an opt-out for the dynamic case.
The tradeoff, stated honestly
Wrapping everything is not merely a default-by-accident; it buys something real. addHook replays IITM's toHook list, so an instrumentation enabled after a module was imported still patches it retroactively. With an include list, the loader must know a module before it is imported, so registerInstrumentations() called after startup would silently stop instrumenting already-loaded modules.
That argues for an escape hatch, not against the default:
The NodeSDK + fixed-instrumentation-list path — which is what register.js and the docs describe — has no late registration, so nothing is lost there.
Late registration is the exception and can keep wrap-everything behaviour behind an explicit option.
waitForAllMessagesAcknowledged() cannot be made implicit by IITM, since it can't know when an app has finished registering hooks — but OTel can know, because it owns sdk.start(). This asymmetry is the core of the argument: it is a default OTel can implement safely and IITM cannot.
Two smaller benefits come along with it: less transform work at startup, and no rewriting of application modules that no instrumentation will ever touch.
Note
This issue was researched and written by Claude (an AI agent) on behalf of @segevfiner, who reviewed it before filing. Findings below were verified against the installed packages on a real project — nothing is inferred from documentation alone.
Important
Overlap disclosed up front. This touches #4933 (which already shows
createAddHookMessageChannelin a proposed bootstrap), shares a root cause with #6489 (IITM proxy holding stale snapshots), and is adjacent to #4845. I believe the ask here is distinct — default behaviour rather than documented bootstrap — but please close as a duplicate or fold it into #4933 if you disagree.The problem
Registering
@opentelemetry/instrumentation/hook.mjsrewrites every module in the graph. For bundled ESM applications that is not merely wasteful, it is a hard crash — and it happens with the hook alone, before any SDK is started.import-in-the-middle replaces ESM live bindings with a snapshot taken when its wrapper evaluates. Bundlers emit lazily initialized bindings (esbuild's
__esmhelper, which rolldown also emits):var X;declared undefined and assigned later inside aninit_*()function, relying on the live binding across a chunk boundary. The wrapper snapshotsundefined, and a synchronous consumer reads that:IITM does retry still-undefined exports, but only on a microtask and then at 0/10/50 ms, so a
class X extends Yevaluated during module evaluation always loses that race. Filed upstream with a three-file reproduction that involves no OpenTelemetry and no bundler: nodejs/import-in-the-middle#280.That crash is arguably IITM's to fix. What is in scope here is that OpenTelemetry chooses the configuration that triggers it, and doesn't have to.
Why the include list is a reasonable default for OTel specifically
In the common path, OpenTelemetry already knows the complete module set at
sdk.start()— it is the code callingHook()for each instrumentation. That is precisely the precondition IITM's include list requires. So the information needed to scope the hook is already in hand and simply isn't used:@opentelemetry/auto-instrumentations-node/build/src/register.js(0.79.0) constructsNodeSDKand callssdk.start()with no include channel.Concretely, on a bundled Fastify service this was the difference between crash-on-boot and working:
With that change the app boots and instrumentation is fully live — not merely non-crashing:
Dropping the hook instead is not an alternative: the service then boots but emits only
dns.lookupspans, losing every request span.Proposal
Default to the include list where OTel controls the bootstrap —
@opentelemetry/auto-instrumentations-node/registerfirst, and the recommended ESM bootstrap in the docs — with an opt-out for the dynamic case.The tradeoff, stated honestly
Wrapping everything is not merely a default-by-accident; it buys something real.
addHookreplays IITM'stoHooklist, so an instrumentation enabled after a module was imported still patches it retroactively. With an include list, the loader must know a module before it is imported, soregisterInstrumentations()called after startup would silently stop instrumenting already-loaded modules.That argues for an escape hatch, not against the default:
NodeSDK+ fixed-instrumentation-list path — which is whatregister.jsand the docs describe — has no late registration, so nothing is lost there.waitForAllMessagesAcknowledged()cannot be made implicit by IITM, since it can't know when an app has finished registering hooks — but OTel can know, because it ownssdk.start(). This asymmetry is the core of the argument: it is a default OTel can implement safely and IITM cannot.Two smaller benefits come along with it: less transform work at startup, and no rewriting of application modules that no instrumentation will ever touch.
Environment
@opentelemetry/sdk-node@opentelemetry/instrumentation@opentelemetry/auto-instrumentations-nodeimport-in-the-middleRelated
class X extends Y) — follow-up to #227 nodejs/import-in-the-middle#280 — the underlying live-binding defect, with a minimal reproductionmodule.register(...)in recommended bootstrap code for ESM support #4933 —module.register(...)in recommended ESM bootstrap (shows the same API)_httpPatchedguard breaks ESM instrumentation when the IITM proxy holds stale snapshots (same snapshot mechanism, different symptom)