From f26828705a8a9678202daf3ba52a70a9e24af855 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 17 Sep 2026 17:09:05 +0300 Subject: [PATCH 1/3] 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 d1d4ce9c3cc8e00fa881c9e939bdb079435f8e6c Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 18 Sep 2026 10:46:16 +0300 Subject: [PATCH 2/3] 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 cafe6444a79fa9260cf49b2010b4b0ebde749baa Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 18 Sep 2026 11:43:32 +0300 Subject: [PATCH 3/3] 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": {