From c567a7c0a97c37f10e48be816b2b3d8b118bb490 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 21 Oct 2026 16:00:02 +0200 Subject: [PATCH 1/2] test(cloudflare): Add Workers AI tracing tests for Clairic Co-Authored-By: Claude Opus 5.5 --- .../src/index.ts | 11 +++++++++ .../tests/send-to-sentry.test.ts | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts index 990859d99f7e..641a211dd92a 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts @@ -46,6 +46,17 @@ export default { }); return Response.json({ traceId: spanContext?.traceId }); } + case '/test-workers-ai-clairic': { + await env.AI.run('@cf/cloudflare/clairic', { + model: 'clairic', + state: 'Invoice INV-2291 from Northwind Supply. Total due: $1,080.00.', + fields: { + invoice_number: { type: 'text' }, + total: { type: 'number', decimals: 2, unit: 'USD', instructions: 'Amount due' }, + }, + }); + return Response.json({ traceId: spanContext?.traceId }); + } case '/test-span': return Response.json({ spanId: spanContext?.spanId, traceId: spanContext?.traceId }); case '/test-workflow-sleep': { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts index fd79fa5b7743..59a3bd0ef6a6 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts @@ -135,3 +135,26 @@ test('Sends a Workers AI gen_ai.decide span for a Clef call to Sentry', async () 'gen_ai.output.messages': expect.stringContaining('"urgent"'), }); }); + +test('Sends a Workers AI gen_ai.extract span for a Clairic call to Sentry', async () => { + const { traceId }: { traceId: string } = JSON.parse( + await fetchFromWorker(`${workerUrl}/test-workers-ai-clairic`, 200), + ); + + console.log(`Polling for gen_ai.extract span: sentry trace view ${traceTarget(traceId)}`); + + let spanId: string | undefined; + await expect + .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.extract')?.event_id), EVENT_POLLING_OPTIONS) + .toBeDefined(); + + await expect + .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) + .toMatchObject({ + 'gen_ai.operation.name': 'extract', + 'gen_ai.request.model': '@cf/cloudflare/clairic', + 'gen_ai.response.model': expect.stringContaining('clairic'), + 'gen_ai.input.messages': expect.stringContaining('Northwind Supply'), + 'gen_ai.output.messages': expect.stringContaining('INV-2291'), + }); +}); From 61d5632c215ca2a3b4eb14e87017e2e420485a51 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 22 Oct 2026 09:46:08 +0200 Subject: [PATCH 2/2] Use conventions constants in the Workers AI E2E tests --- .../package.json | 1 + .../tests/send-to-sentry.test.ts | 53 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json index be9b44cc4422..8650666b7870 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json @@ -19,6 +19,7 @@ "@cloudflare/workers-types": "^5.20260727.1", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", + "@sentry/conventions": "0.27.0", "@types/node": "^26.1.2", "sentry": "~0.44.1", "typescript": "~6.0.3", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts index 59a3bd0ef6a6..ea4e136d5536 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts @@ -1,5 +1,13 @@ import { randomBytes } from 'node:crypto'; import { expect, test } from '@playwright/test'; +import { + GEN_AI_INPUT_MESSAGES, + GEN_AI_OPERATION_NAME, + GEN_AI_OUTPUT_MESSAGES, + GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_MODEL, +} from '@sentry/conventions/attributes'; +import { GEN_AI_CHAT, GEN_AI_DECIDE, HTTP_SERVER } from '@sentry/conventions/op'; import { EVENT_POLLING_OPTIONS, fetchSpanAttributes, @@ -37,7 +45,7 @@ test('Sends an unhandled exception and its request span to Sentry', async () => console.log(`Polling for unhandled error: sentry trace view ${traceTarget(traceId)}`); await expect.poll(() => findErrorInTrace(traceId), EVENT_POLLING_OPTIONS).toBeDefined(); - await expect.poll(() => findSpanInTrace(traceId, 'http.server'), EVENT_POLLING_OPTIONS).toBeDefined(); + await expect.poll(() => findSpanInTrace(traceId, HTTP_SERVER), EVENT_POLLING_OPTIONS).toBeDefined(); }); test('Sends a request span to Sentry', async () => { @@ -48,7 +56,7 @@ test('Sends a request span to Sentry', async () => { console.log(`Polling for request spanId ${spanId}: sentry trace view ${traceTarget(traceId)}`); await expect - .poll(() => findSpanInTrace(traceId, 'http.server'), EVENT_POLLING_OPTIONS) + .poll(() => findSpanInTrace(traceId, HTTP_SERVER), EVENT_POLLING_OPTIONS) .toMatchObject({ event_id: spanId }); }); @@ -82,15 +90,15 @@ test('Sends a Workers AI gen_ai span to Sentry', async () => { let spanId: string | undefined; await expect - .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.chat')?.event_id), EVENT_POLLING_OPTIONS) + .poll(() => (spanId = findSpanInTrace(traceId, GEN_AI_CHAT)?.event_id), EVENT_POLLING_OPTIONS) .toBeDefined(); // Sentry stores `gen_ai.response.text` as `gen_ai.output.messages`. await expect .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) .toMatchObject({ - 'gen_ai.input.messages': expect.stringContaining('Say hi'), - 'gen_ai.output.messages': expect.stringContaining('"role":"assistant"'), + [GEN_AI_INPUT_MESSAGES]: expect.stringContaining('Say hi'), + [GEN_AI_OUTPUT_MESSAGES]: expect.stringContaining('"role":"assistant"'), }); }); @@ -101,17 +109,17 @@ test('Sends a Workers AI gen_ai.decide span for a TypeSafe Jev call to Sentry', let spanId: string | undefined; await expect - .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.decide')?.event_id), EVENT_POLLING_OPTIONS) + .poll(() => (spanId = findSpanInTrace(traceId, GEN_AI_DECIDE)?.event_id), EVENT_POLLING_OPTIONS) .toBeDefined(); await expect .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) .toMatchObject({ - 'gen_ai.operation.name': 'decide', - 'gen_ai.request.model': 'typesafe/jev', - 'gen_ai.response.model': expect.stringMatching(/^jev-/), - 'gen_ai.input.messages': expect.stringContaining('My payouts have been failing'), - 'gen_ai.output.messages': expect.stringContaining('"is_urgent"'), + [GEN_AI_OPERATION_NAME]: 'decide', + [GEN_AI_REQUEST_MODEL]: 'typesafe/jev', + [GEN_AI_RESPONSE_MODEL]: expect.stringMatching(/^jev-/), + [GEN_AI_INPUT_MESSAGES]: expect.stringContaining('My payouts have been failing'), + [GEN_AI_OUTPUT_MESSAGES]: expect.stringContaining('"is_urgent"'), }); }); @@ -122,17 +130,17 @@ test('Sends a Workers AI gen_ai.decide span for a Clef call to Sentry', async () let spanId: string | undefined; await expect - .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.decide')?.event_id), EVENT_POLLING_OPTIONS) + .poll(() => (spanId = findSpanInTrace(traceId, GEN_AI_DECIDE)?.event_id), EVENT_POLLING_OPTIONS) .toBeDefined(); await expect .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) .toMatchObject({ - 'gen_ai.operation.name': 'decide', - 'gen_ai.request.model': '@cf/cloudflare/clef', - 'gen_ai.response.model': expect.stringContaining('clef'), - 'gen_ai.input.messages': expect.stringContaining('Checkout has been failing'), - 'gen_ai.output.messages': expect.stringContaining('"urgent"'), + [GEN_AI_OPERATION_NAME]: 'decide', + [GEN_AI_REQUEST_MODEL]: '@cf/cloudflare/clef', + [GEN_AI_RESPONSE_MODEL]: expect.stringContaining('clef'), + [GEN_AI_INPUT_MESSAGES]: expect.stringContaining('Checkout has been failing'), + [GEN_AI_OUTPUT_MESSAGES]: expect.stringContaining('"urgent"'), }); }); @@ -144,6 +152,7 @@ test('Sends a Workers AI gen_ai.extract span for a Clairic call to Sentry', asyn console.log(`Polling for gen_ai.extract span: sentry trace view ${traceTarget(traceId)}`); let spanId: string | undefined; + // TODO: Use `GEN_AI_EXTRACT` from `@sentry/conventions/op` once getsentry/sentry-conventions#728 is released await expect .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.extract')?.event_id), EVENT_POLLING_OPTIONS) .toBeDefined(); @@ -151,10 +160,10 @@ test('Sends a Workers AI gen_ai.extract span for a Clairic call to Sentry', asyn await expect .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) .toMatchObject({ - 'gen_ai.operation.name': 'extract', - 'gen_ai.request.model': '@cf/cloudflare/clairic', - 'gen_ai.response.model': expect.stringContaining('clairic'), - 'gen_ai.input.messages': expect.stringContaining('Northwind Supply'), - 'gen_ai.output.messages': expect.stringContaining('INV-2291'), + [GEN_AI_OPERATION_NAME]: 'extract', + [GEN_AI_REQUEST_MODEL]: '@cf/cloudflare/clairic', + [GEN_AI_RESPONSE_MODEL]: expect.stringContaining('clairic'), + [GEN_AI_INPUT_MESSAGES]: expect.stringContaining('Northwind Supply'), + [GEN_AI_OUTPUT_MESSAGES]: expect.stringContaining('INV-2291'), }); });