Skip to content

Commit d88f7b6

Browse files
JPeer264claude
andcommitted
test(cloudflare): Port the tracing suites to span streaming
Removes the `traceLifecycle: 'static'` pin from the `suites/tracing` suites, and rewrites the assertions from transaction envelopes to span v2. The `tracing/propagation` suites follow in a separate PR. `tracing/d1` also loses its env-gated `STREAMED` switch and the static copy of its test, so one test now covers the streamed behaviour. Most suites keep the shape they had, and only the assertions change: a span carries its name in `name` rather than `description`, its op and origin as attributes rather than fields, and every attribute as a `{ type, value }` pair. The gen_ai suites keep asserting the complete attribute set, so an unexpected attribute still fails the test. Streaming adds six attributes to every span: `sentry.trace_lifecycle`, `sentry.segment.name`, `sentry.segment.id`, `sentry.sdk.name`, `sentry.sdk.version` and `sentry.environment`. The runs surfaced three behaviours that shape the rewrite. A `url` source segment span keeps the method only. `GET /error` becomes `GET`, so those suites assert `GET` and read the route from the `url.path` attribute. The segment span ends last, so it can arrive in a later envelope than its children. `opentelemetry-tracer/internal-request-handler` failed on that. It waits for its three child spans by name: each envelope is its own request to the mock server, so waiting for the segment alone would let it resolve before the envelope carrying those children arrives. Start timestamps tie at millisecond resolution in workerd, so an order-based assertion over child spans is unreliable. `opentelemetry-tracer/enabled` matches spans by name instead. `instrument-fetcher` uses `collectStreamedSpans`, because the worker and the Durable Object stream their segment spans from separate isolates. The Durable Object storage suites keep a single envelope expectation, because their spans do arrive together. `workers-ai` ignores the `span` envelope where it used to ignore `transaction`. Its error assertion never read the trace, and `transaction` is now an envelope type nothing emits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 013f716 commit d88f7b6

44 files changed

Lines changed: 600 additions & 884 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const mockFetch: typeof fetch = async () =>
2424
export default Sentry.withSentry(
2525
(env: Env) => ({
2626
dsn: env.SENTRY_DSN,
27-
traceLifecycle: 'static',
2827
tracesSampleRate: 1.0,
2928
dataCollection: { genAI: { inputs: true, outputs: true } },
3029
}),

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ import {
1212
GEN_AI_USAGE_INPUT_TOKENS,
1313
GEN_AI_USAGE_OUTPUT_TOKENS,
1414
GEN_AI_USAGE_TOTAL_TOKENS,
15+
SENTRY_SDK_NAME,
16+
SENTRY_SDK_VERSION,
17+
SENTRY_SEGMENT_ID,
18+
SENTRY_SEGMENT_NAME,
19+
SENTRY_TRACE_LIFECYCLE,
1520
} from '@sentry/conventions/attributes';
21+
import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT } from '@sentry/core';
1622
import { createRunner } from '../../../runner';
23+
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
1724

1825
// This test runs the `@anthropic-ai/sdk` on the Workers runtime (with a
1926
// canned fetch) to verify the instrumentation works end-to-end on Cloudflare,
@@ -23,14 +30,14 @@ it('traces a basic message creation request with the anthropic SDK', async ({ si
2330
const runner = createRunner(__dirname)
2431
.ignore('event')
2532
.expect(envelope => {
26-
const transactionEvent = envelope[1]?.[0]?.[1] as any;
27-
expect(transactionEvent.transaction).toBe('GET /');
33+
const spans = getSpansFromEnvelope(envelope);
34+
const segmentSpan = spans.find(span => span.is_segment);
35+
expect(segmentSpan?.name).toBe('GET /');
2836

29-
const container = envelope[1]?.[1]?.[1] as any;
30-
expect(container).toBeDefined();
31-
expect(container.items).toHaveLength(1);
37+
const genAiSpans = spans.filter(span => getSpanOp(span)?.startsWith('gen_ai.'));
38+
expect(genAiSpans).toHaveLength(1);
3239

33-
expect(container.items[0]).toEqual({
40+
expect(genAiSpans[0]).toEqual({
3441
trace_id: expect.any(String),
3542
span_id: expect.any(String),
3643
parent_span_id: expect.any(String),
@@ -58,6 +65,12 @@ it('traces a basic message creation request with the anthropic SDK', async ({ si
5865
[GEN_AI_USAGE_INPUT_TOKENS]: { value: 10, type: 'integer' },
5966
[GEN_AI_USAGE_OUTPUT_TOKENS]: { value: 15, type: 'integer' },
6067
[GEN_AI_USAGE_TOTAL_TOKENS]: { value: 25, type: 'integer' },
68+
[SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' },
69+
[SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' },
70+
[SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' },
71+
[SENTRY_SDK_NAME]: { value: 'sentry.javascript.cloudflare', type: 'string' },
72+
[SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' },
73+
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
6174
},
6275
});
6376
})

‎dev-packages/cloudflare-integration-tests/suites/tracing/d1/index.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import * as Sentry from '@sentry/cloudflare';
22

33
interface Env {
44
SENTRY_DSN: string;
5-
STREAMED?: string;
65
DB: D1Database;
76
}
87

98
export default Sentry.withSentry(
109
(env: Env) => ({
1110
dsn: env.SENTRY_DSN,
12-
traceLifecycle: env.STREAMED === 'true' ? 'stream' : 'static',
11+
1312
tracesSampleRate: 1.0,
1413
}),
1514
{
Lines changed: 97 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { describe, expect, it } from 'vitest';
2-
import type { Envelope, SerializedStreamedSpanContainer } from '@sentry/core';
1+
import { expect, it } from 'vitest';
2+
import type { Envelope, SerializedStreamedSpan } from '@sentry/core';
33
import {
44
SDK_VERSION,
55
SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
@@ -14,213 +14,112 @@ import {
1414
SENTRY_TRACE_LIFECYCLE,
1515
} from '@sentry/conventions/attributes';
1616
import { createRunner } from '../../../runner';
17+
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
1718

18-
it('D1 database queries create spans with correct attributes', async ({ signal }) => {
19-
const runner = createRunner(__dirname)
20-
.expect(envelope => {
21-
const transactionEvent = envelope[1]?.[0]?.[1];
22-
expect(transactionEvent).toEqual(
23-
expect.objectContaining({
24-
type: 'transaction',
25-
transaction: 'GET /init',
26-
spans: [
27-
{
28-
data: {
29-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
30-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
31-
'db.system.name': 'cloudflare-d1',
32-
'db.operation.name': 'exec',
33-
'db.query.text': 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)',
34-
'db.query.summary': 'CREATE TABLE users',
35-
},
36-
description: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)',
37-
op: 'db.query',
38-
origin: 'auto.db.cloudflare.d1',
39-
status: 'ok',
40-
parent_span_id: expect.any(String),
41-
span_id: expect.any(String),
42-
start_timestamp: expect.any(Number),
43-
timestamp: expect.any(Number),
44-
trace_id: expect.any(String),
45-
},
46-
{
47-
data: {
48-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
49-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
50-
'db.system.name': 'cloudflare-d1',
51-
'db.operation.name': 'run',
52-
'db.query.text': 'INSERT INTO users (name) VALUES (?)',
53-
'db.query.summary': 'INSERT users',
54-
'cloudflare.d1.duration': expect.any(Number),
55-
'cloudflare.d1.rows_read': expect.any(Number),
56-
'cloudflare.d1.rows_written': expect.any(Number),
57-
},
58-
description: 'INSERT INTO users (name) VALUES (?)',
59-
op: 'db.query',
60-
origin: 'auto.db.cloudflare.d1',
61-
status: 'ok',
62-
parent_span_id: expect.any(String),
63-
span_id: expect.any(String),
64-
start_timestamp: expect.any(Number),
65-
timestamp: expect.any(Number),
66-
trace_id: expect.any(String),
67-
},
68-
],
69-
}),
70-
);
71-
})
72-
.expect(envelope => {
73-
const transactionEvent = envelope[1]?.[0]?.[1];
74-
expect(transactionEvent).toEqual(
75-
expect.objectContaining({
76-
type: 'transaction',
77-
transaction: 'GET /query',
78-
spans: [
79-
{
80-
data: {
81-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
82-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1',
83-
'db.system.name': 'cloudflare-d1',
84-
'db.operation.name': 'first',
85-
'db.query.text': 'SELECT * FROM users WHERE name = ?',
86-
'db.query.summary': 'SELECT users',
87-
},
88-
description: 'SELECT * FROM users WHERE name = ?',
89-
op: 'db.query',
90-
origin: 'auto.db.cloudflare.d1',
91-
status: 'ok',
92-
parent_span_id: expect.any(String),
93-
span_id: expect.any(String),
94-
start_timestamp: expect.any(Number),
95-
timestamp: expect.any(Number),
96-
trace_id: expect.any(String),
97-
},
98-
],
99-
}),
100-
);
101-
})
102-
.start(signal);
103-
104-
await runner.makeRequest('get', '/init');
105-
await runner.makeRequest('get', '/query');
106-
await runner.completed();
107-
});
19+
/** The `db.query` spans of an envelope, paired with the segment span they belong to. */
20+
function getD1Spans(envelope: Envelope): {
21+
segmentSpan: SerializedStreamedSpan;
22+
d1Spans: SerializedStreamedSpan[];
23+
} {
24+
const spans = getSpansFromEnvelope(envelope);
25+
const segmentSpan = spans.find(span => span.is_segment);
26+
expect(segmentSpan).toBeDefined();
10827

109-
describe('with span streaming enabled', () => {
110-
function getSpanContainer(envelope: Envelope): SerializedStreamedSpanContainer {
111-
const spanItem = envelope[1].find(item => item[0].type === 'span');
112-
expect(spanItem).toBeDefined();
113-
return spanItem![1] as SerializedStreamedSpanContainer;
114-
}
28+
return {
29+
segmentSpan: segmentSpan!,
30+
d1Spans: spans.filter(span => getSpanOp(span) === 'db.query'),
31+
};
32+
}
11533

116-
/** The `db.query` spans of an envelope, paired with the segment span they belong to. */
117-
function getD1Spans(envelope: Envelope): {
118-
segmentSpan: SerializedStreamedSpanContainer['items'][number];
119-
d1Spans: SerializedStreamedSpanContainer['items'];
120-
} {
121-
const items = getSpanContainer(envelope).items;
122-
const segmentSpan = items.find(item => item.is_segment);
123-
expect(segmentSpan).toBeDefined();
34+
function commonAttributes(segmentSpan: SerializedStreamedSpan): SerializedStreamedSpan['attributes'] {
35+
return {
36+
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
37+
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
38+
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
39+
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan.span_id },
40+
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentSpan.name },
41+
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
42+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db.query' },
43+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' },
44+
'db.system.name': { type: 'string', value: 'cloudflare-d1' },
45+
};
46+
}
12447

125-
return {
126-
segmentSpan: segmentSpan!,
127-
d1Spans: items.filter(item => item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'db.query'),
128-
};
129-
}
48+
function commonSpanProps(segmentSpan: SerializedStreamedSpan): Record<string, unknown> {
49+
return {
50+
is_segment: false,
51+
parent_span_id: segmentSpan.span_id,
52+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
53+
trace_id: segmentSpan.trace_id,
54+
start_timestamp: expect.any(Number),
55+
end_timestamp: expect.any(Number),
56+
status: 'ok',
57+
};
58+
}
13059

131-
function commonAttributes(
132-
segmentSpan: SerializedStreamedSpanContainer['items'][number],
133-
): SerializedStreamedSpanContainer['items'][number]['attributes'] {
134-
return {
135-
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
136-
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
137-
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
138-
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan.span_id },
139-
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentSpan.name },
140-
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
141-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db.query' },
142-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' },
143-
'db.system.name': { type: 'string', value: 'cloudflare-d1' },
144-
};
145-
}
60+
// `cloudflare.d1.duration` is only an integer when the query happens to take a whole
61+
// number of milliseconds, so the type can't be pinned down.
62+
const NUMBER_ATTRIBUTE = { type: expect.stringMatching(/^(?:integer|double)$/), value: expect.any(Number) };
14663

147-
function commonSpanProps(segmentSpan: SerializedStreamedSpanContainer['items'][number]): Record<string, unknown> {
148-
return {
149-
is_segment: false,
150-
parent_span_id: segmentSpan.span_id,
151-
span_id: expect.stringMatching(/^[\da-f]{16}$/),
152-
trace_id: segmentSpan.trace_id,
153-
start_timestamp: expect.any(Number),
154-
end_timestamp: expect.any(Number),
155-
status: 'ok',
156-
};
157-
}
158-
159-
// `cloudflare.d1.duration` is only an integer when the query happens to take a whole
160-
// number of milliseconds, so the type can't be pinned down.
161-
const NUMBER_ATTRIBUTE = { type: expect.stringMatching(/^(?:integer|double)$/), value: expect.any(Number) };
162-
163-
it('names D1 query spans after their query summary', async ({ signal }) => {
164-
const runner = createRunner(__dirname)
165-
.withWranglerArgs('--var', 'STREAMED:true')
166-
.expect(envelope => {
167-
const { segmentSpan, d1Spans } = getD1Spans(envelope);
168-
// With span streaming, the server span name is low cardinality, so the request the
169-
// envelope belongs to is only identifiable through `url.path`.
170-
expect(segmentSpan.name).toBe('GET');
171-
expect(segmentSpan.attributes['url.path']).toEqual({ type: 'string', value: '/init' });
64+
it('names D1 query spans after their query summary', async ({ signal }) => {
65+
const runner = createRunner(__dirname)
66+
.expect(envelope => {
67+
const { segmentSpan, d1Spans } = getD1Spans(envelope);
68+
// With span streaming, the server span name is low cardinality, so the request the
69+
// envelope belongs to is only identifiable through `url.path`.
70+
expect(segmentSpan.name).toBe('GET');
71+
expect(segmentSpan.attributes['url.path']).toEqual({ type: 'string', value: '/init' });
17272

173-
expect(d1Spans).toEqual([
174-
{
175-
name: 'CREATE TABLE users',
176-
attributes: {
177-
...commonAttributes(segmentSpan),
178-
'db.operation.name': { type: 'string', value: 'exec' },
179-
'db.query.text': {
180-
type: 'string',
181-
value: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)',
182-
},
183-
'db.query.summary': { type: 'string', value: 'CREATE TABLE users' },
73+
expect(d1Spans).toEqual([
74+
{
75+
name: 'CREATE TABLE users',
76+
attributes: {
77+
...commonAttributes(segmentSpan),
78+
'db.operation.name': { type: 'string', value: 'exec' },
79+
'db.query.text': {
80+
type: 'string',
81+
value: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)',
18482
},
185-
...commonSpanProps(segmentSpan),
83+
'db.query.summary': { type: 'string', value: 'CREATE TABLE users' },
18684
},
187-
{
188-
name: 'INSERT users',
189-
attributes: {
190-
...commonAttributes(segmentSpan),
191-
'db.operation.name': { type: 'string', value: 'run' },
192-
'db.query.text': { type: 'string', value: 'INSERT INTO users (name) VALUES (?)' },
193-
'db.query.summary': { type: 'string', value: 'INSERT users' },
194-
'cloudflare.d1.duration': NUMBER_ATTRIBUTE,
195-
'cloudflare.d1.rows_read': NUMBER_ATTRIBUTE,
196-
'cloudflare.d1.rows_written': NUMBER_ATTRIBUTE,
197-
},
198-
...commonSpanProps(segmentSpan),
85+
...commonSpanProps(segmentSpan),
86+
},
87+
{
88+
name: 'INSERT users',
89+
attributes: {
90+
...commonAttributes(segmentSpan),
91+
'db.operation.name': { type: 'string', value: 'run' },
92+
'db.query.text': { type: 'string', value: 'INSERT INTO users (name) VALUES (?)' },
93+
'db.query.summary': { type: 'string', value: 'INSERT users' },
94+
'cloudflare.d1.duration': NUMBER_ATTRIBUTE,
95+
'cloudflare.d1.rows_read': NUMBER_ATTRIBUTE,
96+
'cloudflare.d1.rows_written': NUMBER_ATTRIBUTE,
19997
},
200-
]);
201-
})
202-
.expect(envelope => {
203-
const { segmentSpan, d1Spans } = getD1Spans(envelope);
204-
expect(segmentSpan.name).toBe('GET');
205-
expect(segmentSpan.attributes['url.path']).toEqual({ type: 'string', value: '/query' });
98+
...commonSpanProps(segmentSpan),
99+
},
100+
]);
101+
})
102+
.expect(envelope => {
103+
const { segmentSpan, d1Spans } = getD1Spans(envelope);
104+
expect(segmentSpan.name).toBe('GET');
105+
expect(segmentSpan.attributes['url.path']).toEqual({ type: 'string', value: '/query' });
206106

207-
expect(d1Spans).toEqual([
208-
{
209-
name: 'SELECT users',
210-
attributes: {
211-
...commonAttributes(segmentSpan),
212-
'db.operation.name': { type: 'string', value: 'first' },
213-
'db.query.text': { type: 'string', value: 'SELECT * FROM users WHERE name = ?' },
214-
'db.query.summary': { type: 'string', value: 'SELECT users' },
215-
},
216-
...commonSpanProps(segmentSpan),
107+
expect(d1Spans).toEqual([
108+
{
109+
name: 'SELECT users',
110+
attributes: {
111+
...commonAttributes(segmentSpan),
112+
'db.operation.name': { type: 'string', value: 'first' },
113+
'db.query.text': { type: 'string', value: 'SELECT * FROM users WHERE name = ?' },
114+
'db.query.summary': { type: 'string', value: 'SELECT users' },
217115
},
218-
]);
219-
})
220-
.start(signal);
116+
...commonSpanProps(segmentSpan),
117+
},
118+
]);
119+
})
120+
.start(signal);
221121

222-
await runner.makeRequest('get', '/init');
223-
await runner.makeRequest('get', '/query');
224-
await runner.completed();
225-
});
122+
await runner.makeRequest('get', '/init');
123+
await runner.makeRequest('get', '/query');
124+
await runner.completed();
226125
});

‎dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ interface Env {
1010
export default Sentry.withSentry(
1111
(env: Env) => ({
1212
dsn: env.SENTRY_DSN,
13-
traceLifecycle: 'static',
1413
tracesSampleRate: 1.0,
1514
}),
1615
{

0 commit comments

Comments
 (0)