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.