Skip to content

Commit ea5a462

Browse files
authored
test(cloudflare): Proof that AI providers work OOTB on Cloudflare (#22313)
The Cloudflare integration tests were already testing all AI integrations, but used mocks. I changed the following in all tests: - Removed the `mocks.ts` and changed it to the actual package - Instead the `.fetch` is being mocked - Aligned the assertions
1 parent 2cfeb04 commit ea5a462

17 files changed

Lines changed: 272 additions & 705 deletions

File tree

dev-packages/cloudflare-integration-tests/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@
1515
},
1616
"dependencies": {
1717
"ai": "^6.0.0",
18+
"@anthropic-ai/sdk": "0.63.0",
19+
"@google/genai": "^1.20.0",
20+
"@langchain/core": "^0.3.80",
1821
"@langchain/langgraph": "^1.0.1",
22+
"@langchain/openai": "^0.5.0",
1923
"@prisma/adapter-d1": "6.15.0",
2024
"@prisma/client": "6.15.0",
2125
"@sentry/cloudflare": "10.66.0",
2226
"@sentry/hono": "10.66.0",
23-
"hono": "^4.12.25"
27+
"hono": "^4.12.25",
28+
"openai": "5.18.1"
2429
},
2530
"devDependencies": {
2631
"@cloudflare/workers-types": "^4.20260426.0",

dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1+
import Anthropic from '@anthropic-ai/sdk';
12
import * as Sentry from '@sentry/cloudflare';
2-
import type { AnthropicAiClient } from '@sentry/core';
3-
import { MockAnthropic } from './mocks';
43

54
interface Env {
65
SENTRY_DSN: string;
76
}
87

9-
const mockClient = new MockAnthropic({
10-
apiKey: 'mock-api-key',
11-
});
8+
// Return a canned response so the `@anthropic-ai/sdk` runs on workerd without hitting the network.
9+
const mockFetch: typeof fetch = async () =>
10+
new Response(
11+
JSON.stringify({
12+
id: 'msg_mock123',
13+
type: 'message',
14+
role: 'assistant',
15+
model: 'claude-3-haiku-20240307',
16+
content: [{ type: 'text', text: 'Hello from Anthropic!' }],
17+
stop_reason: 'end_turn',
18+
stop_sequence: null,
19+
usage: { input_tokens: 10, output_tokens: 15 },
20+
}),
21+
{ status: 200, headers: { 'content-type': 'application/json' } },
22+
);
1223

13-
const client: AnthropicAiClient = Sentry.instrumentAnthropicAiClient(mockClient);
24+
const client = Sentry.instrumentAnthropicAiClient(new Anthropic({ apiKey: 'mock-api-key', fetch: mockFetch }));
1425

1526
export default Sentry.withSentry(
1627
(env: Env) => ({
@@ -20,7 +31,7 @@ export default Sentry.withSentry(
2031
}),
2132
{
2233
async fetch(_request, _env, _ctx) {
23-
const response = await client.messages?.create({
34+
const response = await client.messages.create({
2435
model: 'claude-3-haiku-20240307',
2536
messages: [{ role: 'user', content: 'What is the capital of France?' }],
2637
temperature: 0.7,

dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/mocks.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,57 @@
11
import { expect, it } from 'vitest';
22
import {
33
GEN_AI_OPERATION_NAME_ATTRIBUTE,
4+
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
45
GEN_AI_REQUEST_MODEL_ATTRIBUTE,
56
GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE,
67
GEN_AI_RESPONSE_ID_ATTRIBUTE,
78
GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
89
GEN_AI_SYSTEM_ATTRIBUTE,
910
GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
1011
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
12+
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
1113
} from '../../../../../packages/core/src/tracing/ai/gen-ai-attributes';
1214
import { createRunner } from '../../../runner';
1315

14-
// These tests are not exhaustive because the instrumentation is
15-
// already tested in the node integration tests and we merely
16-
// want to test that the instrumentation does not break in our
17-
// cloudflare SDK.
16+
// This test runs the `@anthropic-ai/sdk` on the Workers runtime (with a
17+
// canned fetch) to verify the instrumentation works end-to-end on Cloudflare,
18+
// not just against a hand-written mock client.
1819

19-
it('traces a basic message creation request', async ({ signal }) => {
20+
it('traces a basic message creation request with the anthropic SDK', async ({ signal }) => {
2021
const runner = createRunner(__dirname)
2122
.ignore('event')
2223
.expect(envelope => {
23-
// Transaction item (first item in envelope)
2424
const transactionEvent = envelope[1]?.[0]?.[1] as any;
2525
expect(transactionEvent.transaction).toBe('GET /');
2626

27-
// Span container item (second item in same envelope)
2827
const container = envelope[1]?.[1]?.[1] as any;
2928
expect(container).toBeDefined();
30-
3129
expect(container.items).toHaveLength(1);
32-
const [firstSpan] = container.items;
3330

34-
// [0] chat claude-3-haiku-20240307
35-
expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307');
36-
expect(firstSpan!.status).toBe('ok');
37-
expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' });
38-
expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' });
39-
expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.anthropic' });
40-
expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'anthropic' });
41-
expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({
42-
type: 'string',
43-
value: 'claude-3-haiku-20240307',
44-
});
45-
expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 });
46-
expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({
47-
type: 'string',
48-
value: 'claude-3-haiku-20240307',
31+
expect(container.items[0]).toEqual({
32+
trace_id: expect.any(String),
33+
span_id: expect.any(String),
34+
parent_span_id: expect.any(String),
35+
name: 'chat claude-3-haiku-20240307',
36+
start_timestamp: expect.any(Number),
37+
end_timestamp: expect.any(Number),
38+
status: 'ok',
39+
is_segment: false,
40+
attributes: {
41+
'sentry.origin': { value: 'auto.ai.anthropic', type: 'string' },
42+
'sentry.op': { value: 'gen_ai.chat', type: 'string' },
43+
[GEN_AI_SYSTEM_ATTRIBUTE]: { value: 'anthropic', type: 'string' },
44+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: { value: 'chat', type: 'string' },
45+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: { value: 'claude-3-haiku-20240307', type: 'string' },
46+
[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: { value: 0.7, type: 'double' },
47+
[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: { value: 100, type: 'integer' },
48+
[GEN_AI_RESPONSE_ID_ATTRIBUTE]: { value: 'msg_mock123', type: 'string' },
49+
[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: { value: 'claude-3-haiku-20240307', type: 'string' },
50+
[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: { value: 10, type: 'integer' },
51+
[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: { value: 15, type: 'integer' },
52+
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: { value: 25, type: 'integer' },
53+
},
4954
});
50-
expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'msg_mock123' });
51-
expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 });
52-
expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 });
5355
})
5456
.start(signal);
5557
await runner.makeRequest('get', '/');

dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/wrangler.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "worker-name",
2+
"name": "anthropic-ai-worker",
33
"compatibility_date": "2025-06-17",
44
"main": "index.ts",
55
"compatibility_flags": ["nodejs_als"],

dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
1+
import { GoogleGenAI } from '@google/genai';
12
import * as Sentry from '@sentry/cloudflare';
2-
import type { GoogleGenAIClient } from '@sentry/core';
3-
import { MockGoogleGenAI } from './mocks';
43

54
interface Env {
65
SENTRY_DSN: string;
76
}
87

9-
const mockClient = new MockGoogleGenAI({
10-
apiKey: 'mock-api-key',
11-
});
8+
// `@google/genai` has no per-client fetch option — it calls the global `fetch`
9+
// directly. Override it (gated to the Gemini host) so the SDK runs on
10+
// workerd without hitting the network, while Sentry envelope delivery still
11+
// uses the original fetch.
12+
const originalFetch = globalThis.fetch;
13+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
14+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
1215

13-
const client: GoogleGenAIClient = Sentry.instrumentGoogleGenAIClient(mockClient);
16+
if (!url.includes('generativelanguage.googleapis.com')) {
17+
return originalFetch(input, init);
18+
}
19+
20+
if (url.includes(':embedContent') || url.includes(':batchEmbedContents')) {
21+
return new Response(JSON.stringify({ embeddings: [{ values: [0.1, 0.2, 0.3, 0.4, 0.5] }] }), {
22+
status: 200,
23+
headers: { 'content-type': 'application/json' },
24+
});
25+
}
26+
27+
return new Response(
28+
JSON.stringify({
29+
candidates: [
30+
{
31+
content: { parts: [{ text: 'Hello from Google GenAI!' }], role: 'model' },
32+
finishReason: 'stop',
33+
index: 0,
34+
},
35+
],
36+
usageMetadata: { promptTokenCount: 8, candidatesTokenCount: 12, totalTokenCount: 20 },
37+
}),
38+
{ status: 200, headers: { 'content-type': 'application/json' } },
39+
);
40+
}) as typeof fetch;
41+
42+
const client = Sentry.instrumentGoogleGenAIClient(new GoogleGenAI({ apiKey: 'mock-api-key' }));
1443

1544
export default Sentry.withSentry(
1645
(env: Env) => ({
@@ -36,12 +65,10 @@ export default Sentry.withSentry(
3665
],
3766
});
3867

39-
const chatResponse = await chat.sendMessage({
40-
message: 'Tell me a joke',
41-
});
68+
await chat.sendMessage({ message: 'Tell me a joke' });
4269

4370
// Test 2: models.generateContent
44-
const modelResponse = await client.models.generateContent({
71+
await client.models.generateContent({
4572
model: 'gemini-1.5-flash',
4673
config: {
4774
temperature: 0.7,
@@ -57,12 +84,12 @@ export default Sentry.withSentry(
5784
});
5885

5986
// Test 3: models.embedContent
60-
const embedResponse = await client.models.embedContent({
87+
await client.models.embedContent({
6188
model: 'text-embedding-004',
6289
contents: 'Hello world',
6390
});
6491

65-
return new Response(JSON.stringify({ chatResponse, modelResponse, embedResponse }));
92+
return new Response(JSON.stringify({ success: true }));
6693
},
6794
},
6895
);

0 commit comments

Comments
 (0)