Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
streamGenAiSpans: true,
}),
{
async fetch(_request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
streamGenAiSpans: true,
}),
{
async fetch(_request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
streamGenAiSpans: true,
}),
{
async fetch(_request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export default Sentry.withSentry(
traceLifecycle: 'static',
tracesSampleRate: 1.0,
dataCollection: { genAI: { inputs: true, outputs: true } },
streamGenAiSpans: true,
}),
{
async fetch(_request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
streamGenAiSpans: true,
}),
{
async fetch(_request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,29 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => {
.ignore('event')
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as any;
expect(transactionEvent.transaction).toBe('GET /');

// 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(
const container = envelope[1]?.[1]?.[1] as any;
expect(container).toBeDefined();
expect(container.items).toHaveLength(1);

expect(container.items[0]).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]: 'chat',
[GEN_AI_REQUEST_MODEL]: '@cf/meta/llama-3.1-8b-instruct',
[GEN_AI_REQUEST_TEMPERATURE]: 0.7,
[GEN_AI_REQUEST_MAX_TOKENS]: 100,
[GEN_AI_USAGE_INPUT_TOKENS]: 12,
[GEN_AI_USAGE_OUTPUT_TOKENS]: 7,
[GEN_AI_USAGE_TOTAL_TOKENS]: 19,
},
}),
],
name: 'chat @cf/meta/llama-3.1-8b-instruct',
status: 'ok',
is_segment: false,
attributes: {
'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' },
'sentry.op': { value: 'gen_ai.chat', type: 'string' },
[GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' },
[GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' },
[GEN_AI_REQUEST_MODEL]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' },
[GEN_AI_REQUEST_TEMPERATURE]: { value: 0.7, type: 'double' },
[GEN_AI_REQUEST_MAX_TOKENS]: { value: 100, type: 'integer' },
[GEN_AI_USAGE_INPUT_TOKENS]: { value: 12, type: 'integer' },
[GEN_AI_USAGE_OUTPUT_TOKENS]: { value: 7, type: 'integer' },
[GEN_AI_USAGE_TOTAL_TOKENS]: { value: 19, type: 'integer' },
},
}),
);
})
Expand All @@ -69,38 +58,29 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) =
.ignore('event')
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as any;
expect(transactionEvent.transaction).toBe('GET /stream');

expect(transactionEvent).toEqual(
const container = envelope[1]?.[1]?.[1] as any;
expect(container).toBeDefined();
expect(container.items).toHaveLength(1);

expect(container.items[0]).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]: 'chat',
[GEN_AI_REQUEST_MODEL]: '@cf/meta/llama-3.1-8b-instruct',
[GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true,
[GEN_AI_RESPONSE_STREAMING]: true,
[GEN_AI_USAGE_INPUT_TOKENS]: 12,
[GEN_AI_USAGE_OUTPUT_TOKENS]: 7,
[GEN_AI_USAGE_TOTAL_TOKENS]: 19,
},
}),
],
name: 'chat @cf/meta/llama-3.1-8b-instruct',
status: 'ok',
is_segment: false,
attributes: {
'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' },
'sentry.op': { value: 'gen_ai.chat', type: 'string' },
[GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' },
Comment thread
sentry[bot] marked this conversation as resolved.
[GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' },
[GEN_AI_REQUEST_MODEL]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' },
[GEN_AI_REQUEST_STREAM_ATTRIBUTE]: { value: true, type: 'boolean' },
[GEN_AI_RESPONSE_STREAMING]: { value: true, type: 'boolean' },
[GEN_AI_USAGE_INPUT_TOKENS]: { value: 12, type: 'integer' },
[GEN_AI_USAGE_OUTPUT_TOKENS]: { value: 7, type: 'integer' },
[GEN_AI_USAGE_TOTAL_TOKENS]: { value: 19, type: 'integer' },
},
}),
);
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,54 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils';
import type { SerializedStreamedSpan } from '@sentry/core';

// Drives Workers AI through the real Cloudflare Agents SDK + Vercel AI SDK + `workers-ai-provider`
// stack (the OpenAI-compatible SSE shape, `choices[].delta.content`) from an Agent's `onRequest`.
// Asserts the streaming response text is captured on the gen_ai span — the regression seen in
// production where only input + usage survived. The model output is read from
// `gen_ai.output.messages`, so the streaming instrumentation must emit it alongside the
// deprecated `gen_ai.response.text`.
function assertGenAiStreamingSpan(spans: Array<Record<string, any>> | undefined): void {
const genAiSpan = (spans ?? []).find(span => span.op === 'gen_ai.chat');

expect(genAiSpan).toBeDefined();
expect(genAiSpan.origin).toBe('auto.ai.cloudflare.workers_ai');
expect(genAiSpan.data).toEqual(
expect.objectContaining({
'sentry.origin': 'auto.ai.cloudflare.workers_ai',
'gen_ai.operation.name': 'chat',
'gen_ai.request.model': '@cf/meta/llama-3.1-8b-instruct',
'gen_ai.response.streaming': true,
'gen_ai.response.text': 'The capital of France is Paris.',
'gen_ai.output.messages': JSON.stringify([
{ role: 'assistant', parts: [{ type: 'text', content: 'The capital of France is Paris.' }] },
]),
'gen_ai.usage.input_tokens': 15,
'gen_ai.usage.output_tokens': 8,
'gen_ai.usage.total_tokens': 23,
}),
function assertGenAiStreamingSpan(span: SerializedStreamedSpan): void {
expect(getSpanOp(span)).toBe('gen_ai.chat');
expect(span.attributes['sentry.origin']?.value).toBe('auto.ai.cloudflare.workers_ai');
expect(span.attributes['gen_ai.operation.name']?.value).toBe('chat');
expect(span.attributes['gen_ai.request.model']?.value).toBe('@cf/meta/llama-3.1-8b-instruct');
expect(span.attributes['gen_ai.response.streaming']?.value).toBe(true);
expect(span.attributes['gen_ai.response.text']?.value).toBe('The capital of France is Paris.');
expect(span.attributes['gen_ai.output.messages']?.value).toBe(
JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'The capital of France is Paris.' }] }]),
);
expect(span.attributes['gen_ai.usage.input_tokens']?.value).toBe(15);
expect(span.attributes['gen_ai.usage.output_tokens']?.value).toBe(8);
expect(span.attributes['gen_ai.usage.total_tokens']?.value).toBe(23);
}

test('captures Workers AI streaming output when driven via an Agent', async ({ request, baseURL }) => {
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
return (
transactionEvent.transaction === 'GET /agents/my-agent/test' &&
(transactionEvent.spans ?? []).some(span => span.op === 'gen_ai.chat')
);
});
const spanPromise = waitForStreamedSpan('cloudflare-agent', span => getSpanOp(span) === 'gen_ai.chat');
const transactionPromise = waitForTransaction(
'cloudflare-agent',
transactionEvent => transactionEvent.transaction === 'GET /agents/my-agent/test',
);

const response = await request.get(`${baseURL}/agents/my-agent/test`);
expect(response.ok()).toBe(true);

const transaction = await transactionPromise;
assertGenAiStreamingSpan(transaction.spans);
const [span, transaction] = await Promise.all([spanPromise, transactionPromise]);
expect(span.trace_id).toBe(transaction.contexts?.trace?.trace_id);
assertGenAiStreamingSpan(span);
});

test('captures Workers AI streaming output when driven via an AIChatAgent', async ({ request, baseURL }) => {
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
return (
transactionEvent.transaction === 'GET /agents/my-chat-agent/test' &&
(transactionEvent.spans ?? []).some(span => span.op === 'gen_ai.chat')
);
});
const spanPromise = waitForStreamedSpan('cloudflare-agent', span => getSpanOp(span) === 'gen_ai.chat');
const transactionPromise = waitForTransaction(
'cloudflare-agent',
transactionEvent => transactionEvent.transaction === 'GET /agents/my-chat-agent/test',
);

const response = await request.get(`${baseURL}/agents/my-chat-agent/test`);
expect(response.ok()).toBe(true);

const transaction = await transactionPromise;
assertGenAiStreamingSpan(transaction.spans);
const [span, transaction] = await Promise.all([spanPromise, transactionPromise]);
expect(span.trace_id).toBe(transaction.contexts?.trace?.trace_id);
assertGenAiStreamingSpan(span);
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils';
import { sendChatMessage } from './agent-socket';

// In the Agents model one agent instance is one conversation, so the instance name is the
// conversation id the SDK correlates the turn's gen_ai spans with.
const CONVERSATION_ID = 'chat-conv-instance';

test('stamps the conversation id on gen_ai spans created inside a chat turn', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
return (
transactionEvent.transaction === 'webSocketMessage' &&
(transactionEvent.spans ?? []).some(span => span.op === 'gen_ai.chat')
);
});
const spanPromise = waitForStreamedSpan('cloudflare-agent', span => getSpanOp(span) === 'gen_ai.chat');
const transactionPromise = waitForTransaction(
'cloudflare-agent',
transactionEvent => transactionEvent.transaction === 'webSocketMessage',
);

await sendChatMessage(baseURL!, {
binding: 'my-chat-agent',
instance: CONVERSATION_ID,
prompt: 'What is the capital of France?',
});

const transaction = await transactionPromise;

const genAiSpan = (transaction.spans ?? []).find(span => span.op === 'gen_ai.chat');
expect(genAiSpan).toBeDefined();
expect(genAiSpan?.data).toEqual(
expect.objectContaining({
'gen_ai.conversation.id': CONVERSATION_ID,
}),
);
const [genAiSpan, transaction] = await Promise.all([spanPromise, transactionPromise]);
expect(genAiSpan.trace_id).toBe(transaction.contexts?.trace?.trace_id);
expect(genAiSpan.attributes['gen_ai.conversation.id']?.value).toBe(CONVERSATION_ID);
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ const sentryOptions = (env: Env) => ({
tracesSampleRate: 1,
enableRpcTracePropagation: true,
durableObjectStorageSpanAllowlist: ['cf_user_key'],
// Keep gen_ai spans embedded in the transaction (instead of streamed as a separate envelope
Comment thread
cursor[bot] marked this conversation as resolved.
// container) so they can be asserted on `transaction.spans`.
streamGenAiSpans: false,
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default Sentry.withSentry(
dsn: env.E2E_TEST_DSN,
environment: 'qa',
tunnel: 'http://localhost:3031/',
streamGenAiSpans: false,
Comment thread
cursor[bot] marked this conversation as resolved.
tracesSampleRate: 1.0,
integrations: [Sentry.vercelAIIntegration()],
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForRequest } from '@sentry-internal/test-utils';

test('does not capture Vercel AI v7 spans without nodejs_compat', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('cloudflare-vercelai-v7-als', txn => {
return txn.transaction === 'GET /generate';
// The transaction envelope also carries any extracted gen_ai spans as a span v2 container item,
// so we wait for the whole envelope and assert neither place contains AI spans.
const envelopePromise = waitForRequest('cloudflare-vercelai-v7-als', ({ envelope }) => {
const transactionItem = envelope[1].find(([header]) => header.type === 'transaction');
return (transactionItem?.[1] as any)?.transaction === 'GET /generate';
});

const response = await fetch(`${baseURL}/generate`);
expect(response.status).toBe(200);

const transaction = await transactionPromise;
const { envelope } = await envelopePromise;

const transaction = envelope[1].find(([header]) => header.type === 'transaction')?.[1] as any;
expect(transaction.transaction).toBe('GET /generate');
expect(transaction.contexts?.trace?.op).toBe('http.server');

// v7 uses diagnostics_channel which is not available with nodejs_als,
// so no AI spans should be present.
const aiSpans = (transaction.spans || []).filter(
// v7 uses diagnostics_channel which is not available with nodejs_als, so no AI spans should be
// present — neither embedded in the transaction nor streamed as a span v2 container item.
const embeddedAiSpans = (transaction.spans || []).filter(
(span: any) => span.op?.startsWith('gen_ai.') || span.description?.includes('generateText'),
);
expect(aiSpans).toHaveLength(0);
expect(embeddedAiSpans).toHaveLength(0);

const streamedGenAiSpans = envelope[1]
.filter(([header]) => header.type === 'span')
.flatMap(([, payload]) => (payload as any).items ?? [])
.filter((span: any) => getSpanOp(span)?.startsWith('gen_ai.'));
expect(streamedGenAiSpans).toHaveLength(0);
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export default Sentry.withSentry(
environment: 'qa',
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1.0,
streamGenAiSpans: false,
integrations: [Sentry.vercelAIIntegration()],
}),
{
Expand Down
Loading
Loading