diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts index 2461cc5fd5aa..96f3bb4b2561 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts @@ -2,7 +2,6 @@ // the worker entry named in wrangler's `main`) and imports its default export as // the options callback for every wrapper it injects. export default (env: Env) => ({ - traceLifecycle: 'static' as const, dsn: env.E2E_TEST_DSN, environment: 'qa', tunnel: 'http://localhost:3031/', diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts index 4a8d4d8f3bce..8f1572841f42 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts @@ -1,27 +1,30 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry/core'; import { callRpc } from './agent-socket'; // The worker entry (`src/index.ts`) contains no Sentry calls at all — every // wrapper below was injected by the Vite auto-instrument plugin at build time. -// Any transaction arriving here therefore proves the injection happened. +// Any span arriving here therefore proves the injection happened. + +function isPlainDoRequestSegment(span: SerializedStreamedSpan): boolean { + return getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === '/plain-do'; +} test('wraps the default export with withSentry (options from instrument.server.ts)', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('cloudflare-autoinstrument', event => { - return event.contexts?.trace?.op === 'http.server' && (event.request?.url ?? '').includes('/plain-do'); - }); + const spanPromise = waitForStreamedSpan('cloudflare-autoinstrument', isPlainDoRequestSegment); const res = await fetch(`${baseURL}/plain-do`); expect(res.status).toBe(200); await expect(res.json()).resolves.toEqual({ durableObject: true }); - const transaction = await transactionPromise; + const span = await spanPromise; - expect(transaction.contexts?.trace?.origin).toBe('auto.http.cloudflare'); + expect(span.attributes['sentry.origin']?.value).toBe('auto.http.cloudflare'); // `environment: 'qa'` is only set in `instrument.server.ts`, so seeing it here // proves the plugin sourced its options callback from that file rather than // falling back to reading configuration off `env`. - expect(transaction.environment).toBe('qa'); + expect(span.attributes['sentry.environment']?.value).toBe('qa'); }); // Each of these three classes is registered in wrangler.jsonc exactly like the @@ -49,10 +52,18 @@ for (const { title, binding, agentClass } of [ test(`applies agent instrumentation to ${title}`, async ({ baseURL }) => { const instance = `${binding}-instance`; - const transactionPromise = waitForTransaction('cloudflare-autoinstrument', event => { + const isGreetRpcSpan = (span: SerializedStreamedSpan): boolean => + getSpanOp(span) === 'rpc' && + span.name === 'greet' && + String(span.attributes['gen_ai.agent.name']?.value ?? '').includes(agentClass); + + // The rpc span is a child of the `webSocketMessage` segment span, which ends after it and is + // streamed in a later envelope, so collect until both have arrived. + const spansPromise = collectStreamedSpans('cloudflare-autoinstrument', spans => { + const rpcSpan = spans.find(isGreetRpcSpan); return ( - event.transaction === 'webSocketMessage' && - (event.spans ?? []).some(span => span.op === 'rpc' && span.description === 'greet') + !!rpcSpan && + spans.some(span => span.is_segment && span.name === 'webSocketMessage' && span.trace_id === rpcSpan.trace_id) ); }); @@ -61,38 +72,35 @@ for (const { title, binding, agentClass } of [ const reply = await callRpc(baseURL!, { binding, instance, method: 'greet', args: ['World'] }); expect(reply).toBe(`Hello, World! (from ${agentClass})`); - const transaction = await transactionPromise; - const rpcSpan = (transaction.spans ?? []).find(span => span.op === 'rpc' && span.description === 'greet'); - - expect(rpcSpan).toEqual( - expect.objectContaining({ - op: 'rpc', - description: 'greet', - origin: 'auto.faas.cloudflare.agents', - data: expect.objectContaining({ - // Read back off the instance at runtime (`_ParentClass.name`), so it - // confirms the wrapper landed on the user's real class. Matched loosely - // because the transform renames the class it wraps to - // `__SENTRY_ORIGINAL___` and the bundler infers that name. - 'gen_ai.agent.name': expect.stringContaining(agentClass), - }), - }), - ); + const spans = await spansPromise; + const rpcSpan = spans.find(isGreetRpcSpan)!; + + expect(rpcSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: 'auto.faas.cloudflare.agents', type: 'string' }, + // Read back off the instance at runtime (`_ParentClass.name`), so it + // confirms the wrapper landed on the user's real class. Matched loosely + // because the transform renames the class it wraps to + // `__SENTRY_ORIGINAL___` and the bundler infers that name. + 'gen_ai.agent.name': { value: expect.stringContaining(agentClass), type: 'string' }, + }); }); } test('applies plain Durable Object instrumentation to a non-Agent class', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('cloudflare-autoinstrument', event => { - return event.contexts?.trace?.op === 'http.server' && (event.request?.url ?? '').includes('/plain-do'); - }); + const spansPromise = collectStreamedSpans('cloudflare-autoinstrument', spans => spans.some(isPlainDoRequestSegment)); const res = await fetch(`${baseURL}/plain-do`); expect(res.status).toBe(200); - const transaction = await transactionPromise; + const spans = await spansPromise; + const segment = spans.find(isPlainDoRequestSegment)!; // A plain Durable Object must NOT pick up agent instrumentation: detection has // to discriminate, not blanket-upgrade every `durable_objects` binding. - const agentSpans = (transaction.spans ?? []).filter(span => span.origin === 'auto.faas.cloudflare.agents'); + const agentSpans = spans.filter( + span => + span.trace_id === segment.trace_id && span.attributes['sentry.origin']?.value === 'auto.faas.cloudflare.agents', + ); expect(agentSpans).toEqual([]); });