From 16075fdec515bd6da2580acfa36d718f7442eb1c Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 17 Sep 2026 17:09:05 +0300 Subject: [PATCH 1/5] test(e2e): Add a cloudflare-flue end-to-end application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `flue init` Cloudflare app with no `instrument()` call anywhere: registration comes from the build, so any `gen_ai` span here is itself proof the auto-wiring worked. Covers AI spans, tool-error capture, a manual span nesting under its tool, and an orchestrion `dataloader` span in the agent's trace. Secrets go through `.dev.vars` rather than `--var`: Flue resolves the provider key inside `pi-ai` at runtime, so unlike Mastra there is nothing for Vite to inline, and `vite preview` is what serves the worker with Flue's generated Durable Object config. `agents` is declared explicitly — Flue's generated worker entry imports it without declaring it, which only resolves under npm's hoisting, not pnpm. The agent and loader modules avoid TypeScript generics and return types because Flue's `'use agent'` scan parses every source file as plain JavaScript. --- .../cloudflare-flue/.gitignore | 1 + .../cloudflare-flue/flue.config.ts | 5 ++ .../cloudflare-flue/package.json | 44 ++++++++++++++ .../cloudflare-flue/playwright.config.ts | 16 +++++ .../cloudflare-flue/src/agents/hello.ts | 50 ++++++++++++++++ .../cloudflare-flue/src/app.ts | 9 +++ .../cloudflare-flue/src/cloudflare.ts | 4 ++ .../cloudflare-flue/src/env.d.ts | 4 ++ .../cloudflare-flue/src/loaders.ts | 13 ++++ .../cloudflare-flue/src/sentry.ts | 21 +++++++ .../cloudflare-flue/start-event-proxy.mjs | 6 ++ .../cloudflare-flue/tests/dataloader.test.ts | 31 ++++++++++ .../cloudflare-flue/tests/errors.test.ts | 24 ++++++++ .../cloudflare-flue/tests/flue.test.ts | 59 +++++++++++++++++++ .../cloudflare-flue/tests/utils.ts | 35 +++++++++++ .../cloudflare-flue/tsconfig.json | 20 +++++++ .../cloudflare-flue/vite.config.mts | 8 +++ .../cloudflare-flue/wrangler.jsonc | 10 ++++ .../cloudflare-flue/write-dev-vars.mjs | 13 ++++ 19 files changed, 373 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/flue.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/src/cloudflare.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/src/loaders.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/vite.config.mts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore new file mode 100644 index 000000000000..babca1bb1d28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore @@ -0,0 +1 @@ +.dev.vars diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/flue.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/flue.config.ts new file mode 100644 index 000000000000..8bdb0fc75151 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/flue.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@flue/runtime/config'; + +export default defineConfig({ + target: 'cloudflare', +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json new file mode 100644 index 000000000000..8ddea4b23e05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json @@ -0,0 +1,44 @@ +{ + "name": "cloudflare-flue", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 4112", + "build": "vite build", + "preview": "vite preview --port 4112", + "typecheck": "tsc --noEmit", + "clean": "npx rimraf node_modules dist .wrangler pnpm-lock.yaml", + "test:build": "pnpm install && pnpm write-dev-vars && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev", + "test:prod": "TEST_ENV=production OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test", + "test:dev": "TEST_ENV=development OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test", + "write-dev-vars": "node write-dev-vars.mjs" + }, + "dependencies": { + "@flue/runtime": "2.0.5", + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "agents": "0.20.1", + "dataloader": "~2.2.3", + "hono": "~4.7.0", + "valibot": "~1.5.0" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "1.52.0", + "@cloudflare/workers-types": "^4.20260426.0", + "@flue/cli": "2.0.5", + "@flue/vite": "2.0.5", + "@playwright/test": "~1.63.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "typescript": "^5.5.2", + "vite": "7.3.5", + "wrangler": "^4.86.0" + }, + "volta": { + "node": "24.15.0", + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts new file mode 100644 index 000000000000..bbe77dd5b055 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts @@ -0,0 +1,16 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig( + // `dev` is vite's dev server; `preview` serves the built worker through wrangler. Both run under + // workerd, so both exercise the build-time registration this app exists to prove. + { startCommand: testEnv === 'development' ? 'pnpm dev' : 'pnpm preview', port: 4112 }, + { timeout: 90_000 }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts new file mode 100644 index 000000000000..df0589a1988e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts @@ -0,0 +1,50 @@ +'use agent'; +import { useModel, useTool } from '@flue/runtime'; +import * as Sentry from '@sentry/cloudflare'; +import * as v from 'valibot'; +import { createItemLoader } from '../loaders.ts'; + +// Flue applies the agent's Durable Object wrapper from this re-export. +export { cloudflare } from '../sentry.ts'; + +// The `'use agent'` directive is how `@flue/vite` binds an identity to this module at build time. +export function Hello() { + useModel('openrouter/anthropic/claude-haiku-4.5'); + + useTool({ + name: 'get_weather', + description: 'Get the current weather for a city.', + input: v.object({ city: v.string() }), + // Wrapped in a manual span: Flue runs the tool while the SDK's `execute_tool` span is active, + // so this should nest directly under it rather than landing beside it. + run: ({ city }) => + Sentry.startSpan({ name: 'resolve-weather', attributes: { 'weather.source': 'static-table' } }, () => { + return `It is 21 degrees and sunny in ${city}.`; + }), + }); + + // Called from inside a tool so the dataloader span lands in the agent's trace beside the AI + // spans. Constructed per execution: a module-level loader caches its keys, so a second call + // would skip the batch function and emit no span. + useTool({ + name: 'count_items', + description: 'Count items by loading them. Call this when the user asks to count items.', + input: v.object({}), + run: async () => { + const loader = createItemLoader(); + const doubled = await Promise.all([loader.load(1), loader.load(2), loader.load(3)]); + return `Loaded ${doubled.length} items: ${doubled.join(', ')}.`; + }, + }); + + useTool({ + name: 'fail_now', + description: 'Always throws an error. Call this when the user asks to trigger a failure.', + input: v.object({}), + run: () => { + throw new Error('Intentional flue tool failure'); + }, + }); + + return 'You are a helpful assistant. Use get_weather when asked about weather, count_items when asked to count items, and fail_now when asked to fail.'; +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts new file mode 100644 index 000000000000..613e9453c506 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts @@ -0,0 +1,9 @@ +import { createAgentRouter } from '@flue/runtime/routing'; +import { Hono } from 'hono'; +import { Hello } from './agents/hello.ts'; + +const app = new Hono(); + +app.route('/agents/hello', createAgentRouter(Hello)); + +export default app; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/cloudflare.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/cloudflare.ts new file mode 100644 index 000000000000..49c7b484e9ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/cloudflare.ts @@ -0,0 +1,4 @@ +// Worker-level Cloudflare code would live here; the Sentry wrapper is in `src/sentry.ts` and is +// re-exported from the agent module, which is how Flue applies it to the agent's Durable Object. + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts new file mode 100644 index 000000000000..ecd15b570543 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts @@ -0,0 +1,4 @@ +interface Env { + E2E_TEST_DSN: string; + OPENROUTER_API_KEY: string; +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/loaders.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/loaders.ts new file mode 100644 index 000000000000..24453fc81b91 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/loaders.ts @@ -0,0 +1,13 @@ +import DataLoader from 'dataloader'; + +/** + * Built per call, not shared: a module-level loader caches its keys, so a second `count_items` would + * skip the batch function and emit no span. + * + * Deliberately free of type annotations and generics. Flue's build scans every source file looking + * for `'use agent'` modules and parses them as plain JavaScript, so a return type or a + * `new DataLoader(…)` fails the build with a parse error pointing at this file. + */ +export function createItemLoader() { + return new DataLoader(async keys => keys.map(key => Number(key) * 2)); +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts new file mode 100644 index 000000000000..5cbc2856bfff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts @@ -0,0 +1,21 @@ +import { extend } from '@flue/runtime/cloudflare'; +import * as Sentry from '@sentry/cloudflare'; + +// Each Flue agent runs in its own Durable Object, so the DO class is what has to be wrapped for +// `Sentry.init` to run and spans to be flushed. The agent module re-exports this as `cloudflare`, +// which is how Flue picks it up — defining it here alone does nothing. +// +// There is deliberately no `instrument()` call in this app: registering the Flue instrumentation is +// what `@sentry/cloudflare/vite` does at build time, and these tests exist to prove it. +export const cloudflare = extend({ + wrap: Final => + Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', + tunnel: 'http://localhost:3031/', // proxy server + tracesSampleRate: 1.0, + }), + Final, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs new file mode 100644 index 000000000000..a24f2d9a08a0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'cloudflare-flue', +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts new file mode 100644 index 000000000000..0193e0febbd0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { newConversationId, runAgentTurn } from './utils'; + +const APP = 'cloudflare-flue'; + +const isDataloaderSpan = (span: { attributes?: Record }): boolean => + span.attributes?.['sentry.origin']?.value === 'auto.db.dataloader'; + +/** + * On Cloudflare orchestrion runs at build time — `@sentry/cloudflare/vite` injects the channels — + * so unlike the Node app there is no `--import` bootstrap and no variant: the span is either there + * or the build-time instrumentation regressed. + */ +test('captures orchestrion-instrumented dataloader spans in the same trace as the AI spans', async ({ baseURL }) => { + const spansPromise = collectStreamedSpans( + APP, + spansOfTrace => + spansOfTrace.some(span => span.attributes?.['gen_ai.tool.name']?.value === 'count_items') && + spansOfTrace.some(isDataloaderSpan), + ); + + await runAgentTurn(baseURL!, newConversationId('dataloader'), 'Please call count_items to count the items.'); + + const spans = await spansPromise; + const dataloaderSpan = spans.find(isDataloaderSpan); + const toolSpan = spans.find(span => span.attributes?.['gen_ai.tool.name']?.value === 'count_items'); + + expect(getSpanOp(dataloaderSpan!)).toBe('cache.get'); + expect(dataloaderSpan?.trace_id).toBe(toolSpan?.trace_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts new file mode 100644 index 000000000000..98a12ebc4c3a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, waitForError } from '@sentry-internal/test-utils'; +import { newConversationId, runAgentTurn } from './utils'; + +const APP = 'cloudflare-flue'; + +test('captures an error thrown inside a Flue tool and marks its span errored', async ({ baseURL }) => { + const errorPromise = waitForError( + APP, + event => event.exception?.values?.[0]?.value === 'Intentional flue tool failure', + ); + const spansPromise = collectStreamedSpans(APP, spansOfTrace => + spansOfTrace.some(span => span.attributes?.['gen_ai.tool.name']?.value === 'fail_now'), + ); + + await runAgentTurn(baseURL!, newConversationId('failure'), 'Please call fail_now to trigger a failure.'); + + const error = await errorPromise; + expect(error.exception?.values?.[0]?.mechanism?.type).toBe('auto.ai.flue'); + + const spans = await spansPromise; + const executeTool = spans.find(span => span.attributes?.['gen_ai.tool.name']?.value === 'fail_now'); + expect(executeTool?.status).toBe('error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts new file mode 100644 index 000000000000..41142178aebd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { newConversationId, runAgentTurn } from './utils'; + +const APP = 'cloudflare-flue'; + +type SpanLike = { name?: string; attributes?: Record }; + +const usedTool = (toolName: string) => (spansOfTrace: SpanLike[]) => + spansOfTrace.some(span => span.attributes?.['gen_ai.tool.name']?.value === toolName); + +/** + * This app never calls `instrument()`. On Cloudflare the registration comes from the build: + * `@sentry/cloudflare/vite` provides the `@flue/runtime` binding and the orchestrion registration + * installs `flueIntegration()`. So any `gen_ai` span here is itself the proof that the auto-wiring + * worked — a manual-registration regression shows up as an empty trace, not a wrong attribute. + */ +test('instruments a Flue agent with no manual instrument() call', async ({ baseURL }) => { + const spansPromise = collectStreamedSpans( + APP, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'gen_ai.invoke_agent') && usedTool('get_weather')(spansOfTrace), + ); + + await runAgentTurn(baseURL!, newConversationId('weather'), 'What is the weather in Paris?'); + + const spans = await spansPromise; + const invokeAgent = spans.find(span => getSpanOp(span) === 'gen_ai.invoke_agent'); + const chat = spans.find(span => getSpanOp(span) === 'gen_ai.chat'); + const executeTool = spans.find(span => getSpanOp(span) === 'gen_ai.execute_tool'); + + expect(invokeAgent?.attributes?.['sentry.origin']?.value).toBe('auto.ai.flue'); + expect(invokeAgent?.attributes?.['gen_ai.agent.name']?.value).toBe('Hello'); + + expect(chat?.attributes?.['sentry.origin']?.value).toBe('auto.ai.flue'); + expect(chat?.attributes?.['gen_ai.provider.name']?.value).toBe('openrouter'); + expect(typeof chat?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe('number'); + expect(typeof chat?.attributes?.['gen_ai.cost.total_tokens']?.value).toBe('number'); + + expect(executeTool?.attributes?.['gen_ai.tool.name']?.value).toBe('get_weather'); + expect(chat?.parent_span_id).toBe(invokeAgent?.span_id); + expect(executeTool?.parent_span_id).toBe(invokeAgent?.span_id); +}); + +test('nests a manual span raised inside a tool under that tool span', async ({ baseURL }) => { + const spansPromise = collectStreamedSpans( + APP, + spansOfTrace => usedTool('get_weather')(spansOfTrace) && spansOfTrace.some(span => span.name === 'resolve-weather'), + ); + + await runAgentTurn(baseURL!, newConversationId('manual-span'), 'What is the weather in Berlin?'); + + const spans = await spansPromise; + const executeTool = spans.find(span => getSpanOp(span) === 'gen_ai.execute_tool'); + const manualSpan = spans.find(span => span.name === 'resolve-weather'); + + expect(manualSpan?.attributes?.['weather.source']?.value).toBe('static-table'); + expect(manualSpan?.parent_span_id).toBe(executeTool?.span_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts new file mode 100644 index 000000000000..4a3932ada389 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +/** A conversation id nothing has used yet, so a settled record cannot end the wait early. */ +export function newConversationId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +} + +/** + * Run one agent turn and wait for it to settle. + * + * `POST /:id` only admits the work — it returns `202` and the turn runs after — so this reads the + * conversation back until it reports a settlement. + */ +export async function runAgentTurn(baseURL: string, conversationId: string, message: string): Promise { + const url = `${baseURL}/agents/hello/${conversationId}`; + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ kind: 'user', body: message }), + }); + expect(res.status).toBe(202); + await res.text(); + + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + const conversation = (await (await fetch(url)).json()) as { settlements?: unknown[] }; + if (conversation.settlements?.length) { + return; + } + await new Promise(resolve => setTimeout(resolve, 250)); + } + + throw new Error(`Flue turn for "${conversationId}" did not settle within 60s`); +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json new file mode 100644 index 000000000000..0db64daee718 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "types": ["@cloudflare/workers-types/experimental"] + }, + "exclude": ["tests"], + "include": ["src/**/*.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/vite.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/vite.config.mts new file mode 100644 index 000000000000..c73a98b625f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/vite.config.mts @@ -0,0 +1,8 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import { flue, flueWorkerConfig } from '@flue/vite'; +import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [flue(), cloudflare({ config: flueWorkerConfig() }), sentryCloudflareVitePlugin()], +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc new file mode 100644 index 000000000000..3e42e749cd5f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "cloudflare-flue", + "compatibility_date": "2026-06-01", + "compatibility_flags": ["nodejs_compat"], + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FlueHelloAgent"] }], + // `vite preview` takes no `--var` flag, so secrets are declared here and read from the process + // environment the Playwright web server inherits. + "secrets": { "required": ["E2E_TEST_DSN", "OPENROUTER_API_KEY"] }, +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs new file mode 100644 index 000000000000..c92867dff336 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs @@ -0,0 +1,13 @@ +import { writeFileSync } from 'node:fs'; + +// `vite preview` takes no `--var`, and Flue resolves the provider key inside `pi-ai` at runtime +// rather than in app code Vite could inline — so the secrets are written here for the Cloudflare +// plugin to load. Gitignored; written from the environment the e2e runner provides. +writeFileSync( + '.dev.vars', + [ + `E2E_TEST_DSN=${process.env.E2E_TEST_DSN ?? ''}`, + `OPENROUTER_API_KEY=${process.env.E2E_OPENROUTER_API_KEY ?? ''}`, + '', + ].join('\n'), +); From cfa4bc693acc64de040c5d833275f0e1d9c52ba3 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 18 Sep 2026 10:46:16 +0300 Subject: [PATCH 2/5] test(e2e): Run the cloudflare-flue suite against the built worker `vite preview` serves the worker that wrangler builds, which is the artifact the build-time registration this app exists to prove actually ships in. Co-Authored-By: Claude Opus 5 --- .../test-applications/cloudflare-flue/package.json | 4 +--- .../test-applications/cloudflare-flue/playwright.config.ts | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json index 8ddea4b23e05..22db322508fd 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json @@ -4,15 +4,13 @@ "private": true, "type": "module", "scripts": { - "dev": "vite dev --port 4112", "build": "vite build", "preview": "vite preview --port 4112", "typecheck": "tsc --noEmit", "clean": "npx rimraf node_modules dist .wrangler pnpm-lock.yaml", "test:build": "pnpm install && pnpm write-dev-vars && pnpm build", - "test:assert": "pnpm test:prod && pnpm test:dev", + "test:assert": "pnpm test:prod", "test:prod": "TEST_ENV=production OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test", - "test:dev": "TEST_ENV=development OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test", "write-dev-vars": "node write-dev-vars.mjs" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts index bbe77dd5b055..a11f1656965f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts @@ -7,9 +7,9 @@ if (!testEnv) { } const config = getPlaywrightConfig( - // `dev` is vite's dev server; `preview` serves the built worker through wrangler. Both run under - // workerd, so both exercise the build-time registration this app exists to prove. - { startCommand: testEnv === 'development' ? 'pnpm dev' : 'pnpm preview', port: 4112 }, + { startCommand: 'pnpm preview', port: 4112 }, + // Each test drives a real OpenRouter turn and then waits for the spans to flush, which does not + // fit the default 30s timeout when the provider is slow. { timeout: 90_000 }, ); From c9e80c7d393e2992449f1db9eadd9706ec690fa3 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 18 Sep 2026 11:43:32 +0300 Subject: [PATCH 3/5] fix(e2e): Align the cloudflare-flue hono range with Flue's own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@flue/runtime` depends on `hono@^4.8.3`, so a `~4.7.0` pin here cannot share it — pnpm installs a second copy and the app's router runs on 4.7.11 while Flue runs 4.13.8. That older copy also carries five high-severity advisories, which fails dependency review. Widening the range collapses both copies onto one. Co-Authored-By: Claude Opus 5 --- .../e2e-tests/test-applications/cloudflare-flue/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json index 22db322508fd..420d37a7bc0b 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json @@ -18,7 +18,7 @@ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "agents": "0.20.1", "dataloader": "~2.2.3", - "hono": "~4.7.0", + "hono": "^4.13.8", "valibot": "~1.5.0" }, "devDependencies": { From 9d21acff64a13010e26d24040bb506ddee66f6c8 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Mon, 21 Sep 2026 16:07:32 +0300 Subject: [PATCH 4/5] test(e2e): Drop the cloudflare-flue dev-vars shim `@cloudflare/vite-plugin` already populates the worker's secret bindings from the preview process environment, so writing a `.dev.vars` file first was dead weight. Running `vite preview` with `E2E_TEST_DSN` and `OPENROUTER_API_KEY` set and no `.dev.vars` on disk logs `Using secrets defined in process.env` and both land on `env`. `cloudflare-mastra` and `cloudflare-vercelai-v7` already rely on this. Also drop the `/experimental` entry point from the workers-types tsconfig setting, since nothing in `src/` needs it. Co-Authored-By: Claude Opus 5 --- .../test-applications/cloudflare-flue/.gitignore | 1 - .../test-applications/cloudflare-flue/package.json | 5 ++--- .../test-applications/cloudflare-flue/tsconfig.json | 2 +- .../cloudflare-flue/write-dev-vars.mjs | 13 ------------- 4 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore deleted file mode 100644 index babca1bb1d28..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.dev.vars diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json index 420d37a7bc0b..72451c99f60b 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json @@ -8,10 +8,9 @@ "preview": "vite preview --port 4112", "typecheck": "tsc --noEmit", "clean": "npx rimraf node_modules dist .wrangler pnpm-lock.yaml", - "test:build": "pnpm install && pnpm write-dev-vars && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod", - "test:prod": "TEST_ENV=production OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test", - "write-dev-vars": "node write-dev-vars.mjs" + "test:prod": "TEST_ENV=production OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test" }, "dependencies": { "@flue/runtime": "2.0.5", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json index 0db64daee718..c612b8a5194a 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json @@ -13,7 +13,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "types": ["@cloudflare/workers-types/experimental"] + "types": ["@cloudflare/workers-types"] }, "exclude": ["tests"], "include": ["src/**/*.ts"] diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs deleted file mode 100644 index c92867dff336..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/write-dev-vars.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { writeFileSync } from 'node:fs'; - -// `vite preview` takes no `--var`, and Flue resolves the provider key inside `pi-ai` at runtime -// rather than in app code Vite could inline — so the secrets are written here for the Cloudflare -// plugin to load. Gitignored; written from the environment the e2e runner provides. -writeFileSync( - '.dev.vars', - [ - `E2E_TEST_DSN=${process.env.E2E_TEST_DSN ?? ''}`, - `OPENROUTER_API_KEY=${process.env.E2E_OPENROUTER_API_KEY ?? ''}`, - '', - ].join('\n'), -); From 0c23ddb3f1ed013daedc6894988b9998b4b30703 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Mon, 21 Sep 2026 16:42:05 +0300 Subject: [PATCH 5/5] fix(e2e): Supply the cloudflare-flue provider key at build time `vite build` snapshots the secrets it can see into `dist/.dev.vars`, and `vite preview` then reads that file in preference to its own environment. So mapping `E2E_OPENROUTER_API_KEY` in `test:prod` was too late: the build had already written a `.dev.vars` holding only the DSN, and the preview used it and ignored the key the test script had set. Map it for the build instead. This is what `write-dev-vars.mjs` was doing, by writing the same file by hand one step earlier. Removing it in 9d21acf broke the suite with `Provider is not configured: openrouter`. Co-Authored-By: Claude Opus 5 --- .../e2e-tests/test-applications/cloudflare-flue/package.json | 2 +- .../test-applications/cloudflare-flue/wrangler.jsonc | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json index 72451c99f60b..ec82ef912a33 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json @@ -8,7 +8,7 @@ "preview": "vite preview --port 4112", "typecheck": "tsc --noEmit", "clean": "npx rimraf node_modules dist .wrangler pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", + "test:build": "pnpm install && OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY pnpm build", "test:assert": "pnpm test:prod", "test:prod": "TEST_ENV=production OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test" }, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc index 3e42e749cd5f..273779ec6976 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc @@ -4,7 +4,8 @@ "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FlueHelloAgent"] }], - // `vite preview` takes no `--var` flag, so secrets are declared here and read from the process - // environment the Playwright web server inherits. + // `vite build` snapshots these from the build-time environment into `dist/.dev.vars`, and + // `vite preview` reads that file in preference to its own environment. So both have to be set + // for the build, which is why `test:build` maps `E2E_OPENROUTER_API_KEY` across. "secrets": { "required": ["E2E_TEST_DSN", "OPENROUTER_API_KEY"] }, }