From 6745f12f670d3879f1cd770f9d7bf879452f0110 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 7 Sep 2026 18:30:52 +0200 Subject: [PATCH] test(cloudflare): Port the vite-autoinstrument suites to span streaming Removes the `traceLifecycle: 'static'` pin from the seventeen `suites/vite-autoinstrument` suites, and rewrites the assertions from transaction envelopes to span v2. Under streaming a whole request chain is one trace, so a suite reads the trace rather than one envelope per hop. Every suite now uses `collectStreamedSpans`, because each hop, a Durable Object, a `WorkerEntrypoint` or a Workflow step, runs in its own isolate and streams its own envelope. The proof of instrumentation is unchanged, only its shape is. A Durable Object still shows the `durable_object_storage_get` and `durable_object_storage_put` pair, but as children of its own segment span rather than as `spans` of its transaction. The chain is now asserted directly: a hop's segment span carries the previous hop's `span_id` as its `parent_span_id`. Every route in these suites is a raw URL, so the streamed segment name keeps the method only. `GET /greet` becomes `GET`, and the hop is identified by its `url.path` attribute. That matters for the entrypoint and combination suites, where the segment name used to tell the hops apart. The per-suite `expectDurableObjectTransaction`, `expectMainWorkerTransaction` and `expectPlainTransaction` helpers are gone. The assertions are inlined, and the suites that hit two endpoints loop over the two paths instead. Co-Authored-By: Claude Opus 5 (1M context) --- .../instrument.server.ts | 1 - .../combination-entrypoint-do-chained/test.ts | 81 +++++++------- .../index.ts | 2 +- .../instrument.server.ts | 1 - .../test.ts | 97 +++++++++-------- .../instrument.server.ts | 1 - .../combination-entrypoint-do/test.ts | 100 ++++++++++-------- .../default-export/instrument.server.ts | 1 - .../default-export/test.ts | 17 +-- .../durableobject-manual-wrap/index.ts | 2 +- .../instrument.server.ts | 1 - .../durableobject-manual-wrap/test.ts | 80 ++++++-------- .../durableobject-mixed/index.ts | 2 +- .../durableobject-mixed/instrument.server.ts | 1 - .../durableobject-mixed/test.ts | 91 +++++++--------- .../instrument.server.ts | 1 - .../durableobject-multiple/test.ts | 88 ++++++--------- .../counter.ts | 2 +- .../instrument.server.ts | 1 - .../test.ts | 78 ++++++-------- .../durableobject-rpc/instrument.server.ts | 1 - .../durableobject-rpc/test.ts | 31 +++--- .../instrument.server.ts | 1 - .../durableobject-specifier-alias/test.ts | 76 ++++++------- .../instrument.server.ts | 1 - .../durableobject-specifier/test.ts | 76 ++++++------- .../index.ts | 2 +- .../instrument.server.ts | 1 - .../test.ts | 97 +++++++---------- .../instrument.server.ts | 1 - .../durableobject-workflow-specifier/test.ts | 97 +++++++---------- .../instrument.server.ts | 1 - .../durableobject-workflow/test.ts | 97 +++++++---------- .../durableobject/instrument.server.ts | 1 - .../vite-autoinstrument/durableobject/test.ts | 81 ++++++-------- .../workerentrypoint/instrument.server.ts | 1 - .../workerentrypoint/test.ts | 47 ++++---- .../workflow/instrument.server.ts | 1 - .../vite-autoinstrument/workflow/test.ts | 40 ++++--- 39 files changed, 564 insertions(+), 737 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/test.ts index 42e7a536a9a9..78d8445fbafd 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/test.ts @@ -1,50 +1,49 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// The Durable Object, reached from inside the entrypoint, emits an `http.server` -// transaction whose only children are the two -// `auto.db.cloudflare.durable_object` storage spans (`get` + `put`) — present -// only when the class was auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -function expectPlainTransaction(name: string) { - return (transactionEvent: TransactionEvent): void => { - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - expect(transactionEvent.transaction).toBe(name); - expect(transactionEvent.spans).toHaveLength(0); - }; -} +import { getSpanOp } from '../../../spanUtils'; // A single request fans out through the whole auto-wrapped chain: default // handler (`/chain`) → self-bound `CounterEntrypoint` (`/work`) → `Counter` -// Durable Object. All three transactions arrive only if the build-time transform -// wrapped the default export, the entrypoint, and the DO — and it proves a DO -// invoked from *within* an auto-instrumented entrypoint is itself instrumented. +// Durable Object. All three segment spans arrive only if the build-time +// transform wrapped the default export, the entrypoint, and the DO — and it +// proves a DO invoked from *within* an auto-instrumented entrypoint is itself +// instrumented. +// +// Every route here is a raw URL, so the streamed segment names keep the method +// only and each hop is identified by its `url.path` attribute. it('auto-instruments a Durable Object invoked from within a WorkerEntrypoint', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectPlainTransaction('GET /chain')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectPlainTransaction('GET /work')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // Each hop streams from its own isolate, so the three segment spans of the trace arrive in + // separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 3, + ); await runner.makeRequest('get', '/chain'); - await runner.completed(); + + const spans = await spansPromise; + const chainSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/chain'); + const entrypointSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/work'); + const durableObjectSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/increment'); + + expect(getSpanOp(chainSpan!)).toBe('http.server'); + expect(chainSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(entrypointSpan!)).toBe('http.server'); + expect(entrypointSpan?.parent_span_id).toBe(chainSpan?.span_id); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.parent_span_id).toBe(entrypointSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/index.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/index.ts index 3e2e640e26ef..075aa006aa21 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/index.ts @@ -32,7 +32,7 @@ class CounterImpl extends DurableObject { // untouched (no double-wrap) while still auto-wrapping the plain entrypoint and // default export in the same file. export const Counter = Sentry.instrumentDurableObjectWithSentry( - (env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }), + (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }), CounterImpl, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/test.ts index 70879a90ca71..1630042291f6 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/test.ts @@ -1,56 +1,69 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A Durable Object invoked via `fetch` emits an `http.server` transaction whose -// only children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`). Here they come from the manual wrap — the assertion also -// proves the transform did NOT double-wrap (a double-wrap would nest proxies or -// break the build). -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -function expectPlainTransaction(name: string) { - return (transactionEvent: TransactionEvent): void => { - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - expect(transactionEvent.transaction).toBe(name); - expect(transactionEvent.spans).toHaveLength(0); - }; -} +import { getSpanOp } from '../../../spanUtils'; // The Durable Object is manually wrapped with // `Sentry.instrumentDurableObjectWithSentry`; the `GreeterEntrypoint` and the // default export are plain. The transform must skip the manual DO (no // double-wrap) yet still auto-wrap the entrypoint and default handler — so the -// manual DO transaction (with storage spans) and both auto-wrapped transactions -// all arrive exactly once. +// manual DO span (with storage children) and both auto-wrapped spans all arrive +// exactly once. +// +// Every route here is a raw URL, so the streamed segment names keep the method +// only and each hop is identified by its `url.path` attribute. it('leaves a manually wrapped Durable Object untouched while auto-wrapping a sibling WorkerEntrypoint', async ({ signal, }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectPlainTransaction('GET /call-entrypoint')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectPlainTransaction('GET /greet')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectPlainTransaction('GET /increment')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // Each hop streams from its own isolate, so the segment spans of a trace arrive in separate + // envelopes. + const entrypointSpansPromise = runner.collectStreamedSpans(spansOfTrace => { + const paths = spansOfTrace.filter(span => span.is_segment).map(span => span.attributes['url.path']?.value); + return paths.includes('/call-entrypoint') && paths.includes('/greet'); + }); await runner.makeRequest('get', '/call-entrypoint'); + + const entrypointTraceSpans = await entrypointSpansPromise; + const callEntrypointSpan = entrypointTraceSpans.find( + span => span.is_segment && span.attributes['url.path']?.value === '/call-entrypoint', + ); + const greetSpan = entrypointTraceSpans.find( + span => span.is_segment && span.attributes['url.path']?.value === '/greet', + ); + + expect(getSpanOp(callEntrypointSpan!)).toBe('http.server'); + expect(callEntrypointSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(greetSpan!)).toBe('http.server'); + expect(greetSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(greetSpan?.parent_span_id).toBe(callEntrypointSpan?.span_id); + + const durableObjectSpansPromise = runner.collectStreamedSpans( + spansOfTrace => + spansOfTrace.filter(span => span.is_segment).length === 2 && + spansOfTrace.some(span => span.attributes['url.path']?.value === '/increment'), + ); + await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const durableObjectTraceSpans = await durableObjectSpansPromise; + const workerSpan = durableObjectTraceSpans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = durableObjectTraceSpans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The storage pair comes from the manual wrap here. Exactly two of them also rules out a + // double-wrap, which would nest proxies or break the build. + expect( + durableObjectTraceSpans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/test.ts index e50bceea5be2..cf4768c16c01 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/test.ts @@ -1,55 +1,67 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A Durable Object invoked via `fetch` emits an `http.server` transaction whose -// only children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class was auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// A plain `http.server` transaction with no child spans, identified by its -// transaction name. Used for the two main-worker entries and the entrypoint — -// asserting the name keeps each expectation disjoint under unordered matching. -function expectPlainTransaction(name: string) { - return (transactionEvent: TransactionEvent): void => { - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - expect(transactionEvent.transaction).toBe(name); - expect(transactionEvent.spans).toHaveLength(0); - }; -} +import { getSpanOp } from '../../../spanUtils'; // A single worker exports a plain `WorkerEntrypoint`, a plain `DurableObject`, // and a plain default handler. The runner builds it with the Sentry Vite plugin -// (auto-instrumentation on) and serves the output — so every transaction below -// only arrives if the build-time transform wrapped all three: `withSentry` for -// the default export, the self-bound `GreeterEntrypoint`, and `Counter` via +// (auto-instrumentation on) and serves the output — so every span below only +// arrives if the build-time transform wrapped all three: `withSentry` for the +// default export, the self-bound `GreeterEntrypoint`, and `Counter` via // `instrumentDurableObjectWithSentry`. +// +// Every route here is a raw URL, so the streamed segment names keep the method +// only and each hop is identified by its `url.path` attribute. it('auto-instruments a WorkerEntrypoint and a Durable Object exported from the same worker', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectPlainTransaction('GET /call-entrypoint')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectPlainTransaction('GET /greet')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectPlainTransaction('GET /increment')(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // Each hop streams from its own isolate, so the segment spans of a trace arrive in separate + // envelopes. + const entrypointSpansPromise = runner.collectStreamedSpans(spansOfTrace => { + const paths = spansOfTrace.filter(span => span.is_segment).map(span => span.attributes['url.path']?.value); + return paths.includes('/call-entrypoint') && paths.includes('/greet'); + }); await runner.makeRequest('get', '/call-entrypoint'); + + const entrypointTraceSpans = await entrypointSpansPromise; + const callEntrypointSpan = entrypointTraceSpans.find( + span => span.is_segment && span.attributes['url.path']?.value === '/call-entrypoint', + ); + const greetSpan = entrypointTraceSpans.find( + span => span.is_segment && span.attributes['url.path']?.value === '/greet', + ); + + expect(getSpanOp(callEntrypointSpan!)).toBe('http.server'); + expect(callEntrypointSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(greetSpan!)).toBe('http.server'); + expect(greetSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(greetSpan?.parent_span_id).toBe(callEntrypointSpan?.span_id); + + const durableObjectSpansPromise = runner.collectStreamedSpans( + spansOfTrace => + spansOfTrace.filter(span => span.is_segment).length === 2 && + spansOfTrace.some(span => span.attributes['url.path']?.value === '/increment'), + ); + await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const durableObjectTraceSpans = await durableObjectSpansPromise; + const workerSpan = durableObjectTraceSpans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = durableObjectTraceSpans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + durableObjectTraceSpans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/test.ts index 00ebb3b19fd9..4bd63673a387 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/test.ts @@ -1,18 +1,21 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; +import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; // The worker entry is a plain, unwrapped `export default {...}`. The runner // detects `vite.config.mts`, runs `vite build`, and serves the generated output -// — so this transaction only arrives if the build-time transform wrapped the -// default export with `withSentry`. +// — so this span only arrives if the build-time transform wrapped the default +// export with `withSentry`. it('auto-instruments a plain default-export handler', async ({ signal }) => { const runner = createRunner(__dirname) .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - expect(transactionEvent.transaction).toBe('GET /hello'); - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); + const segmentSpan = getSpansFromEnvelope(envelope).find(span => span.is_segment); + + // `/hello` is a raw URL, so the streamed segment name keeps the method only. + expect(segmentSpan?.name).toBe('GET'); + expect(segmentSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/hello' }); + expect(getSpanOp(segmentSpan!)).toBe('http.server'); + expect(segmentSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); }) .start(signal); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/index.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/index.ts index d51f25fcdeb6..15ab3a07d5c3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/index.ts @@ -19,7 +19,7 @@ class CounterImpl extends DurableObject { // leave it untouched — no second wrap — while still wrapping the plain default // export below. export const Counter = Sentry.instrumentDurableObjectWithSentry( - (env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }), + (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }), CounterImpl, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/test.ts index bfefd563f474..e538a69912a9 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/test.ts @@ -1,59 +1,43 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class is instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transaction. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // The Durable Object is already wrapped manually with // `Sentry.instrumentDurableObjectWithSentry`. The transform must recognize the // existing wrap and NOT wrap it again (a double-wrap would either break the // build or nest proxies), while still auto-wrapping the plain default export. -// We therefore expect exactly one storage-bearing DO transaction (from the -// manual wrap) and one child-less main-worker transaction (from the auto wrap). +// We therefore expect exactly one storage-bearing DO segment span (from the +// manual wrap) and one child-less main-worker segment span (from the auto wrap). it('leaves a manually wrapped Durable Object untouched and still wraps the default export', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. Exactly two of them also rules out a double-wrap. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/index.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/index.ts index 580a58ca73c5..a936b747202d 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/index.ts @@ -19,7 +19,7 @@ class ManualImpl extends DurableObject { // Manually wrapped — the transform must leave this alone. export const Manual = Sentry.instrumentDurableObjectWithSentry( - (env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }), + (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }), ManualImpl, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/test.ts index 6cb366444117..972039a27bda 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/test.ts @@ -1,64 +1,45 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class is instrumented (whether by the -// manual wrap or the build-time auto-wrap). -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transactions. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // One DO (`Manual`) is wrapped by hand, the other (`Auto`) is a plain inline // export. Both are bound in wrangler. The transform must skip the manual one and -// auto-wrap only `Auto` — so both endpoints report a storage-bearing DO -// transaction (one from the manual wrap, one from the auto wrap) without +// auto-wrap only `Auto` — so both endpoints report a storage-bearing DO segment +// span (one from the manual wrap, one from the auto wrap) without // double-instrumenting `Manual`. it('wraps only the unwrapped Durable Object when a sibling is manually wrapped', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - // One storage-bearing DO transaction from the manual wrap, one from the auto wrap. - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - // One child-less main worker transaction per request. - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // Each request runs in its own trace, and inside a trace the worker and the Durable Object stream + // from separate isolates. One collector per request therefore waits for that trace's two segment + // spans. + for (const path of ['/manual', '/auto']) { + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); + + await runner.makeRequest('get', path); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['url.path']).toEqual({ type: 'string', value: path }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); - await runner.makeRequest('get', '/manual'); - await runner.makeRequest('get', '/auto'); - await runner.completed(); + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. Exactly two of them also rules out a double-wrap of `Manual`. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); + } }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/test.ts index 6ecb348b4382..eaa634bf2073 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/test.ts @@ -1,61 +1,43 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class was actually auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transactions. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // Two Durable Object classes are bound in wrangler. Hitting both must produce a -// storage-bearing DO transaction for each, proving the transform wrapped every +// storage-bearing DO segment span for each, proving the transform wrapped every // configured class rather than stopping after the first match. it('auto-instruments multiple Durable Object classes in one entry', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - // One storage-bearing DO transaction per configured class. - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - // One child-less main worker transaction per request. - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // Each request runs in its own trace, and inside a trace the worker and the Durable Object stream + // from separate isolates. One collector per request therefore waits for that trace's two segment + // spans. + for (const path of ['/increment-a', '/increment-b']) { + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); + + await runner.makeRequest('get', path); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['url.path']).toEqual({ type: 'string', value: path }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); - await runner.makeRequest('get', '/increment-a'); - await runner.makeRequest('get', '/increment-b'); - await runner.completed(); + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); + } }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/counter.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/counter.ts index 0692079bfd9f..c06b3753cfca 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/counter.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/counter.ts @@ -17,6 +17,6 @@ class CounterImpl extends DurableObject { // The Durable Object is manually instrumented here, in a module *separate* from // the worker entry. The entry only imports and re-exports the wrapped class. export const Counter = Sentry.instrumentDurableObjectWithSentry( - (env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }), + (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }), CounterImpl, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/test.ts index d09b4ceeeda6..7dbf5aa672c3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/test.ts @@ -1,45 +1,6 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class is instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transaction. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // `Counter` is manually wrapped with `instrumentDurableObjectWithSentry` in a // separate module (`./counter`), imported into the entry, and re-exported via a @@ -47,16 +8,39 @@ function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { // than a local class declaration, the transform cannot wrap it in the entry and // must leave it alone — no double-wrap, no broken build. The DO stays // instrumented via the manual wrap, so we still expect a storage-bearing DO -// transaction, alongside the auto-wrapped default export's child-less one. +// segment span, alongside the auto-wrapped default export's child-less one. it('leaves an imported, already-instrumented Durable Object untouched and still wraps the default export', async ({ signal, }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. Exactly two of them also rules out a double-wrap. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/instrument.server.ts index d2e70b59d426..8d9cf9740b68 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/instrument.server.ts @@ -3,6 +3,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; // `rpcTracePropagationBindings` is deliberately absent, the Vite plugin derives `COUNTER` on its own. export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/test.ts index 20483653eeec..050b1dd3ef26 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-rpc/test.ts @@ -1,31 +1,24 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; +import { getSpanOp } from '../../../spanUtils'; it('propagates the trace over a Durable Object RPC call without configuring the binding', async ({ signal }) => { - let workerTraceId: string | undefined; - let doTraceId: string | undefined; + const runner = createRunner(__dirname).start(signal); - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - expect(transactionEvent.contexts?.trace?.op).toBe('rpc'); - doTraceId = transactionEvent.contexts?.trace?.trace_id; - }) - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - workerTraceId = transactionEvent.contexts?.trace?.trace_id; - }) - .start(signal); + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); // `argumentCount` proves the receiver stripped the metadata argument again. const response = await runner.makeRequest<{ count: number; argumentCount: number }>('get', '/increment'); expect(response).toEqual({ count: 1, argumentCount: 1 }); - await runner.completed(); + const segmentSpans = (await spansPromise).filter(span => span.is_segment); + const workerSpan = segmentSpans.find(span => getSpanOp(span) === 'http.server'); + const durableObjectSpan = segmentSpans.find(span => getSpanOp(span) === 'rpc'); - expect(workerTraceId).toBeDefined(); - expect(doTraceId).toBe(workerTraceId); + expect(workerSpan).toBeDefined(); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/test.ts index e943483b6542..a54d5dfa0308 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/test.ts @@ -1,57 +1,41 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class was actually auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transaction. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // The wrangler binding references `Counter`, which is only an exported *alias* // of the local `CounterImpl` class (`export { CounterImpl as Counter }`). The // transform resolves the alias and wraps the local class, so the DO storage // spans only arrive if the aliased-specifier form was handled. it('auto-instruments a Durable Object exported via an aliased specifier', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/test.ts index 1935635262e3..7b02c87d3df9 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/test.ts @@ -1,57 +1,41 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class was actually auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transaction. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // The Durable Object is exported through a specifier list (`export { Counter }`) // instead of an inline `export class`. The transform renames the class to // `__SENTRY_ORIGINAL_Counter__` and rebinds `Counter` to the wrapped class, so // the DO storage spans only arrive if that specifier form was handled. it('auto-instruments a Durable Object exported via a specifier list', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/index.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/index.ts index 65dac0f7e3ad..895b61a4c8e2 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/index.ts @@ -20,7 +20,7 @@ class CounterImpl extends DurableObject { // existing `instrumentDurableObjectWithSentry` call — matched by the DO-kind // wrapper method, not the workflow one — and leave it untouched (no double-wrap). export const Counter = Sentry.instrumentDurableObjectWithSentry( - (env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }), + (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }), CounterImpl, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/test.ts index 390b25ea64a2..4abc814568d3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/test.ts @@ -1,71 +1,50 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present here because the class was manually wrapped. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// A workflow step runs in its own invocation and reports a `function` / -// `auto.faas.cloudflare.workflow` transaction named after the step — present -// only because the transform auto-wrapped the Workflow class. -function expectWorkflowStepTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent.transaction).toBe('step-one'); - expect(transactionEvent.contexts?.trace?.op).toBe('function'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.faas.cloudflare.workflow'); -} - -// The main worker transaction for `/increment` just forwards to the DO, so it -// carries no child spans. The empty-spans assertion keeps it disjoint from the -// DO and workflow transactions regardless of arrival order. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // The Durable Object is wrapped by hand with `instrumentDurableObjectWithSentry` // while the Workflow sibling is a plain inline export. The transform must match // the manual wrap by its DO-kind method and skip it (no double-wrap) while still // auto-wrapping the Workflow with `instrumentWorkflowWithSentry`. We therefore -// expect a storage-bearing DO transaction (manual wrap) and a `step-one` -// transaction (auto wrap), plus the child-less main worker transaction. +// expect a storage-bearing DO segment span (manual wrap) and a `step-one` +// segment span (auto wrap). it('leaves a manually wrapped Durable Object untouched and still auto-wraps a Workflow sibling', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectWorkflowStepTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The workflow step runs in its own trace, separate from either request. + const stepSpansPromise = runner.collectStreamedSpansUntilSegment('step-one'); + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the `/increment` trace arrive in separate envelopes. + const durableObjectSpansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); + + const spans = await durableObjectSpansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. Exactly two of them also rules out a double-wrap. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); + await runner.makeRequest('get', '/workflow/trigger'); - await runner.completed(); + + const stepSpan = (await stepSpansPromise).find(span => span.name === 'step-one'); + expect(getSpanOp(stepSpan!)).toBe('function'); + expect(stepSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.faas.cloudflare.workflow' }); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/test.ts index a9d88f6d64bd..ffefbcade6c3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/test.ts @@ -1,70 +1,49 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class was auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// A workflow step runs in its own invocation and reports a `function` / -// `auto.faas.cloudflare.workflow` transaction named after the step — present -// only when the Workflow class was wrapped with `instrumentWorkflowWithSentry`. -function expectWorkflowStepTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent.transaction).toBe('step-one'); - expect(transactionEvent.contexts?.trace?.op).toBe('function'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.faas.cloudflare.workflow'); -} - -// The main worker transaction for `/increment` just forwards to the DO, so it -// carries no child spans. The empty-spans assertion keeps it disjoint from the -// DO and workflow transactions regardless of arrival order. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // A Durable Object and a Workflow are both exported through a single specifier // list (`export { Counter, MyWorkflow }`) instead of inline `export class`. The // transform renames each local class and rebinds the exported name to the -// kind-specific wrapper, so the DO storage spans and the `step-one` workflow -// transaction only arrive if the specifier form was handled for both kinds. +// kind-specific wrapper, so the DO storage spans and the `step-one` segment span +// only arrive if the specifier form was handled for both kinds. it('auto-instruments a Durable Object and a Workflow exported via a specifier list', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectWorkflowStepTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The workflow step runs in its own trace, separate from either request. + const stepSpansPromise = runner.collectStreamedSpansUntilSegment('step-one'); + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the `/increment` trace arrive in separate envelopes. + const durableObjectSpansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); + + const spans = await durableObjectSpansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); + await runner.makeRequest('get', '/workflow/trigger'); - await runner.completed(); + + const stepSpan = (await stepSpansPromise).find(span => span.name === 'step-one'); + expect(getSpanOp(stepSpan!)).toBe('function'); + expect(stepSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.faas.cloudflare.workflow' }); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/test.ts index e77bb20128d5..9861748a30ce 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/test.ts @@ -1,69 +1,48 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; - -// A fetch-invoked Durable Object emits an `http.server` transaction whose only -// children are the two `auto.db.cloudflare.durable_object` storage spans -// (`get` + `put`) — present only when the class was auto-instrumented. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// A workflow step runs in its own invocation and reports a `function` / -// `auto.faas.cloudflare.workflow` transaction named after the step — present -// only when the Workflow class was wrapped with `instrumentWorkflowWithSentry`. -function expectWorkflowStepTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent.transaction).toBe('step-one'); - expect(transactionEvent.contexts?.trace?.op).toBe('function'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.faas.cloudflare.workflow'); -} - -// The main worker transaction for `/increment` just forwards to the DO, so it -// carries no child spans. The empty-spans assertion keeps it disjoint from the -// DO and workflow transactions regardless of arrival order. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} +import { getSpanOp } from '../../../spanUtils'; // A single entry exports both a Durable Object and a Workflow, neither wrapped by // hand. The transform must wrap each with its kind-specific helper: hitting the -// DO yields a storage-bearing DO transaction and triggering the workflow yields a -// `step-one` transaction. Both only arrive if both classes were auto-wrapped. +// DO yields a storage-bearing DO segment span and triggering the workflow yields +// a `step-one` segment span. Both only arrive if both classes were auto-wrapped. it('auto-instruments a Durable Object and a Workflow in the same entry', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectWorkflowStepTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The workflow step runs in its own trace, separate from either request. + const stepSpansPromise = runner.collectStreamedSpansUntilSegment('step-one'); + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the `/increment` trace arrive in separate envelopes. + const durableObjectSpansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); + + const spans = await durableObjectSpansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + // The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an + // instrumented Durable Object. + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); + await runner.makeRequest('get', '/workflow/trigger'); - await runner.completed(); + + const stepSpan = (await stepSpansPromise).find(span => span.name === 'step-one'); + expect(getSpanOp(stepSpan!)).toBe('function'); + expect(stepSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.faas.cloudflare.workflow' }); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/test.ts index 42a78ea2840a..c7012293e240 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/test.ts @@ -1,62 +1,47 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; +import { getSpanOp } from '../../../spanUtils'; -// A Durable Object invoked via `fetch` produces its own `http.server` / -// `auto.http.cloudflare` transaction (its `fetch` is wrapped with +// A Durable Object invoked via `fetch` gets its own `http.server` / +// `auto.http.cloudflare` segment span (its `fetch` is wrapped with // `wrapRequestHandler`, not the faas wrapper used for alarms/websockets/RPC). // The proof the class was auto-instrumented is the pair of // `auto.db.cloudflare.durable_object` storage spans (`get` + `put`) it emits — // absent entirely when the class is left unwrapped. -function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(2); - expect(transactionEvent.spans).toEqual([ - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_get', - origin: 'auto.db.cloudflare.durable_object', - }), - expect.objectContaining({ - op: 'db', - description: 'durable_object_storage_put', - origin: 'auto.db.cloudflare.durable_object', - }), - ]); -} - -// The main worker transaction just forwards to the DO, so it carries no child -// spans. The empty-spans assertion keeps it disjoint from the DO transaction, so -// neither can satisfy the other's expectation regardless of arrival order. -function expectMainWorkerTransaction(transactionEvent: TransactionEvent): void { - expect(transactionEvent).toEqual( - expect.objectContaining({ - contexts: expect.objectContaining({ - trace: expect.objectContaining({ op: 'http.server', origin: 'auto.http.cloudflare' }), - }), - }), - ); - expect(transactionEvent.spans).toHaveLength(0); -} - +// // The worker is built by the Sentry Vite plugin (auto-instrumentation on). The // runner detects `vite.config.mts`, runs `vite build`, and serves the generated -// output — so these transactions only arrive if the build-time transform wrapped -// both the default handler (`withSentry`) and the `Counter` Durable Object +// output — so these spans only arrive if the build-time transform wrapped both +// the default handler (`withSentry`) and the `Counter` Durable Object // (`instrumentDurableObjectWithSentry`). it('auto-instruments the default handler and a Durable Object', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .expect(envelope => expectMainWorkerTransaction(envelope[1]?.[0]?.[1] as TransactionEvent)) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The worker and the Durable Object stream from separate isolates, so the two segment spans of + // the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/increment'); - await runner.completed(); + + const spans = await spansPromise; + const workerSpan = spans.find(span => span.is_segment && !span.parent_span_id); + const durableObjectSpan = spans.find(span => span.is_segment && span.parent_span_id); + + expect(getSpanOp(workerSpan!)).toBe('http.server'); + expect(workerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + + expect(getSpanOp(durableObjectSpan!)).toBe('http.server'); + expect(durableObjectSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); + expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id); + + expect( + spans + .filter(span => span.parent_span_id === durableObjectSpan?.span_id) + .map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })), + ).toEqual([ + { name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + { name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' }, + ]); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/test.ts index de3e96f9850d..02c432918863 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/test.ts @@ -1,30 +1,35 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; +import { getSpanOp } from '../../../spanUtils'; // The worker is built by the Sentry Vite plugin (auto-instrumentation on). The // runner detects `vite.config.mts`, runs `vite build`, and serves the generated -// output — so these transactions only arrive if the build-time transform wrapped -// both the default handler and the self-bound `GreeterEntrypoint`. +// output — so these spans only arrive if the build-time transform wrapped both +// the default handler and the self-bound `GreeterEntrypoint`. it('auto-instruments the default handler and a self-bound WorkerEntrypoint', async ({ signal }) => { - const runner = createRunner(__dirname) - .unordered() - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - // Main worker's http.server transaction — proves `withSentry` wrapped the - // unwrapped default export. - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.transaction).toBe('GET /call-entrypoint'); - }) - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - // The entrypoint's own http.server transaction — proves the auto-wrap - // identified and wrapped the named `WorkerEntrypoint`. - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.transaction).toBe('GET /greet'); - }) - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The default handler and the entrypoint stream from separate isolates, so the two segment spans + // of the trace arrive in separate envelopes. + const spansPromise = runner.collectStreamedSpans( + spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2, + ); await runner.makeRequest('get', '/call-entrypoint'); - await runner.completed(); + + const segmentSpans = (await spansPromise).filter(span => span.is_segment); + // Both routes are raw URLs, so the streamed segment names keep the method only and the route is + // read from `url.path`. + const workerSpan = segmentSpans.find(span => span.attributes['url.path']?.value === '/call-entrypoint'); + const entrypointSpan = segmentSpans.find(span => span.attributes['url.path']?.value === '/greet'); + + // Main worker span — proves `withSentry` wrapped the unwrapped default export. + expect(workerSpan?.name).toBe('GET'); + expect(getSpanOp(workerSpan!)).toBe('http.server'); + + // The entrypoint's own span — proves the auto-wrap identified and wrapped the named + // `WorkerEntrypoint`. + expect(entrypointSpan?.name).toBe('GET'); + expect(getSpanOp(entrypointSpan!)).toBe('http.server'); + expect(entrypointSpan?.parent_span_id).toBe(workerSpan?.span_id); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/instrument.server.ts index 4355b90010d6..e577550ae0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/instrument.server.ts @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare'; export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, - traceLifecycle: 'static', tracesSampleRate: 1.0, })); diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts index f2771ae8f4f5..089f1b345982 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts @@ -1,32 +1,30 @@ -import type { TransactionEvent } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; +import { getSpanOp } from '../../../spanUtils'; // The worker is built by the Sentry Vite plugin (auto-instrumentation on). The // runner detects `vite.config.mts`, runs `vite build`, and serves the generated -// output — so these transactions only arrive if the build-time transform wrapped +// output — so these spans only arrive if the build-time transform wrapped // `MyWorkflow` with `instrumentWorkflowWithSentry` and the default export with // `withSentry`. -// -// The workflow step and the triggering request are separate executions whose -// envelopes race, so both are expected `unordered`. it('auto-instruments a Workflow class', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - expect(transactionEvent.transaction).toBe('step-one'); - expect(transactionEvent.contexts?.trace?.op).toBe('function'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.faas.cloudflare.workflow'); - }) - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; - expect(transactionEvent.transaction).toBe('GET /workflow/trigger'); - expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); - }) - .unordered() - .start(signal); + const runner = createRunner(__dirname).start(signal); + + // The workflow step and the triggering request are separate executions, each with its own trace. + const stepSpansPromise = runner.collectStreamedSpansUntilSegment('step-one'); + const triggerSpansPromise = runner.collectStreamedSpansUntilSegment( + span => span.attributes['url.path']?.value === '/workflow/trigger', + ); await runner.makeRequest('get', '/workflow/trigger'); - await runner.completed(); + + const stepSpan = (await stepSpansPromise).find(span => span.name === 'step-one'); + expect(getSpanOp(stepSpan!)).toBe('function'); + expect(stepSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.faas.cloudflare.workflow' }); + + const triggerSpan = (await triggerSpansPromise).find(span => span.is_segment); + // `/workflow/trigger` is a raw URL, so the streamed segment name keeps the method only. + expect(triggerSpan?.name).toBe('GET'); + expect(getSpanOp(triggerSpan!)).toBe('http.server'); + expect(triggerSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' }); });