Repository navigation
Surface missing or late diagnostics-channel injection instead of silently dropping spans #25677
Description
Library instrumentation (DB, framework, queue, AI) depends on diagnostics channels being injected, either at runtime (--import / hooks registered in init(), Deno --preload) or at build time (bundler plugins, @sentry/bun/plugin, @sentry/cloudflare/vite). When injection does not happen, or happens too late, these libraries produce no spans. HTTP spans still work, so a setup that is only partly instrumented looks healthy to users, and in several cases nothing tells them otherwise unless debug: true is set.
Cases that are currently silent
- Node: a library is imported before
Sentry.init().NodeClientregisters the hooks ininit()(packages/node/src/sdk/client.ts), so any instrumented library loaded earlier stays uninstrumented. This happens when users skip--import, import a driver in the same file asinit, or use a framework that loads drivers early (e.g. Nuxt dev server loads DB drivers before Sentry.init() #23876). Nothing checks for it today. - Bun without
@sentry/bun/plugin. Runtime hook registration is skipped with only adebug.warn(packages/server-runtime-injection/src/register.ts), anddetectOrchestrionSetup()also only usesdebug.warn. - Deno without
--preload=npm:@sentry/deno/import. Injection only happens in the preload. Deno'sinitdoes not calldetectOrchestrionSetup()at all. - Cloudflare without the Vite plugin. The channel integrations are never registered, and no warning is printed.
On Node, registration failures and a bundled @sentry/server-runtime-injection already warn without debug, so these are not covered here.
Proposal
-
Warn by default when nothing is injected, on every runtime. If spans are enabled and neither runtime nor bundler injection ran,
detectOrchestrionSetup()should warn once throughconsole.warn(withconsoleSandbox) instead ofdebug.warn. Call it from the Bun, Deno and Cloudflare SDKs too, each with its own fix hint (Bun plugin, Deno--preload, Cloudflare Vite plugin, Node--import). This condition cannot be a false positive: tracing is on and nothing can produce library spans. -
Node: warn about instrumented libraries that loaded before
init(). When the hooks are registered, scanrequire.cachefor paths undernode_modules/<name>/for every instrumented package name (from the orchestrion configs, seegetModuleNames). Any match was loaded too early and will not be traced. Warn once and name the libraries, e.g. "pg,iorediswere imported beforeSentry.init()and will not be traced; use--import …". CJS packages imported from ESM also end up inrequire.cache, so this works for ESM apps as well, and most affected packages (pg, mysql2, ioredis, mongodb, express, …) are CJS. ESM-only packages are not detectable this way, and that gap is accepted. The cost is one pass over the cache keys at init. -
Report the injection state in SDK metadata. Attach a general indicator of how the process was instrumented (e.g.
runtime/bundler/both/none) to the SDK metadata on outgoing envelopes. That lets us measure how common broken or partial setups are, and lets the product surface a hint later. Reporting which individual libraries were instrumented is out of scope here and will be handled separately.
Activity
linear-code commentedon Oct 23, 2026
JS-3990
isaacs commentedon Oct 23, 2026
One tiny correction: as of #25546, Deno does register on
init(), just like Node. But of course, that doesn't change the overall point, just means that Deno needs effectively the same detection as Node. Ie, we can't shortcut Deno by warning whenever--preloadis missing, sinceinit()might be doing it.isaacs commentedon Oct 23, 2026
Digging a bit deeper into this:
Avoiding a class of user footguns is appealing. But there's some obstacles to doing all three parts of this as described.
On Node and Deno, a successful registration in the client constructor sets
marker.runtime = []before detection runs. The "nothing injected" branch is then only reached in three cases:debug.debug, but also, all supported versions of node and deno have either Module.registerHooks or Module.register, so there's really no gap there to worry about.enableRuntimeChannelInjection: falseand has no bundler. They asked for that, so a default warning would be noise.The other case for Node and Deno where nothing gets injected is if there's no preload, and modules get loaded before calling
Sentry.init(), discussed below, because that's Item 2.That leaves Bun and Cloudflare as the cases this item really fixes, and "cannot be a false positive" is not true:
Bun.serveor plain Workersfetchhandlers. So they're getting spans, but from our pov, nothing was injected and no module was instrumented.Cloudflare detection has to look at
__SENTRY_ORCHESTRION__.integrations(packages/cloudflare/src/baseSdk.ts line 38), not only atruntime/bundler. The Vite plugin writes there, anddetectOrchestrionSetup()does not read it.require.cachedoesn't work on ESM, and there's no corollary torequire.cachefor ESM modules. So we'd have to decide that it's worth detecting this sort of shortcoming, but only in CommonJS modules, which are likely to continue to drop in popularity.node_modules/<name>/is too coarse. The configs target specific files:module.filePath, e.g.mysql → lib/Connection.js. If a package has loaded but its target file has not, that file still gets instrumented when it loads later. Packages that load sub-files lazily (AWS SDK, Prisma, AI SDKs) would warn wrongly. So we'd have to matchnode_modules/<name>/<filePath>instead, and normalize Windows separators.--importalready registered the hooks, modules in the cache were already transformed. The early return inregisterDiagnosticsChannelInjectionhandles this as long as the scan lives after that guard.versionRangewould never be traced anyway, so "use--import" is the wrong fix for it. We'll have to either check the version, or word the message more softly.Imo, being unable to detect this failure for ESM pretty dramatically reduces the value of this scan.
This is reasonable, and imo a good idea. We need to decide the field name.
sdk.features: ['orchestrion:runtime']or similar would avoid schema work.bundlerSet is empty at boot and fills as modules load. "bundler" therefore means "the plugin ran", not "the plugin instrumented something". That matches the proposed runtime/bundler/both/none values, but the data has to be read that way.