Skip to content

Commit f2e09f7

Browse files
mydeaclaude
andcommitted
fix(server-utils): defer integration-factory read in orchestrion injected snippet
The build-time snippet injected into each instrumented module passed the integration factory by reference (`orchestrionModuleInjected('@flue/runtime', flueIntegration)`), reading the binding the instant that module evaluated. For a provided-module integration like Flue — which `@sentry/*/vite` imports back into its own instrumented package — this closes an import cycle and crashes the worker at startup with "Cannot access 'flueIntegration' before initialization". Wrapping it in an arrow (`() => flueIntegration()`) defers the read until the stored thunk runs (at `init()`), which is what breaks the cycle; the consumer still gets an Integration from `factory()`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f6fbbef commit f2e09f7

3 files changed

Lines changed: 15 additions & 8 deletions

File tree

‎packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ function moduleInjectedSnippet(
7676
? `import { ${bindings} } from ${JSON.stringify(importSpecifier)};`
7777
: `const { ${bindings} } = require(${JSON.stringify(importSpecifier)});`;
7878

79-
const args = exportName ? `${JSON.stringify(moduleName)}, ${exportName}` : JSON.stringify(moduleName);
79+
// `exportName` is itself an integration factory, invoked inside the arrow (`() => exportName()`), so
80+
// `orchestrionModuleInjected`'s consumer still gets an Integration back from calling the stored
81+
// thunk. The point of the arrow is to defer the read of the `exportName` binding to when that thunk
82+
// runs (at `init()`), instead of reading it by reference the moment this snippet evaluates. A
83+
// provided-module integration (e.g. `flueIntegration`) is imported back into its own instrumented
84+
// package by `@sentry/*/vite`, so a direct reference here would close an import cycle and touch the
85+
// binding in its TDZ ("Cannot access '…' before initialization"); deferring the read breaks that.
86+
const args = exportName ? `${JSON.stringify(moduleName)}, () => ${exportName}()` : JSON.stringify(moduleName);
8087
return `${importStmt}\n${MODULE_INJECTED_SINK} = orchestrionModuleInjected(${args});`;
8188
}
8289

‎packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('module-injected transform', () => {
8282
// The helper is called with the REAL module name, so no reverse lookup is
8383
// needed at runtime and the lazy-subscription event matches what channel
8484
// integrations wait for.
85-
expect(result!.code).toContain('orchestrionModuleInjected("mysql", mysqlIntegration)');
85+
expect(result!.code).toContain('orchestrionModuleInjected("mysql", () => mysqlIntegration())');
8686
// The result is assigned to a global. `@sentry/server-utils` is `sideEffects: false` and the
8787
// helper returns `void`, so a bare call statement is one a bundler can prove droppable.
8888
// rollup >= 4.63.0 removes it, leaving the module instrumented but unsubscribed.
@@ -107,7 +107,7 @@ describe('module-injected transform', () => {
107107
/import\s*\{\s*orchestrionModuleInjected,\s*postgresIntegration\s*\}\s*from\s*["']@sentry\/server-utils["']/,
108108
);
109109
expect(result!.code).not.toContain('@sentry/core');
110-
expect(result!.code).toContain('orchestrionModuleInjected("pg", postgresIntegration)');
110+
expect(result!.code).toContain('orchestrionModuleInjected("pg", () => postgresIntegration())');
111111
});
112112

113113
it('injects a helper-only snippet for a module with no subscriber factory', () => {
@@ -157,7 +157,7 @@ describe('module-injected transform', () => {
157157
expect(result!.code).toMatch(
158158
/const\s*\{\s*orchestrionModuleInjected,\s*redisIntegration\s*\}\s*=\s*require\(["']@sentry\/server-utils["']\)/,
159159
);
160-
expect(result!.code).toContain('orchestrionModuleInjected("ioredis", redisIntegration)');
160+
expect(result!.code).toContain('orchestrionModuleInjected("ioredis", () => redisIntegration())');
161161
// The library publishes its own channels, so nothing else is injected: no
162162
// diagnostics_channel import, no channel declaration, no function wrapper.
163163
expect(result!.code).not.toContain('diagnostics_channel');
@@ -173,7 +173,7 @@ describe('module-injected transform', () => {
173173
expect(result!.code).toMatch(
174174
/import\s*\{\s*orchestrionModuleInjected,\s*vercelAIIntegration\s*\}\s*from\s*["']@sentry\/server-utils["']/,
175175
);
176-
expect(result!.code).toContain('orchestrionModuleInjected("ai", vercelAIIntegration)');
176+
expect(result!.code).toContain('orchestrionModuleInjected("ai", () => vercelAIIntegration())');
177177
expect(result!.code).not.toContain('diagnostics_channel');
178178
expect(result!.code).not.toContain('tr_ch_apm');
179179
});

‎packages/server-utils/test/orchestrion/webpack-loader.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('orchestrion webpack/Turbopack loader', () => {
6565
expect(code).toMatch(
6666
/const\s*\{\s*orchestrionModuleInjected,\s*mysqlIntegration\s*\}\s*=\s*require\(["']@sentry\/server-utils["']\)/,
6767
);
68-
expect(code).toContain('orchestrionModuleInjected("mysql", mysqlIntegration)');
68+
expect(code).toContain('orchestrionModuleInjected("mysql", () => mysqlIntegration())');
6969
});
7070

7171
it('honors a fixed importSpecifier option', () => {
@@ -116,7 +116,7 @@ describe('orchestrion webpack/Turbopack loader', () => {
116116
expect(error).toBeNull();
117117
expect(code).toContain('orchestrion:@mastra/core:mastraConstructor');
118118
expect(code).toContain('import {orchestrionModuleInjected, mastraIntegration} from "@sentry/server-utils"');
119-
expect(code).toContain('orchestrionModuleInjected("@mastra/core", mastraIntegration)');
119+
expect(code).toContain('orchestrionModuleInjected("@mastra/core", () => mastraIntegration())');
120120
});
121121

122122
it('transforms a hashed `.mjs` `@mastra/core` chunk that contains `class Mastra`', () => {
@@ -127,7 +127,7 @@ describe('orchestrion webpack/Turbopack loader', () => {
127127
expect(error).toBeNull();
128128
expect(code).toContain('orchestrion:@mastra/core:mastraConstructor');
129129
expect(code).toContain('import {orchestrionModuleInjected, mastraIntegration} from "@sentry/server-utils"');
130-
expect(code).toContain('orchestrionModuleInjected("@mastra/core", mastraIntegration)');
130+
expect(code).toContain('orchestrionModuleInjected("@mastra/core", () => mastraIntegration())');
131131
});
132132

133133
it('does not transform the stable Mastra re-export that does not contain the class', () => {

0 commit comments

Comments
 (0)