Skip to content

Commit cfc4547

Browse files
authored
Merge branch 'develop' into sig/nextjs-orchestion-tests
2 parents 6174940 + 29b67d3 commit cfc4547

446 files changed

Lines changed: 12144 additions & 6679 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.

‎MIGRATION.md‎

Lines changed: 114 additions & 51 deletions
Large diffs are not rendered by default.

‎dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/ai-streaming.test.ts‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import { getSpanOp, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33
import type { SerializedStreamedSpan } from '@sentry/core';
44

55
// Drives Workers AI through the real Cloudflare Agents SDK + Vercel AI SDK + `workers-ai-provider`
@@ -26,32 +26,41 @@ function assertGenAiStreamingSpan(span: SerializedStreamedSpan): void {
2626
expect(span.attributes['gen_ai.conversation.id']?.value).toMatch(/^[0-9a-f]{32}$/);
2727
}
2828

29+
// With span streaming, URL-sourced `http.server` spans are named by method only, so the request
30+
// segments below are identified by their `url.path` attribute.
31+
2932
test('captures Workers AI streaming output when driven via an Agent', async ({ request, baseURL }) => {
3033
const spanPromise = waitForStreamedSpan('cloudflare-agent', span => getSpanOp(span) === 'gen_ai.chat');
31-
const transactionPromise = waitForTransaction(
34+
const requestSpanPromise = waitForStreamedSpan(
3235
'cloudflare-agent',
33-
transactionEvent => transactionEvent.transaction === 'GET /agents/my-agent/test',
36+
span =>
37+
getSpanOp(span) === 'http.server' &&
38+
span.is_segment &&
39+
span.attributes['url.path']?.value === '/agents/my-agent/test',
3440
);
3541

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

39-
const [span, transaction] = await Promise.all([spanPromise, transactionPromise]);
40-
expect(span.trace_id).toBe(transaction.contexts?.trace?.trace_id);
45+
const [span, requestSpan] = await Promise.all([spanPromise, requestSpanPromise]);
46+
expect(span.trace_id).toBe(requestSpan.trace_id);
4147
assertGenAiStreamingSpan(span);
4248
});
4349

4450
test('captures Workers AI streaming output when driven via an AIChatAgent', async ({ request, baseURL }) => {
4551
const spanPromise = waitForStreamedSpan('cloudflare-agent', span => getSpanOp(span) === 'gen_ai.chat');
46-
const transactionPromise = waitForTransaction(
52+
const requestSpanPromise = waitForStreamedSpan(
4753
'cloudflare-agent',
48-
transactionEvent => transactionEvent.transaction === 'GET /agents/my-chat-agent/test',
54+
span =>
55+
getSpanOp(span) === 'http.server' &&
56+
span.is_segment &&
57+
span.attributes['url.path']?.value === '/agents/my-chat-agent/test',
4958
);
5059

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

54-
const [span, transaction] = await Promise.all([spanPromise, transactionPromise]);
55-
expect(span.trace_id).toBe(transaction.contexts?.trace?.trace_id);
63+
const [span, requestSpan] = await Promise.all([spanPromise, requestSpanPromise]);
64+
expect(span.trace_id).toBe(requestSpan.trace_id);
5665
assertGenAiStreamingSpan(span);
5766
});
Lines changed: 119 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,192 +1,177 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
4+
// The agent request segment is the Durable Object's `http.server` span. It has a parent because
5+
// the worker propagates its trace over the RPC binding; the worker's own segment for the same URL
6+
// does not. With span streaming the name is the method only, so the segment is picked by `url.path`.
37

48
test('@callable() methods work correctly with Sentry instrumentAgentWithSentry', async ({ page, baseURL }) => {
5-
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
6-
return (
7-
transactionEvent.transaction === 'GET /agents/my-agent/user-123' &&
8-
transactionEvent.contexts?.trace?.parent_span_id !== undefined
9-
);
10-
});
9+
const requestSpanPromise = waitForStreamedSpan(
10+
'cloudflare-agent',
11+
span =>
12+
getSpanOp(span) === 'http.server' &&
13+
span.is_segment &&
14+
span.attributes['url.path']?.value === '/agents/my-agent/user-123' &&
15+
span.parent_span_id !== undefined,
16+
);
1117

12-
// The greet() call goes over the websocket, so its storage spans land in a webSocketMessage
13-
// transaction. Filter for the one carrying our put span — control messages produce their own
14-
// webSocketMessage transactions without storage spans.
15-
const storageTransactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
16-
return (
17-
transactionEvent.transaction === 'webSocketMessage' &&
18-
(transactionEvent.spans ?? []).some(span => span.description === 'durable_object_storage_put')
19-
);
20-
});
18+
// The greet() call goes over the websocket, so its storage spans are children of the `greet` rpc
19+
// span inside a webSocketMessage segment. Control messages produce their own webSocketMessage
20+
// segments without storage spans. Streamed children arrive before their segment, so collect until
21+
// the segment that closes the trace of a `greet` call has arrived.
22+
const storageSpansPromise = collectStreamedSpans(
23+
'cloudflare-agent',
24+
spans =>
25+
spans.some(span => getSpanOp(span) === 'rpc' && span.name === 'greet') &&
26+
spans.some(span => span.is_segment && span.name === 'webSocketMessage'),
27+
);
2128

2229
await page.goto(baseURL!);
2330

2431
await expect(page.getByText('Connected')).toBeVisible();
2532
await page.getByRole('button', { name: 'Call Agent' }).click();
2633
await expect(page.getByText('Hello, World!')).toBeVisible();
2734

28-
const transaction = await transactionPromise;
29-
30-
expect(transaction).toEqual({
31-
contexts: {
32-
trace: {
33-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
34-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
35-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
36-
data: expect.any(Object),
37-
op: 'http.server',
38-
status: 'ok',
39-
origin: 'auto.http.cloudflare',
40-
},
41-
cloud_resource: { 'cloud.provider': 'cloudflare' },
42-
culture: { timezone: expect.any(String) },
43-
runtime: { name: 'cloudflare' },
44-
},
45-
spans: [],
35+
const requestSpan = await requestSpanPromise;
36+
37+
expect(requestSpan).toEqual({
38+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
39+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
40+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
41+
name: 'GET',
4642
start_timestamp: expect.any(Number),
47-
timestamp: expect.any(Number),
48-
transaction: 'GET /agents/my-agent/user-123',
49-
type: 'transaction',
50-
request: {
51-
cookies: {},
52-
headers: expect.any(Object),
53-
method: 'GET',
54-
url: expect.stringContaining('/agents/my-agent/user-123'),
55-
query_string: expect.any(String),
56-
},
57-
transaction_info: { source: 'url' },
58-
platform: 'javascript',
59-
event_id: expect.stringMatching(/[a-f0-9]{32}/),
60-
environment: expect.any(String),
61-
user: {
62-
ip_address: '127.0.0.1',
63-
},
64-
release: expect.any(String),
65-
sdk: {
66-
integrations: expect.any(Array),
67-
name: 'sentry.javascript.cloudflare',
68-
version: expect.any(String),
69-
packages: expect.any(Array),
70-
},
43+
end_timestamp: expect.any(Number),
44+
status: 'ok',
45+
is_segment: true,
46+
attributes: expect.objectContaining({
47+
'sentry.op': { value: 'http.server', type: 'string' },
48+
'sentry.origin': { value: 'auto.http.cloudflare', type: 'string' },
49+
'sentry.segment.name.source': { value: 'url', type: 'string' },
50+
'http.request.method': { value: 'GET', type: 'string' },
51+
'url.path': { value: '/agents/my-agent/user-123', type: 'string' },
52+
'sentry.environment': { value: expect.any(String), type: 'string' },
53+
}),
7154
});
7255

7356
// greet() touches 6 storage keys: 2 user ops + 3 framework-internal keys (cf_, __ps_, /) that
7457
// must be filtered + 1 allowlisted cf_ key. Spans carry no key attribute, so filtering can only
7558
// be verified by count — exactly these 3 storage spans (in execution order) should survive, and
7659
// any framework-internal span leaking through shows up as an extra entry here.
77-
const storageTransaction = await storageTransactionPromise;
60+
const spans = await storageSpansPromise;
61+
const rpcSpan = spans.find(span => getSpanOp(span) === 'rpc' && span.name === 'greet')!;
7862

79-
const storageSpans = (storageTransaction.spans ?? []).filter(
80-
span => span.origin === 'auto.db.cloudflare.durable_object',
81-
);
63+
const storageSpans = spans
64+
.filter(
65+
span =>
66+
span.parent_span_id === rpcSpan.span_id &&
67+
span.attributes['sentry.origin']?.value === 'auto.db.cloudflare.durable_object',
68+
)
69+
.sort((a, b) => a.start_timestamp - b.start_timestamp);
8270

8371
expect(storageSpans).toEqual([
84-
expect.objectContaining({
85-
data: {
86-
'db.operation.name': 'put',
87-
'db.system.name': 'cloudflare.durable_object.storage',
88-
'sentry.op': 'db',
89-
'sentry.origin': 'auto.db.cloudflare.durable_object',
90-
},
91-
description: 'durable_object_storage_put',
92-
op: 'db',
93-
origin: 'auto.db.cloudflare.durable_object',
94-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
95-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
72+
{
73+
name: 'durable_object_storage_put',
74+
attributes: expect.objectContaining({
75+
'db.operation.name': { value: 'put', type: 'string' },
76+
'db.system.name': { value: 'cloudflare.durable_object.storage', type: 'string' },
77+
'sentry.op': { value: 'db', type: 'string' },
78+
'sentry.origin': { value: 'auto.db.cloudflare.durable_object', type: 'string' },
79+
}),
80+
parent_span_id: rpcSpan.span_id,
81+
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
9682
start_timestamp: expect.any(Number),
97-
timestamp: expect.any(Number),
98-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
99-
}),
100-
expect.objectContaining({
101-
data: {
102-
'db.operation.name': 'get',
103-
'db.system.name': 'cloudflare.durable_object.storage',
104-
'sentry.op': 'db',
105-
'sentry.origin': 'auto.db.cloudflare.durable_object',
106-
},
107-
description: 'durable_object_storage_get',
108-
op: 'db',
109-
origin: 'auto.db.cloudflare.durable_object',
110-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
111-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
83+
end_timestamp: expect.any(Number),
84+
status: 'ok',
85+
is_segment: false,
86+
trace_id: rpcSpan.trace_id,
87+
},
88+
{
89+
name: 'durable_object_storage_get',
90+
attributes: expect.objectContaining({
91+
'db.operation.name': { value: 'get', type: 'string' },
92+
'db.system.name': { value: 'cloudflare.durable_object.storage', type: 'string' },
93+
'sentry.op': { value: 'db', type: 'string' },
94+
'sentry.origin': { value: 'auto.db.cloudflare.durable_object', type: 'string' },
95+
}),
96+
parent_span_id: rpcSpan.span_id,
97+
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
11298
start_timestamp: expect.any(Number),
113-
timestamp: expect.any(Number),
114-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
115-
}),
116-
expect.objectContaining({
117-
data: {
118-
'db.operation.name': 'get',
119-
'db.system.name': 'cloudflare.durable_object.storage',
120-
'sentry.op': 'db',
121-
'sentry.origin': 'auto.db.cloudflare.durable_object',
122-
},
123-
description: 'durable_object_storage_get',
124-
op: 'db',
125-
origin: 'auto.db.cloudflare.durable_object',
126-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
127-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
99+
end_timestamp: expect.any(Number),
100+
status: 'ok',
101+
is_segment: false,
102+
trace_id: rpcSpan.trace_id,
103+
},
104+
{
105+
name: 'durable_object_storage_get',
106+
attributes: expect.objectContaining({
107+
'db.operation.name': { value: 'get', type: 'string' },
108+
'db.system.name': { value: 'cloudflare.durable_object.storage', type: 'string' },
109+
'sentry.op': { value: 'db', type: 'string' },
110+
'sentry.origin': { value: 'auto.db.cloudflare.durable_object', type: 'string' },
111+
}),
112+
parent_span_id: rpcSpan.span_id,
113+
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
128114
start_timestamp: expect.any(Number),
129-
timestamp: expect.any(Number),
130-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
131-
}),
115+
end_timestamp: expect.any(Number),
116+
status: 'ok',
117+
is_segment: false,
118+
trace_id: rpcSpan.trace_id,
119+
},
132120
]);
133121
});
134122

135123
test('does not emit db.query spans for the agents runtime `cf_`-prefixed internal tables', async ({
136124
page,
137125
baseURL,
138126
}) => {
139-
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
140-
return (
141-
transactionEvent.transaction === 'GET /agents/my-agent/user-123' &&
142-
transactionEvent.contexts?.trace?.parent_span_id !== undefined
143-
);
144-
});
127+
const spansPromise = collectStreamedSpans('cloudflare-agent', spans =>
128+
spans.some(
129+
span =>
130+
getSpanOp(span) === 'http.server' &&
131+
span.is_segment &&
132+
span.attributes['url.path']?.value === '/agents/my-agent/user-123' &&
133+
span.parent_span_id !== undefined,
134+
),
135+
);
145136

146137
await page.goto(baseURL!);
147138

148139
await expect(page.getByText('Connected')).toBeVisible();
149140
await page.getByRole('button', { name: 'Call Agent' }).click();
150141
await expect(page.getByText('Hello, World!')).toBeVisible();
151142

152-
const transaction = await transactionPromise;
143+
const spans = await spansPromise;
153144

154145
// The agents runtime constantly queries its own `cf_agents_*` / `cf_agent_*` bookkeeping tables.
155146
// These are framework internals and are filtered out by default, so no such span should leak.
156-
const internalTableSpans = (transaction.spans ?? []).filter(
157-
span => span.op === 'db.query' && /\bcf_/.test((span.data?.['db.query.summary'] as string) ?? ''),
147+
const internalTableSpans = spans.filter(
148+
span => getSpanOp(span) === 'db.query' && /\bcf_/.test(String(span.attributes['db.query.summary']?.value ?? '')),
158149
);
159150

160151
expect(internalTableSpans).toEqual([]);
161152
});
162153

163154
test('creates an rpc span named after the @callable() method', async ({ page, baseURL }) => {
164-
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
165-
return (
166-
transactionEvent.transaction === 'webSocketMessage' &&
167-
(transactionEvent.spans ?? []).some(span => span.op === 'rpc' && span.description === 'greet')
168-
);
169-
});
155+
const spansPromise = collectStreamedSpans(
156+
'cloudflare-agent',
157+
spans =>
158+
spans.some(span => getSpanOp(span) === 'rpc' && span.name === 'greet') &&
159+
spans.some(span => span.is_segment && span.name === 'webSocketMessage'),
160+
);
170161

171162
await page.goto(baseURL!);
172163

173164
await expect(page.getByText('Connected')).toBeVisible();
174165
await page.getByRole('button', { name: 'Call Agent' }).click();
175166
await expect(page.getByText('Hello, World!')).toBeVisible();
176167

177-
const transaction = await transactionPromise;
168+
const spans = await spansPromise;
169+
const rpcSpan = spans.find(span => getSpanOp(span) === 'rpc' && span.name === 'greet')!;
178170

179-
const rpcSpans = (transaction.spans ?? []).filter(span => span.op === 'rpc');
171+
const rpcSpans = spans.filter(span => getSpanOp(span) === 'rpc');
180172
expect(rpcSpans).toHaveLength(1);
181173

182-
expect(rpcSpans[0]).toEqual(
183-
expect.objectContaining({
184-
op: 'rpc',
185-
description: 'greet',
186-
origin: 'auto.faas.cloudflare.agents',
187-
data: expect.objectContaining({
188-
'gen_ai.agent.name': 'MyBaseAgent',
189-
}),
190-
}),
191-
);
174+
expect(rpcSpan.attributes['sentry.op']?.value).toBe('rpc');
175+
expect(rpcSpan.attributes['sentry.origin']?.value).toBe('auto.faas.cloudflare.agents');
176+
expect(rpcSpan.attributes['gen_ai.agent.name']?.value).toBe('MyBaseAgent');
192177
});

0 commit comments

Comments
 (0)