Skip to content

test(cloudflare): Add e2e test app for @cloudflare/think - #24660

Merged
RulaKhaled merged 5 commits into
developfrom
feat/cloudflare-think-e2e
Sep 28, 2026
Merged

RulaKhaled merged 5 commits into
developfrom
feat/cloudflare-think-e2e

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a cloudflare-think e2e app that records what a Think agent produces with the SDK as it stands today, before any Think-specific code exists. Same shape as node-eve and cloudflare-mastra.

The setup is the whole point: the worker imports no Sentry API at all.

// vite.config.mts
export default defineConfig({
  plugins: [cloudflare(), sentryCloudflareVitePlugin()],
});
// src/index.ts
export class ThinkAgent extends Think<Env> {
  getModel() { return openrouter('openai/gpt-4o-mini'); }
  getSystemPrompt() { /* ... */ }
  getTools() { /* get_weather, fail_now */ }
}

sentryCloudflareVitePlugin() detects extends Think and wraps the export at build time, and ai publishes the telemetry vercelAIIntegration already consumes. Options come from src/instrument.server.ts by convention.

What works today

Five checks, the same set the other AI apps cover:

  1. The gen_ai hierarchy. gen_ai.invoke_agent, one gen_ai.generate_content per step, and gen_ai.execute_tool, correctly nested and all with origin auto.vercelai.channel. Token usage is right per step and rolls up onto the agent span; prompts, tool arguments and results, tool definitions and finish reasons are all present.
  2. Tool errors. A throw becomes an issue with mechanism auto.vercelai.channel and marks only its own span errored, leaving the model calls ok.
  3. Manual spans. A Sentry.startSpan raised inside a tool nests under that tool's span, so Think runs tools inside the async context the SDK opened.
  4. The provider request. The outgoing model call joins the turn's trace rather than running beside it.
  5. Other instrumented libraries. dataloader spans land in the same trace, so the orchestrion module transform reaches a bundled Think worker with no --import bootstrap, unlike the Node apps.

What does not

The provider HTTP call nests under gen_ai.generate_content on ai v7 but is a sibling of it on v4 to v6. That difference is ours: only v7's native channel binds the model-call span into async context. Asserted per lane so the gap is recorded rather than hidden.

The agent span is named from this.constructor.name, which the Vite plugin's class rename currently mangles into invoke_agent __SENTRY_ORIGINAL_ThinkAgent__. Not asserted here. A follow-up fixes it and adds the assertion.

There is no gen_ai.agent.name, and none of the cloudflare.agents.turn.* context Think supplies (agent id, Durable Object conversation id, turn trigger, continuation) reaches a span. Think sends all of it, as runtimeContext on v7 and experimental_telemetry.metadata on v6, and neither subscriber reads either field.

Notes

sentryTest.optional: true keeps this in the optional matrix like node-eve, since it makes real OpenRouter calls. The v6 lane rides along as an optionalVariants entry rather than a second app. E2E_OPENROUTER_API_KEY is already wired in build.yml from the eve PR, so CI needs no change.

ref JS-3568

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Sep 23, 2026

Copy link
Copy Markdown

JS-3568

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.24 kB - -
@sentry/browser - with treeshaking flags 27.5 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.4 kB - -
@sentry/browser (incl. Tracing) 51.15 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 51.17 kB - -
@sentry/browser (incl. Tracing, Profiling) 54.18 kB - -
@sentry/browser (incl. Tracing, Replay) 90.76 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.86 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 95.46 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 108.41 kB - -
@sentry/browser (incl. Feedback) 46.76 kB - -
@sentry/browser (incl. sendFeedback) 34.3 kB - -
@sentry/browser (incl. FeedbackAsync) 39.41 kB - -
@sentry/browser (incl. Metrics) 30.25 kB - -
@sentry/browser (incl. Logs) 30.51 kB - -
@sentry/browser (incl. Metrics & Logs) 31.18 kB - -
@sentry/react 31.08 kB - -
@sentry/react (incl. Tracing) 53.54 kB - -
@sentry/vue 36.74 kB - -
@sentry/vue (incl. Tracing) 53.7 kB - -
@sentry/svelte 29.26 kB - -
CDN Bundle 31.02 kB - -
CDN Bundle (incl. Tracing) 51.77 kB - -
CDN Bundle (incl. Logs, Metrics) 33.29 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 53.75 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74 kB - -
CDN Bundle (incl. Tracing, Replay) 89.36 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 91.33 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 95.53 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 97.5 kB - -
CDN Bundle - uncompressed 91.66 kB - -
CDN Bundle (incl. Tracing) - uncompressed 154.03 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 98.23 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 159.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 227.8 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 273.76 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 279.7 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 287.46 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 293.39 kB - -
@sentry/nextjs (client) 55.77 kB - -
@sentry/sveltekit (client) 51.59 kB - -
@sentry/core/server 39.95 kB - -
@sentry/core/browser 13.63 kB - -
@sentry/node 137.15 kB +0.02% +14 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.8 kB - -
@sentry/node - without tracing 90.81 kB +0.02% +15 B 🔺
@sentry/node - without channel injection 115.51 kB +0.01% +8 B 🔺
@sentry/aws-serverless 99.08 kB +0.01% +8 B 🔺
@sentry/cloudflare (withSentry) - minified 206.62 kB - -
@sentry/cloudflare (withSentry) 514.02 kB - -

View base workflow run

@RulaKhaled
RulaKhaled marked this pull request as ready for review September 24, 2026 08:51
Comment thread dev-packages/e2e-tests/test-applications/cloudflare-think/tests/think.test.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread dev-packages/e2e-tests/test-applications/cloudflare-think/playwright.config.ts Outdated
Comment on lines +59 to +60
execute: async (_input: { reason: string }): Promise<string> => {
throw new Error('Think tool failed on purpose');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The runTurn method can return a result where continuation is undefined due to a race condition. This value is used without a check, leading to an incorrect API response.
Severity: MEDIUM

Suggested Fix

Add a check to ensure result and result.continuation are not undefined before returning the response. If result.continuation is missing, consider throwing an error or returning a more explicit error response to the client instead of an empty object. For example: if (!result?.continuation) { throw new Error('Failed to get continuation from agent turn.'); } return Response.json({ continuation: result.continuation });.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: dev-packages/e2e-tests/test-applications/cloudflare-think/src/index.ts#L59-L60

Potential issue: In
`dev-packages/e2e-tests/test-applications/cloudflare-think/src/index.ts`, the
`onRequest` method calls `runTurn` and then directly accesses `result.continuation` to
build the JSON response. Due to a known race condition in the `@cloudflare/think`
library, concurrent agent turns can cause `runTurn` to resolve with a result where the
`continuation` property is `undefined`. When this happens, `Response.json({
continuation: undefined })` serializes to an empty object `{}`, breaking the API
contract and causing silent failures for clients that expect the `continuation` field.
This race condition is reproducible in the e2e test environment.

RulaKhaled added a commit that referenced this pull request Sep 25, 2026
…ort (#24700)

The Vite transform freed an exported class's binding for the wrapper by
renaming the declaration to `__SENTRY_ORIGINAL_<Name>__`. That also
renames `Function.prototype.name`, and libraries read it:
`@cloudflare/think` passes `this.constructor.name` to the AI SDK as the
`functionId`, so a zero-config agent span came out as `invoke_agent
__SENTRY_ORIGINAL_ThinkAgent__`, and `agents` puts the same string in
`gen_ai.agent.name`.

Only the export has to be the wrapper, so the declaration now stays
exactly as written and the export is aliased to a wrapper binding. That
is the shape the transform already emits for classes re-exported from
another module and the shape the docs show for manual wrapping, so local
and cross-module classes now share one code path. A default export of an
already-wrapped class is re-pointed at that binding.

The one observable change is that in-module references to the class see
the original rather than the wrapper. That matches manual wrapping, and
Durable Objects, Agents, Workflows and entrypoints are constructed by
the runtime through the export.

Verified against a real worker with the `cloudflare-autoinstrument` e2e
app, where `gen_ai.agent.name` is now an exact match. The Think
span-name assertions land with #24660.

Fixes #24696

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
RulaKhaled and others added 3 commits September 25, 2026 11:52
Records what a Think agent produces with the current SDK, before any
Think-specific code exists. The worker imports no Sentry API at all:
`sentryCloudflareVitePlugin()` detects `class ThinkAgent extends Think` and
wraps the export at build time, and `ai` publishes the telemetry
`vercelAIIntegration` already consumes.

Covers the same five things the other AI apps do: the gen_ai hierarchy, a tool
throw becoming an issue with an errored span, a manual span nesting under the
tool span, the provider request staying inside the turn, and `dataloader`
proving the orchestrion transform reaches a bundled worker.

Runs on `ai` v7 by default with a v6 lane as an optional variant, because
Think's peer range spans both and the two produce different trace shapes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`getPlaywrightConfig` already defaults to `workers: 1` and a 30s timeout. The app
overrode both, so five tests each driving a live OpenRouter turn ran in parallel
under a cap meant for mocked models. That came from copying
`cloudflare-vercelai-v7`, which mocks its model; the two apps that call a real
provider, `cloudflare-mastra` and `node-flue`, keep the serial default and raise
the timeout to 90s instead.

Also corrects the comment above the first test, which still claimed the worker
imports no Sentry API. That stopped being true when the manual-span test was
added, and the worker now imports `startSpan` for exactly that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…name

Depends on #24700, which stops the Vite transform from renaming the wrapped class.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@JPeer264 JPeer264 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

Comment on lines +1 to +8
import type { CloudflareOptions } from '@sentry/cloudflare';

export default (env: Env): CloudflareOptions => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa',
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1.0,
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: We should use defineCloudflareOptions instead

Suggested change
import type { CloudflareOptions } from '@sentry/cloudflare';
export default (env: Env): CloudflareOptions => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa',
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1.0,
});
import { defineCloudflareOptions } from '@sentry/cloudflare';
export default defineCloudflareOptions((env) => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa',
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1.0,
}));

* into `instrumentAgentWithSentry(...)` at build time. Wrapping it here would prove nothing about
* the zero-config path.
*/
export class ThinkAgent extends Think<Env> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Since auto wrapping is super tough within Vite it would be better if this class would be defined in another file and then re-exported. So in the future we'd know if this would break.

export { ThinkAgent } from './think-agent.ts';

"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"types": ["@cloudflare/workers-types/experimental"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l:

Suggested change
"types": ["@cloudflare/workers-types/experimental"]
"types": ["@cloudflare/workers-types"]

Review follow-ups on #24660.

The agent class now lives in `src/think-agent.ts` and the worker entry only
re-exports it. That is the harder case for the Vite plugin: the entry never
binds the class, so detection has to follow the re-export and the wrapper has
to import the class under a private name. The built worker still wraps it with
`instrumentAgentWithSentry` and keeps the class name, so the
`invoke_agent ThinkAgent` assertion holds.

Also use `defineCloudflareOptions` in `instrument.server.ts`, which is the
documented shape for the plugin's options module, and switch `tsconfig.json`
to the stable `@cloudflare/workers-types` entry.

Suite is green on both the default lane and the `ai v6` variant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@RulaKhaled
RulaKhaled enabled auto-merge (squash) September 28, 2026 10:01
@RulaKhaled
RulaKhaled disabled auto-merge September 28, 2026 10:33

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5994ebc. Configure here.

const invokeAgent = spans.find(span => getSpanOp(span) === 'gen_ai.invoke_agent');
const parent = spans.find(span => span.span_id === providerRequest?.parent_span_id);

expect(providerRequest?.trace_id).toBe(invokeAgent?.trace_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test drops late invoke_agent span

Medium Severity

The provider HTTP test returns from collectStreamedSpans once generate_content and the OpenRouter http.client span exist, then reads invoke_agent for the shared-trace check and, on the v6 lane, as the HTTP parent. That agent span is the parent that stays open and flushes in a later envelope, so it is often still missing when the predicate succeeds and those assertions fail.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5994ebc. Configure here.

@RulaKhaled
RulaKhaled merged commit bc94aca into develop Sep 28, 2026
44 checks passed
@RulaKhaled
RulaKhaled deleted the feat/cloudflare-think-e2e branch September 28, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants