From 702ac36e0849486df1288499a9940f08386d3d77 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 25 Aug 2026 16:25:13 +0200 Subject: [PATCH] fix(server-utils): Keep orchestrion registration out of tree-shaking The bundler transform injected a bare `orchestrionModuleInjected(...)` call. The helper returns `void` and `@sentry/server-utils` sets `sideEffects: false`, so bundlers may prove the statement droppable. rollup 4.63.0 does, and removes the registration: instrumented modules publish on their channel, but nothing subscribes, so no spans are recorded. Assign the result to a `globalThis` property. A write to a global is a side effect no bundler can shake out, so the call survives while the rest of the package still tree-shakes. Co-Authored-By: Claude Opus 5 (1M context) --- .../bundler/moduleInjectedTransform.ts | 16 +++++++++++++++- .../orchestrion/moduleInjectedTransform.test.ts | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts b/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts index 86af80d8d4ab..2d9c85a8a91c 100644 --- a/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts +++ b/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts @@ -19,6 +19,13 @@ interface ProgramNode { // tree-shakes to just the helper and the factories actually referenced. const DEFAULT_IMPORT_SPECIFIER = '@sentry/server-utils'; +/** + * Assignment target that keeps the injected call from being tree-shaken. See + * {@link moduleInjectedSnippet}. The value written is always `undefined`; only + * the assignment matters. + */ +const MODULE_INJECTED_SINK = 'globalThis.__SENTRY_ORCHESTRION_INJECT__'; + /** * Entry-chunk banner that marks "the bundler plugin ran" for * `detectOrchestrionSetup()`. Merge-only (`g.bundler = g.bundler || []`) so it @@ -50,6 +57,13 @@ export const ORCHESTRION_BUNDLER_MARKER_BANNER = * inside a transformed `node_modules` file can't resolve (Turbopack under * isolated installs); it's embedded via `JSON.stringify` so absolute Windows * paths survive. + * + * The call result is assigned to a global rather than discarded. The helper + * returns `void` and `@sentry/server-utils` is `sideEffects: false`, so a bare + * call statement is something a bundler can prove droppable: rollup >= 4.63.0 + * does exactly that and removes the whole registration, leaving the module + * instrumented but unsubscribed. Writing to a property of `globalThis` is a + * side effect no bundler can shake out, so the call survives. */ function moduleInjectedSnippet( moduleName: string, @@ -63,7 +77,7 @@ function moduleInjectedSnippet( : `const { ${bindings} } = require(${JSON.stringify(importSpecifier)});`; const args = exportName ? `${JSON.stringify(moduleName)}, ${exportName}` : JSON.stringify(moduleName); - return `${importStmt}\norchestrionModuleInjected(${args});`; + return `${importStmt}\n${MODULE_INJECTED_SINK} = orchestrionModuleInjected(${args});`; } /** diff --git a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts index 0ec2eb58006d..581d9cf552fa 100644 --- a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts +++ b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts @@ -83,6 +83,10 @@ describe('module-injected transform', () => { // needed at runtime and the lazy-subscription event matches what channel // integrations wait for. expect(result!.code).toContain('orchestrionModuleInjected("mysql", mysqlIntegration)'); + // The result is assigned to a global. `@sentry/server-utils` is `sideEffects: false` and the + // helper returns `void`, so a bare call statement is one a bundler can prove droppable. + // rollup >= 4.63.0 removes it, leaving the module instrumented but unsubscribed. + expect(result!.code).toContain('globalThis.__SENTRY_ORCHESTRION_INJECT__ = orchestrionModuleInjected('); // No separate @sentry/core import at the injection site — the helper owns that. expect(result!.code).not.toContain('@sentry/core'); // It imports ONLY the mysql factory — no central dispatch pulling in others.