From c99fc287cf4ad03daa542a66dda0bf03e336681c Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Fri, 17 Jul 2026 17:09:03 +0200 Subject: [PATCH 1/9] fixup! feat(core): Instrument workers-ai-provider --- .../suites/tracing/workers-ai/index.ts | 30 ++++++++++++++ .../suites/tracing/workers-ai/test.ts | 39 +++++++++++++++++++ .../suites/tracing/workers-ai/wrangler.jsonc | 6 +++ packages/cloudflare/src/index.ts | 1 + 4 files changed, 76 insertions(+) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts new file mode 100644 index 000000000000..eb9a4d7512e7 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; +} + +// A stand-in for the `env.AI` binding whose `run` rejects, so we can assert that a +// failing Workers AI call bubbles up out of the handler and is reported by the +// top-level Cloudflare instrumentation, rather than being captured inside the +// Workers AI integration itself. +const ai = { + run: async (_model: string, _inputs: Record) => { + throw new Error('Workers AI run failed'); + }, +}; + +const instrumentedAi = Sentry.instrumentWorkersAiClient(ai); + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + }), + { + async fetch(_request, _env, _ctx) { + const result = await instrumentedAi.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); + + return new Response(JSON.stringify(result)); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts new file mode 100644 index 000000000000..364375f9771e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts @@ -0,0 +1,39 @@ +import { expect, it } from 'vitest'; +import { eventEnvelope } from '../../../expect'; +import { createRunner } from '../../../runner'; + +// The Workers AI integration deliberately does not call `captureException` itself. +// When a `run` call fails, the error must bubble up out of the fetch handler and be +// reported by the top-level Cloudflare instrumentation instead — so it shows up in +// Sentry exactly once, with the `auto.http.cloudflare` mechanism. +it('bubbles up Workers AI errors to be captured by the top-level handler', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect( + eventEnvelope( + { + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'Workers AI run failed', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'auto.http.cloudflare', handled: false }, + }, + ], + }, + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.any(String), + }, + }, + { includeTransaction: false }, + ), + ) + .start(signal); + await runner.makeRequest('get', '/', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc new file mode 100644 index 000000000000..2cdb43e06500 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "workers-ai-worker", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_als"], +} diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 20a537c5b307..7a2cce03ee7b 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -79,6 +79,7 @@ export { instrumentOpenAiClient, instrumentGoogleGenAIClient, instrumentAnthropicAiClient, + instrumentWorkersAiClient, eventFiltersIntegration, linkedErrorsIntegration, requestDataIntegration, From 964cfee3316cf8eafd1ee8900472d2862be86781 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Sat, 18 Jul 2026 14:14:25 +0200 Subject: [PATCH 2/9] feat(core): Emit gen_ai.output.messages from Workers AI instrumentation The Sentry product reads model output from `gen_ai.output.messages`, treating `gen_ai.response.text` and `gen_ai.response.tool_calls` as deprecated. Relay migrates `response.text` into `output.messages` at ingestion, but the tool-calls half of that migration is lossy, so tool-call turns rendered an empty Output tab. Build `gen_ai.output.messages` directly in both the streaming and non-streaming paths (mirroring the Vercel AI integration), normalizing the OpenAI-compatible (`function.{name,arguments}`) and native (top-level `name`/`arguments`) tool-call shapes. The deprecated attributes are still written for backward compatibility. The streaming parser is also extended to read the OpenAI-compatible SSE shape (`choices[].delta.content` / `choices[].delta.tool_calls`) that models routed through the OpenAI-compatible endpoint emit, which previously dropped both text and tool calls while still capturing usage. Co-Authored-By: Claude Opus 4.8 --- .../suites/tracing/workers-ai/index.ts | 48 ++++-- .../suites/tracing/workers-ai/mocks.ts | 45 ++++++ .../suites/tracing/workers-ai/test.ts | 139 ++++++++++++++++-- 3 files changed, 204 insertions(+), 28 deletions(-) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts index eb9a4d7512e7..177ab580d5e1 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -1,28 +1,50 @@ import * as Sentry from '@sentry/cloudflare'; +import { MockAi } from './mocks'; interface Env { SENTRY_DSN: string; } -// A stand-in for the `env.AI` binding whose `run` rejects, so we can assert that a -// failing Workers AI call bubbles up out of the handler and is reported by the -// top-level Cloudflare instrumentation, rather than being captured inside the -// Workers AI integration itself. -const ai = { - run: async (_model: string, _inputs: Record) => { - throw new Error('Workers AI run failed'); - }, -}; - -const instrumentedAi = Sentry.instrumentWorkersAiClient(ai); +const ai = Sentry.instrumentWorkersAiClient(new MockAi()); export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + // Keep gen_ai spans embedded in the transaction (instead of streamed as a + // separate envelope container) so they can be asserted on `transaction.spans`. + streamGenAiSpans: false, }), { - async fetch(_request, _env, _ctx) { - const result = await instrumentedAi.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === '/error') { + // The Workers AI integration deliberately does not call `captureException` itself. + // A failing `run` must bubble up out of the handler so the top-level Cloudflare + // instrumentation reports it instead — showing up in Sentry exactly once. + const result = await ai.run('error-model', { prompt: 'Hello' }); + return new Response(JSON.stringify(result)); + } + + if (url.pathname === '/stream') { + const stream = (await ai.run('@cf/meta/llama-3.1-8b-instruct', { + messages: [{ role: 'user', content: 'What is the capital of France?' }], + stream: true, + })) as ReadableStream; + + const text = await new Response(stream).text(); + return new Response(text); + } + + const result = await ai.run('@cf/meta/llama-3.1-8b-instruct', { + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'What is the capital of France?' }, + ], + temperature: 0.7, + max_tokens: 100, + }); return new Response(JSON.stringify(result)); }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts new file mode 100644 index 000000000000..bce096d4ae75 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts @@ -0,0 +1,45 @@ +import { simulateReadableStream } from 'ai'; + +function createSseStream(events: string[]): ReadableStream { + const encoder = new TextEncoder(); + return simulateReadableStream({ + initialDelayInMs: 0, + chunkDelayInMs: 0, + chunks: events.map(event => encoder.encode(`data: ${event}\n\n`)), + }); +} + +/** + * Minimal mock of the Cloudflare Workers AI binding (`env.AI`). + */ +export class MockAi { + public async run(model: string, inputs: Record): Promise { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + if (model === 'error-model') { + const error = new Error('Model not found'); + (error as unknown as { status: number }).status = 404; + throw error; + } + + if (inputs?.stream === true) { + return createSseStream([ + '{"response":"The capital "}', + '{"response":"of France "}', + '{"response":"is Paris."}', + '{"response":"","usage":{"prompt_tokens":12,"completion_tokens":7,"total_tokens":19}}', + '[DONE]', + ]); + } + + return { + response: 'The capital of France is Paris.', + usage: { + prompt_tokens: 12, + completion_tokens: 7, + total_tokens: 19, + }, + }; + } +} diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts index 364375f9771e..e5c835f37508 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts @@ -1,39 +1,148 @@ +import { GEN_AI_PROVIDER_NAME } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; -import { eventEnvelope } from '../../../expect'; +import { + GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, + GEN_AI_REQUEST_MODEL_ATTRIBUTE, + GEN_AI_REQUEST_STREAM_ATTRIBUTE, + GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, + GEN_AI_RESPONSE_STREAMING_ATTRIBUTE, + GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, +} from '../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; import { createRunner } from '../../../runner'; +// These tests are not exhaustive because the instrumentation is +// already tested in the core unit tests and we merely want to test +// that the instrumentation does not break in our cloudflare SDK. + +it('traces a basic Workers AI text generation request', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + + // The transaction event is framework-generated and carries non-deterministic fields + // (random ports, ids, timestamps, sdk version), so we assert the stable subset. + expect(transactionEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'GET /', + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.cloudflare', + status: 'ok', + }), + }), + spans: [ + expect.objectContaining({ + description: 'chat @cf/meta/llama-3.1-8b-instruct', + op: 'gen_ai.chat', + origin: 'auto.ai.cloudflare.workers_ai', + data: { + 'sentry.origin': 'auto.ai.cloudflare.workers_ai', + 'sentry.op': 'gen_ai.chat', + [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', + [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, + [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, + }, + }), + ], + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/'); + await runner.completed(); +}); + +it('traces a streaming Workers AI text generation request', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'GET /stream', + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.cloudflare', + status: 'ok', + }), + }), + spans: [ + expect.objectContaining({ + description: 'chat @cf/meta/llama-3.1-8b-instruct', + op: 'gen_ai.chat', + origin: 'auto.ai.cloudflare.workers_ai', + data: { + 'sentry.origin': 'auto.ai.cloudflare.workers_ai', + 'sentry.op': 'gen_ai.chat', + [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', + [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, + [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, + }, + }), + ], + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/stream'); + await runner.completed(); +}); + // The Workers AI integration deliberately does not call `captureException` itself. // When a `run` call fails, the error must bubble up out of the fetch handler and be // reported by the top-level Cloudflare instrumentation instead — so it shows up in // Sentry exactly once, with the `auto.http.cloudflare` mechanism. it('bubbles up Workers AI errors to be captured by the top-level handler', async ({ signal }) => { const runner = createRunner(__dirname) - .expect( - eventEnvelope( - { + // A failing run still produces a (sampled) transaction; we only care about the error event here. + .ignore('transaction') + .expect(envelope => { + const errorEvent = envelope[1]?.[0]?.[1] as any; + + expect(errorEvent).toEqual( + expect.objectContaining({ level: 'error', exception: { values: [ - { + expect.objectContaining({ type: 'Error', - value: 'Workers AI run failed', + value: 'Model not found', stacktrace: { frames: expect.any(Array), }, mechanism: { type: 'auto.http.cloudflare', handled: false }, - }, + }), ], }, - request: { - headers: expect.any(Object), + request: expect.objectContaining({ method: 'GET', url: expect.any(String), - }, - }, - { includeTransaction: false }, - ), - ) + }), + }), + ); + }) .start(signal); - await runner.makeRequest('get', '/', { expectError: true }); + await runner.makeRequest('get', '/error', { expectError: true }); await runner.completed(); }); From e0f5d170c6f9d68dbc6571df1d4a53e1bcb66e06 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 20 Jul 2026 09:28:43 +0200 Subject: [PATCH 3/9] ref: wrong PR --- .../suites/tracing/workers-ai/index.ts | 52 ------ .../suites/tracing/workers-ai/mocks.ts | 45 ------ .../suites/tracing/workers-ai/test.ts | 148 ------------------ .../suites/tracing/workers-ai/wrangler.jsonc | 6 - 4 files changed, 251 deletions(-) delete mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts delete mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts delete mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts delete mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts deleted file mode 100644 index 177ab580d5e1..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as Sentry from '@sentry/cloudflare'; -import { MockAi } from './mocks'; - -interface Env { - SENTRY_DSN: string; -} - -const ai = Sentry.instrumentWorkersAiClient(new MockAi()); - -export default Sentry.withSentry( - (env: Env) => ({ - dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, - // Keep gen_ai spans embedded in the transaction (instead of streamed as a - // separate envelope container) so they can be asserted on `transaction.spans`. - streamGenAiSpans: false, - }), - { - async fetch(request) { - const url = new URL(request.url); - - if (url.pathname === '/error') { - // The Workers AI integration deliberately does not call `captureException` itself. - // A failing `run` must bubble up out of the handler so the top-level Cloudflare - // instrumentation reports it instead — showing up in Sentry exactly once. - const result = await ai.run('error-model', { prompt: 'Hello' }); - return new Response(JSON.stringify(result)); - } - - if (url.pathname === '/stream') { - const stream = (await ai.run('@cf/meta/llama-3.1-8b-instruct', { - messages: [{ role: 'user', content: 'What is the capital of France?' }], - stream: true, - })) as ReadableStream; - - const text = await new Response(stream).text(); - return new Response(text); - } - - const result = await ai.run('@cf/meta/llama-3.1-8b-instruct', { - messages: [ - { role: 'system', content: 'You are a helpful assistant.' }, - { role: 'user', content: 'What is the capital of France?' }, - ], - temperature: 0.7, - max_tokens: 100, - }); - - return new Response(JSON.stringify(result)); - }, - }, -); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts deleted file mode 100644 index bce096d4ae75..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { simulateReadableStream } from 'ai'; - -function createSseStream(events: string[]): ReadableStream { - const encoder = new TextEncoder(); - return simulateReadableStream({ - initialDelayInMs: 0, - chunkDelayInMs: 0, - chunks: events.map(event => encoder.encode(`data: ${event}\n\n`)), - }); -} - -/** - * Minimal mock of the Cloudflare Workers AI binding (`env.AI`). - */ -export class MockAi { - public async run(model: string, inputs: Record): Promise { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); - - if (model === 'error-model') { - const error = new Error('Model not found'); - (error as unknown as { status: number }).status = 404; - throw error; - } - - if (inputs?.stream === true) { - return createSseStream([ - '{"response":"The capital "}', - '{"response":"of France "}', - '{"response":"is Paris."}', - '{"response":"","usage":{"prompt_tokens":12,"completion_tokens":7,"total_tokens":19}}', - '[DONE]', - ]); - } - - return { - response: 'The capital of France is Paris.', - usage: { - prompt_tokens: 12, - completion_tokens: 7, - total_tokens: 19, - }, - }; - } -} diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts deleted file mode 100644 index e5c835f37508..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { GEN_AI_PROVIDER_NAME } from '@sentry/conventions/attributes'; -import { expect, it } from 'vitest'; -import { - GEN_AI_OPERATION_NAME_ATTRIBUTE, - GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, - GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_REQUEST_STREAM_ATTRIBUTE, - GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, - GEN_AI_RESPONSE_STREAMING_ATTRIBUTE, - GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, - GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, - GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, -} from '../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; -import { createRunner } from '../../../runner'; - -// These tests are not exhaustive because the instrumentation is -// already tested in the core unit tests and we merely want to test -// that the instrumentation does not break in our cloudflare SDK. - -it('traces a basic Workers AI text generation request', async ({ signal }) => { - const runner = createRunner(__dirname) - .ignore('event') - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as any; - - // The transaction event is framework-generated and carries non-deterministic fields - // (random ports, ids, timestamps, sdk version), so we assert the stable subset. - expect(transactionEvent).toEqual( - expect.objectContaining({ - type: 'transaction', - transaction: 'GET /', - transaction_info: { source: 'route' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.cloudflare', - status: 'ok', - }), - }), - spans: [ - expect.objectContaining({ - description: 'chat @cf/meta/llama-3.1-8b-instruct', - op: 'gen_ai.chat', - origin: 'auto.ai.cloudflare.workers_ai', - data: { - 'sentry.origin': 'auto.ai.cloudflare.workers_ai', - 'sentry.op': 'gen_ai.chat', - [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, - }, - }), - ], - }), - ); - }) - .start(signal); - await runner.makeRequest('get', '/'); - await runner.completed(); -}); - -it('traces a streaming Workers AI text generation request', async ({ signal }) => { - const runner = createRunner(__dirname) - .ignore('event') - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as any; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - type: 'transaction', - transaction: 'GET /stream', - transaction_info: { source: 'url' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.cloudflare', - status: 'ok', - }), - }), - spans: [ - expect.objectContaining({ - description: 'chat @cf/meta/llama-3.1-8b-instruct', - op: 'gen_ai.chat', - origin: 'auto.ai.cloudflare.workers_ai', - data: { - 'sentry.origin': 'auto.ai.cloudflare.workers_ai', - 'sentry.op': 'gen_ai.chat', - [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, - }, - }), - ], - }), - ); - }) - .start(signal); - await runner.makeRequest('get', '/stream'); - await runner.completed(); -}); - -// The Workers AI integration deliberately does not call `captureException` itself. -// When a `run` call fails, the error must bubble up out of the fetch handler and be -// reported by the top-level Cloudflare instrumentation instead — so it shows up in -// Sentry exactly once, with the `auto.http.cloudflare` mechanism. -it('bubbles up Workers AI errors to be captured by the top-level handler', async ({ signal }) => { - const runner = createRunner(__dirname) - // A failing run still produces a (sampled) transaction; we only care about the error event here. - .ignore('transaction') - .expect(envelope => { - const errorEvent = envelope[1]?.[0]?.[1] as any; - - expect(errorEvent).toEqual( - expect.objectContaining({ - level: 'error', - exception: { - values: [ - expect.objectContaining({ - type: 'Error', - value: 'Model not found', - stacktrace: { - frames: expect.any(Array), - }, - mechanism: { type: 'auto.http.cloudflare', handled: false }, - }), - ], - }, - request: expect.objectContaining({ - method: 'GET', - url: expect.any(String), - }), - }), - ); - }) - .start(signal); - await runner.makeRequest('get', '/error', { expectError: true }); - await runner.completed(); -}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc deleted file mode 100644 index 2cdb43e06500..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "workers-ai-worker", - "compatibility_date": "2025-06-17", - "main": "index.ts", - "compatibility_flags": ["nodejs_als"], -} From 532407560bd3a9f999dc229b3918708953d2d825 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 8 Jul 2026 22:11:01 +0200 Subject: [PATCH 4/9] feat(cloudflare): Auto-instrument Workers AI binding via env instrumentation Detect the Workers AI binding (env.AI) in instrumentEnv via duck-typing (run + gateway + toMarkdown) and wrap it automatically, matching how D1, R2, and Queue bindings are instrumented. Manual wrapping via instrumentWorkersAiClient remains available for custom options and is now guarded against double-wrapping. Also removes the unused WORKERS_AI_INTEGRATION_NAME constant and aligns the integration test wrangler config with sibling suites (nodejs_als). Co-Authored-By: Claude Fable 5 --- .../instrumentations/worker/instrumentEnv.ts | 9 ++++ packages/cloudflare/src/utils/isBinding.ts | 16 ++++++- .../instrumentations/instrumentEnv.test.ts | 43 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts index 4f8eb0aa3142..474fbf831b86 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts @@ -1,6 +1,8 @@ import { isObjectLike } from '@sentry/core'; +import { instrumentWorkersAiClient } from '@sentry/core'; import type { CloudflareOptions } from '../../client'; import { + isAiBinding, isD1Database, isDurableObjectNamespace, isJSRPC, @@ -33,6 +35,7 @@ const instrumentedBindings = new WeakMap(); * - Queue producers (via `send` + `sendBatch` duck-typing) * - R2 Buckets (via `head` + `put` + `createMultipartUpload` duck-typing) * - Rate limiters (via `limit` duck-typing) + * - Workers AI (via `run` + `gateway` + `toMarkdown` duck-typing) * * @param env - The Cloudflare env object to instrument * @param options - Optional CloudflareOptions to control RPC trace propagation @@ -85,6 +88,12 @@ export function instrumentEnv>(env: Env, opt return instrumented; } + if (isAiBinding(item)) { + const instrumented = instrumentWorkersAiClient(item); + instrumentedBindings.set(item, instrumented); + return instrumented; + } + if (!rpcPropagation) { return item; } diff --git a/packages/cloudflare/src/utils/isBinding.ts b/packages/cloudflare/src/utils/isBinding.ts index c8f6387f3c07..4f4e6d012c74 100644 --- a/packages/cloudflare/src/utils/isBinding.ts +++ b/packages/cloudflare/src/utils/isBinding.ts @@ -31,7 +31,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import type { D1Database, DurableObjectNamespace, Queue, R2Bucket, RateLimit } from '@cloudflare/workers-types'; +import type { Ai, D1Database, DurableObjectNamespace, Queue, R2Bucket, RateLimit } from '@cloudflare/workers-types'; /** * Checks if a value is a JSRPC proxy (service binding). @@ -82,6 +82,20 @@ export function isD1Database(item: unknown): item is D1Database { ); } +/** + * Duck-type check for Workers AI bindings. + * The Ai binding has `run`, `gateway`, and `toMarkdown` methods. + */ +export function isAiBinding(item: unknown): item is Ai { + return ( + item != null && + isNotJSRPC(item) && + typeof item.run === 'function' && + typeof item.gateway === 'function' && + typeof item.toMarkdown === 'function' + ); +} + /** * Duck-type check for R2 Bucket bindings. * R2Bucket has `head`, `put`, and `createMultipartUpload` methods. diff --git a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts index 40c575898de0..72f9d0774507 100644 --- a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts @@ -284,6 +284,49 @@ describe('instrumentEnv', () => { expect(instrumented.MY_RATE_LIMITER).toBe(instrumented.MY_RATE_LIMITER); }); + describe('Workers AI bindings', () => { + function createMockAiBinding() { + return { + run: vi.fn().mockResolvedValue({ response: 'Paris', usage: { prompt_tokens: 1, completion_tokens: 2 } }), + gateway: vi.fn(), + toMarkdown: vi.fn(), + models: vi.fn(), + autorag: vi.fn(), + }; + } + + it('detects and wraps AI bindings, forwarding run calls unchanged', async () => { + const ai = createMockAiBinding(); + const env = { AI: ai }; + const instrumented = instrumentEnv(env); + + const wrapped = instrumented.AI as typeof ai; + expect(wrapped).not.toBe(ai); + + const result = await wrapped.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); + + expect(ai.run).toHaveBeenCalledTimes(1); + expect(ai.run).toHaveBeenCalledWith('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); + expect(result).toEqual({ response: 'Paris', usage: { prompt_tokens: 1, completion_tokens: 2 } }); + }); + + it('caches the wrapped AI binding across repeated access', () => { + const ai = createMockAiBinding(); + const env = { AI: ai }; + const instrumented = instrumentEnv(env); + + expect(instrumented.AI).toBe(instrumented.AI); + }); + + it('does not treat bindings with only a run method as AI bindings', () => { + const notAi = { run: vi.fn() }; + const env = { RUNNER: notAi }; + const instrumented = instrumentEnv(env); + + expect(instrumented.RUNNER).toBe(notAi); + }); + }); + describe('mTLS Fetcher bindings', () => { function createMtlsFetcherProxy(mockFetch: ReturnType) { return new Proxy( From 3396d10c4b35d4e63791197704878a080808f545 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 13 Jul 2026 17:09:26 +0200 Subject: [PATCH 5/9] chore: Update size-limits --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 2dac16a7922c..a7f84c55b573 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -460,7 +460,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '183 KiB', + limit: '193 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { @@ -480,7 +480,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '448 KiB', + limit: '470 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { From 15e1152dd45de8b962e55e7196521575fb905c93 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 20 Jul 2026 09:29:24 +0200 Subject: [PATCH 6/9] test(cloudflare): Add workers-ai tests --- .../suites/tracing/workers-ai/index.ts | 52 ++++++ .../suites/tracing/workers-ai/mocks.ts | 45 ++++++ .../suites/tracing/workers-ai/test.ts | 148 ++++++++++++++++++ .../suites/tracing/workers-ai/wrangler.jsonc | 6 + 4 files changed, 251 insertions(+) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts new file mode 100644 index 000000000000..177ab580d5e1 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -0,0 +1,52 @@ +import * as Sentry from '@sentry/cloudflare'; +import { MockAi } from './mocks'; + +interface Env { + SENTRY_DSN: string; +} + +const ai = Sentry.instrumentWorkersAiClient(new MockAi()); + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + // Keep gen_ai spans embedded in the transaction (instead of streamed as a + // separate envelope container) so they can be asserted on `transaction.spans`. + streamGenAiSpans: false, + }), + { + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === '/error') { + // The Workers AI integration deliberately does not call `captureException` itself. + // A failing `run` must bubble up out of the handler so the top-level Cloudflare + // instrumentation reports it instead — showing up in Sentry exactly once. + const result = await ai.run('error-model', { prompt: 'Hello' }); + return new Response(JSON.stringify(result)); + } + + if (url.pathname === '/stream') { + const stream = (await ai.run('@cf/meta/llama-3.1-8b-instruct', { + messages: [{ role: 'user', content: 'What is the capital of France?' }], + stream: true, + })) as ReadableStream; + + const text = await new Response(stream).text(); + return new Response(text); + } + + const result = await ai.run('@cf/meta/llama-3.1-8b-instruct', { + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'What is the capital of France?' }, + ], + temperature: 0.7, + max_tokens: 100, + }); + + return new Response(JSON.stringify(result)); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts new file mode 100644 index 000000000000..bce096d4ae75 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts @@ -0,0 +1,45 @@ +import { simulateReadableStream } from 'ai'; + +function createSseStream(events: string[]): ReadableStream { + const encoder = new TextEncoder(); + return simulateReadableStream({ + initialDelayInMs: 0, + chunkDelayInMs: 0, + chunks: events.map(event => encoder.encode(`data: ${event}\n\n`)), + }); +} + +/** + * Minimal mock of the Cloudflare Workers AI binding (`env.AI`). + */ +export class MockAi { + public async run(model: string, inputs: Record): Promise { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + if (model === 'error-model') { + const error = new Error('Model not found'); + (error as unknown as { status: number }).status = 404; + throw error; + } + + if (inputs?.stream === true) { + return createSseStream([ + '{"response":"The capital "}', + '{"response":"of France "}', + '{"response":"is Paris."}', + '{"response":"","usage":{"prompt_tokens":12,"completion_tokens":7,"total_tokens":19}}', + '[DONE]', + ]); + } + + return { + response: 'The capital of France is Paris.', + usage: { + prompt_tokens: 12, + completion_tokens: 7, + total_tokens: 19, + }, + }; + } +} diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts new file mode 100644 index 000000000000..e5c835f37508 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts @@ -0,0 +1,148 @@ +import { GEN_AI_PROVIDER_NAME } from '@sentry/conventions/attributes'; +import { expect, it } from 'vitest'; +import { + GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, + GEN_AI_REQUEST_MODEL_ATTRIBUTE, + GEN_AI_REQUEST_STREAM_ATTRIBUTE, + GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, + GEN_AI_RESPONSE_STREAMING_ATTRIBUTE, + GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, +} from '../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; +import { createRunner } from '../../../runner'; + +// These tests are not exhaustive because the instrumentation is +// already tested in the core unit tests and we merely want to test +// that the instrumentation does not break in our cloudflare SDK. + +it('traces a basic Workers AI text generation request', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + + // The transaction event is framework-generated and carries non-deterministic fields + // (random ports, ids, timestamps, sdk version), so we assert the stable subset. + expect(transactionEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'GET /', + transaction_info: { source: 'route' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.cloudflare', + status: 'ok', + }), + }), + spans: [ + expect.objectContaining({ + description: 'chat @cf/meta/llama-3.1-8b-instruct', + op: 'gen_ai.chat', + origin: 'auto.ai.cloudflare.workers_ai', + data: { + 'sentry.origin': 'auto.ai.cloudflare.workers_ai', + 'sentry.op': 'gen_ai.chat', + [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', + [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, + [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, + }, + }), + ], + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/'); + await runner.completed(); +}); + +it('traces a streaming Workers AI text generation request', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'GET /stream', + transaction_info: { source: 'url' }, + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + origin: 'auto.http.cloudflare', + status: 'ok', + }), + }), + spans: [ + expect.objectContaining({ + description: 'chat @cf/meta/llama-3.1-8b-instruct', + op: 'gen_ai.chat', + origin: 'auto.ai.cloudflare.workers_ai', + data: { + 'sentry.origin': 'auto.ai.cloudflare.workers_ai', + 'sentry.op': 'gen_ai.chat', + [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', + [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, + [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, + }, + }), + ], + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/stream'); + await runner.completed(); +}); + +// The Workers AI integration deliberately does not call `captureException` itself. +// When a `run` call fails, the error must bubble up out of the fetch handler and be +// reported by the top-level Cloudflare instrumentation instead — so it shows up in +// Sentry exactly once, with the `auto.http.cloudflare` mechanism. +it('bubbles up Workers AI errors to be captured by the top-level handler', async ({ signal }) => { + const runner = createRunner(__dirname) + // A failing run still produces a (sampled) transaction; we only care about the error event here. + .ignore('transaction') + .expect(envelope => { + const errorEvent = envelope[1]?.[0]?.[1] as any; + + expect(errorEvent).toEqual( + expect.objectContaining({ + level: 'error', + exception: { + values: [ + expect.objectContaining({ + type: 'Error', + value: 'Model not found', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'auto.http.cloudflare', handled: false }, + }), + ], + }, + request: expect.objectContaining({ + method: 'GET', + url: expect.any(String), + }), + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/error', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc new file mode 100644 index 000000000000..2cdb43e06500 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "workers-ai-worker", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_als"], +} From bae674077748e113265dae72c18586da52a0164e Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 20 Jul 2026 10:25:37 +0200 Subject: [PATCH 7/9] fixup! test(cloudflare): Add workers-ai tests --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index a7f84c55b573..476a39a18815 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -480,7 +480,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '470 KiB', + limit: '475 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { From cb6a88b3fd0c5f1fd3ba55b6641fdf0c25c2acd8 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 20 Jul 2026 11:08:58 +0200 Subject: [PATCH 8/9] fixup! fixup! test(cloudflare): Add workers-ai tests --- packages/cloudflare/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 7a2cce03ee7b..20a537c5b307 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -79,7 +79,6 @@ export { instrumentOpenAiClient, instrumentGoogleGenAIClient, instrumentAnthropicAiClient, - instrumentWorkersAiClient, eventFiltersIntegration, linkedErrorsIntegration, requestDataIntegration, From b1b50ff1ba24a836efe657d0edbcf50e533fae8f Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 20 Jul 2026 11:20:27 +0200 Subject: [PATCH 9/9] fixup! fixup! fixup! test(cloudflare): Add workers-ai tests --- dev-packages/cloudflare-integration-tests/package.json | 1 + .../suites/tracing/workers-ai/index.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index c360d7449ed6..7f3362d1f612 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -23,6 +23,7 @@ "@prisma/adapter-d1": "6.15.0", "@prisma/client": "6.15.0", "@sentry/cloudflare": "10.66.0", + "@sentry/core": "10.66.0", "@sentry/hono": "10.66.0", "hono": "^4.12.25", "openai": "5.18.1" diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts index 177ab580d5e1..9064ea1cbd4e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -1,11 +1,12 @@ import * as Sentry from '@sentry/cloudflare'; +import { instrumentWorkersAiClient } from '@sentry/core'; import { MockAi } from './mocks'; interface Env { SENTRY_DSN: string; } -const ai = Sentry.instrumentWorkersAiClient(new MockAi()); +const ai = instrumentWorkersAiClient(new MockAi()); export default Sentry.withSentry( (env: Env) => ({