diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/client/main.ts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/client/main.ts
new file mode 100644
index 000000000000..7d443658fd7b
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/client/main.ts
@@ -0,0 +1,7 @@
+import { streamText } from 'ai';
+
+// The browser bundle also pulls in an orchestrion-instrumented module (`ai`).
+// Injected `node:diagnostics_channel` calls only exist server-side, so the
+// client bundle must stay free of them (they throw `X is not a function` in
+// the browser otherwise).
+document.title = `streamText: ${typeof streamText}`;
diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/index.html b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/index.html
new file mode 100644
index 000000000000..9241085484ab
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/index.html
@@ -0,0 +1,9 @@
+
+
+
+ orchestrion client build
+
+
+
+
+
diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/index.ts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/index.ts
new file mode 100644
index 000000000000..df01c6e476cb
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/index.ts
@@ -0,0 +1,12 @@
+import { streamText } from 'ai';
+
+// The worker imports an orchestrion-instrumented module (`ai`), so the server
+// bundle is expected to contain `diagnostics_channel` injections.
+export default {
+ async fetch(request: Request): Promise {
+ if (new URL(request.url).pathname === '/worker') {
+ return new Response(`streamText: ${typeof streamText}`);
+ }
+ return new Response('not found', { status: 404 });
+ },
+};
diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/test.ts
new file mode 100644
index 000000000000..3d858b0a3068
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/test.ts
@@ -0,0 +1,33 @@
+import { readdirSync, readFileSync } from 'fs';
+import { join } from 'path';
+import { expect, it } from 'vitest';
+import { createRunner } from '../../../../runner';
+
+function readBundles(dir: string): string {
+ return readdirSync(dir, { withFileTypes: true, recursive: true })
+ .filter(entry => entry.isFile() && /\.m?js$/.test(entry.name))
+ .map(entry => readFileSync(join(entry.parentPath, entry.name), 'utf8'))
+ .join('\n');
+}
+
+// Regression test: orchestrion splices `node:diagnostics_channel` calls into
+// instrumented modules, which only exist server-side. When a worker ships
+// browser assets, Vite produces a `client` bundle next to the server (worker)
+// bundle — and the injected `tracingChannel` calls used to land in the client
+// bundle too, where they throw `X is not a function` in the browser.
+it('injects diagnostics_channel calls into the server bundle only, not the client bundle', async ({ signal }) => {
+ const runner = createRunner(__dirname).start(signal);
+
+ // Waits for `vite build` + wrangler boot and proves the instrumented worker
+ // still runs.
+ const response = await runner.makeRequest('get', '/worker');
+ expect(response).toBe('streamText: function');
+
+ // The worker imports `ai`, so the server bundle must actually be
+ // instrumented — otherwise a plugin that never runs would also pass.
+ const workerBundle = readBundles(join(__dirname, 'dist', 'cloudflare_vite_dc_client_build'));
+ expect(workerBundle).toContain('orchestrion:ai:streamText');
+
+ const clientBundle = readBundles(join(__dirname, 'dist', 'client'));
+ expect(clientBundle).not.toContain('orchestrion:ai');
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/vite.config.mts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/vite.config.mts
new file mode 100644
index 000000000000..541d36ac0a61
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/vite.config.mts
@@ -0,0 +1,14 @@
+import { cloudflare } from '@cloudflare/vite-plugin';
+import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ cloudflare(),
+ sentryCloudflareVitePlugin({
+ _experimental: {
+ useDiagnosticsChannelInjection: true,
+ },
+ }),
+ ],
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/wrangler.jsonc
new file mode 100644
index 000000000000..6c4f68500e1e
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/client-build/wrangler.jsonc
@@ -0,0 +1,15 @@
+{
+ "$schema": "../../../node_modules/wrangler/config-schema.json",
+ "name": "cloudflare-vite-dc-client-build",
+ // `main` points at the source entry; the runner detects `vite.config.mts`, runs
+ // `vite build`, and serves the built output (so the orchestrion transform runs).
+ "main": "index.ts",
+ "compatibility_date": "2026-04-26",
+ "compatibility_flags": ["nodejs_compat"],
+ // Giving the worker assets makes the Cloudflare Vite plugin produce a browser
+ // (`client`) bundle next to the server (worker) bundle — the setup where the
+ // orchestrion plugin must not touch the client output.
+ "assets": {
+ "directory": "./dist/client",
+ },
+}
diff --git a/packages/server-utils/src/orchestrion/bundler/vite.ts b/packages/server-utils/src/orchestrion/bundler/vite.ts
index 90274bb52212..dc3426e9ba02 100644
--- a/packages/server-utils/src/orchestrion/bundler/vite.ts
+++ b/packages/server-utils/src/orchestrion/bundler/vite.ts
@@ -21,6 +21,12 @@ import { externalEntryMatchesModule, externalizedModulesWarning, orchestrionTran
export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin {
return {
...codeTransformer(orchestrionTransformOptions(options)),
+ applyToEnvironment(environment) {
+ // Orchestrion splices `node:diagnostics_channel` calls into instrumented modules, which only
+ // exist server-side. Only apply to server-consumed environments so injected `tracingChannel`
+ // calls never land in a browser (`client`) bundle (where they'd throw `X is not a function`).
+ return environment.config.consumer === 'server';
+ },
config(): { ssr: { noExternal: string[] } } {
// Force-bundle every instrumented package so the code transform actually
// sees its source. Vite externalizes dependencies in SSR builds by