Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<title>orchestrion client build</title>
</head>
<body>
<script type="module" src="/client/main.ts"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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<Response> {
if (new URL(request.url).pathname === '/worker') {
return new Response(`streamText: ${typeof streamText}`);
}
return new Response('not found', { status: 404 });
},
};
Original file line number Diff line number Diff line change
@@ -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<string>('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');
});
Original file line number Diff line number Diff line change
@@ -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,
},
}),
],
});
Original file line number Diff line number Diff line change
@@ -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",
},
}
6 changes: 6 additions & 0 deletions packages/server-utils/src/orchestrion/bundler/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import { externalEntryMatchesModule, externalizedModulesWarning, orchestrionTran
export function sentryOrchestrionPlugin(options: PluginOptions = {}): ReturnType<typeof codeTransformer> {
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
Expand Down
Loading