Skip to content

Commit 8613506

Browse files
authored
fix(v10/server-utils): Do not inject dc into client bundle (#22765)
Backport of: #22668
1 parent 6543f7f commit 8613506

7 files changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { streamText } from 'ai';
2+
3+
// The browser bundle also pulls in an orchestrion-instrumented module (`ai`).
4+
// Injected `node:diagnostics_channel` calls only exist server-side, so the
5+
// client bundle must stay free of them (they throw `X is not a function` in
6+
// the browser otherwise).
7+
document.title = `streamText: ${typeof streamText}`;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>orchestrion client build</title>
5+
</head>
6+
<body>
7+
<script type="module" src="/client/main.ts"></script>
8+
</body>
9+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { streamText } from 'ai';
2+
3+
// The worker imports an orchestrion-instrumented module (`ai`), so the server
4+
// bundle is expected to contain `diagnostics_channel` injections.
5+
export default {
6+
async fetch(request: Request): Promise<Response> {
7+
if (new URL(request.url).pathname === '/worker') {
8+
return new Response(`streamText: ${typeof streamText}`);
9+
}
10+
return new Response('not found', { status: 404 });
11+
},
12+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { readdirSync, readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { expect, it } from 'vitest';
4+
import { createRunner } from '../../../../runner';
5+
6+
function readBundles(dir: string): string {
7+
return readdirSync(dir, { withFileTypes: true, recursive: true })
8+
.filter(entry => entry.isFile() && /\.m?js$/.test(entry.name))
9+
.map(entry => readFileSync(join(entry.parentPath, entry.name), 'utf8'))
10+
.join('\n');
11+
}
12+
13+
// Regression test: orchestrion splices `node:diagnostics_channel` calls into
14+
// instrumented modules, which only exist server-side. When a worker ships
15+
// browser assets, Vite produces a `client` bundle next to the server (worker)
16+
// bundle — and the injected `tracingChannel` calls used to land in the client
17+
// bundle too, where they throw `X is not a function` in the browser.
18+
it('injects diagnostics_channel calls into the server bundle only, not the client bundle', async ({ signal }) => {
19+
const runner = createRunner(__dirname).start(signal);
20+
21+
// Waits for `vite build` + wrangler boot and proves the instrumented worker
22+
// still runs.
23+
const response = await runner.makeRequest<string>('get', '/worker');
24+
expect(response).toBe('streamText: function');
25+
26+
// The worker imports `ai`, so the server bundle must actually be
27+
// instrumented — otherwise a plugin that never runs would also pass.
28+
const workerBundle = readBundles(join(__dirname, 'dist', 'cloudflare_vite_dc_client_build'));
29+
expect(workerBundle).toContain('orchestrion:ai:streamText');
30+
31+
const clientBundle = readBundles(join(__dirname, 'dist', 'client'));
32+
expect(clientBundle).not.toContain('orchestrion:ai');
33+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { cloudflare } from '@cloudflare/vite-plugin';
2+
import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
3+
import { defineConfig } from 'vite';
4+
5+
export default defineConfig({
6+
plugins: [
7+
cloudflare(),
8+
sentryCloudflareVitePlugin({
9+
_experimental: {
10+
useDiagnosticsChannelInjection: true,
11+
},
12+
}),
13+
],
14+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "../../../node_modules/wrangler/config-schema.json",
3+
"name": "cloudflare-vite-dc-client-build",
4+
// `main` points at the source entry; the runner detects `vite.config.mts`, runs
5+
// `vite build`, and serves the built output (so the orchestrion transform runs).
6+
"main": "index.ts",
7+
"compatibility_date": "2026-04-26",
8+
"compatibility_flags": ["nodejs_compat"],
9+
// Giving the worker assets makes the Cloudflare Vite plugin produce a browser
10+
// (`client`) bundle next to the server (worker) bundle — the setup where the
11+
// orchestrion plugin must not touch the client output.
12+
"assets": {
13+
"directory": "./dist/client",
14+
},
15+
}

packages/server-utils/src/orchestrion/bundler/vite.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import { externalEntryMatchesModule, externalizedModulesWarning, orchestrionTran
2121
export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType<typeof codeTransformer> {
2222
return {
2323
...codeTransformer(orchestrionTransformOptions(options)),
24+
applyToEnvironment(environment) {
25+
// Orchestrion splices `node:diagnostics_channel` calls into instrumented modules, which only
26+
// exist server-side. Only apply to server-consumed environments so injected `tracingChannel`
27+
// calls never land in a browser (`client`) bundle (where they'd throw `X is not a function`).
28+
return environment.config.consumer === 'server';
29+
},
2430
config(): { ssr: { noExternal: string[] } } {
2531
// Force-bundle every instrumented package so the code transform actually
2632
// sees its source. Vite externalizes dependencies in SSR builds by

0 commit comments

Comments
 (0)