11import { 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
48test ( '@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 => {
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 ( 'cloudflare-agent' , spans => {
23+ const rpcSpan = spans . find ( span => getSpanOp ( span ) === 'rpc' && span . name === 'greet' ) ;
1624 return (
17- transactionEvent . transaction === 'webSocketMessage' &&
18- ( transactionEvent . spans ?? [ ] ) . some ( span => span . description === 'durable_object_storage_put' )
25+ ! ! rpcSpan &&
26+ spans . some ( span => span . is_segment && span . name === 'webSocketMessage' && span . trace_id === rpcSpan . trace_id )
1927 ) ;
2028 } ) ;
2129
@@ -25,146 +33,141 @@ test('@callable() methods work correctly with Sentry instrumentAgentWithSentry',
2533 await page . getByRole ( 'button' , { name : 'Call Agent' } ) . click ( ) ;
2634 await expect ( page . getByText ( 'Hello, World!' ) ) . toBeVisible ( ) ;
2735
28- const transaction = await transactionPromise ;
29-
30- expect ( transaction ) . toEqual ( {
31- contexts : {
32- trace : {
33- parent_span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
34- span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
35- trace_id : expect . stringMatching ( / [ a - f 0 - 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 : [ ] ,
36+ const requestSpan = await requestSpanPromise ;
37+
38+ expect ( requestSpan ) . toEqual ( {
39+ trace_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 32 } / ) ,
40+ span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
41+ parent_span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
42+ name : 'GET' ,
4643 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 - f 0 - 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- } ,
44+ end_timestamp : expect . any ( Number ) ,
45+ status : 'ok' ,
46+ is_segment : true ,
47+ attributes : expect . objectContaining ( {
48+ 'sentry.op' : { value : 'http.server' , type : 'string' } ,
49+ 'sentry.origin' : { value : 'auto.http.cloudflare' , type : 'string' } ,
50+ 'sentry.segment.name.source' : { value : 'url' , type : 'string' } ,
51+ 'http.request.method' : { value : 'GET' , type : 'string' } ,
52+ 'url.path' : { value : '/agents/my-agent/user-123' , type : 'string' } ,
53+ 'sentry.environment' : { value : expect . any ( String ) , type : 'string' } ,
54+ } ) ,
7155 } ) ;
7256
7357 // greet() touches 6 storage keys: 2 user ops + 3 framework-internal keys (cf_, __ps_, /) that
7458 // must be filtered + 1 allowlisted cf_ key. Spans carry no key attribute, so filtering can only
7559 // be verified by count — exactly these 3 storage spans (in execution order) should survive, and
7660 // any framework-internal span leaking through shows up as an extra entry here.
77- const storageTransaction = await storageTransactionPromise ;
61+ const spans = await storageSpansPromise ;
62+ const rpcSpan = spans . find ( span => getSpanOp ( span ) === 'rpc' && span . name === 'greet' ) ! ;
7863
79- const storageSpans = ( storageTransaction . spans ?? [ ] ) . filter (
80- span => span . origin === 'auto.db.cloudflare.durable_object' ,
81- ) ;
64+ const storageSpans = spans
65+ . filter (
66+ span =>
67+ span . parent_span_id === rpcSpan . span_id &&
68+ span . attributes [ 'sentry.origin' ] ?. value === 'auto.db.cloudflare.durable_object' ,
69+ )
70+ . sort ( ( a , b ) => a . start_timestamp - b . start_timestamp ) ;
8271
8372 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 - f 0 - 9 ] { 16 } / ) ,
95- span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
73+ {
74+ name : 'durable_object_storage_put' ,
75+ attributes : expect . objectContaining ( {
76+ 'db.operation.name' : { value : 'put' , type : 'string' } ,
77+ 'db.system.name' : { value : 'cloudflare.durable_object.storage' , type : 'string' } ,
78+ 'sentry.op' : { value : 'db' , type : 'string' } ,
79+ 'sentry.origin' : { value : 'auto.db.cloudflare.durable_object' , type : 'string' } ,
80+ } ) ,
81+ parent_span_id : rpcSpan . span_id ,
82+ span_id : expect . stringMatching ( / ^ [ a - f 0 - 9 ] { 16 } $ / ) ,
9683 start_timestamp : expect . any ( Number ) ,
97- timestamp : expect . any ( Number ) ,
98- trace_id : expect . stringMatching ( / [ a - f 0 - 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 - f 0 - 9 ] { 16 } / ) ,
111- span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
84+ end_timestamp : expect . any ( Number ) ,
85+ status : 'ok' ,
86+ is_segment : false ,
87+ trace_id : rpcSpan . trace_id ,
88+ } ,
89+ {
90+ name : 'durable_object_storage_get ' ,
91+ attributes : expect . objectContaining ( {
92+ 'db.operation.name ' : { value : 'get' , type : 'string' } ,
93+ 'db.system.name' : { value : 'cloudflare.durable_object.storage' , type : 'string' } ,
94+ 'sentry.op' : { value : 'db' , type : 'string' } ,
95+ 'sentry.origin' : { value : 'auto.db.cloudflare.durable_object' , type : 'string' } ,
96+ } ) ,
97+ parent_span_id : rpcSpan . span_id ,
98+ span_id : expect . stringMatching ( / ^ [ a - f 0 - 9 ] { 16 } $ / ) ,
11299 start_timestamp : expect . any ( Number ) ,
113- timestamp : expect . any ( Number ) ,
114- trace_id : expect . stringMatching ( / [ a - f 0 - 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 - f 0 - 9 ] { 16 } / ) ,
127- span_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 16 } / ) ,
100+ end_timestamp : expect . any ( Number ) ,
101+ status : 'ok' ,
102+ is_segment : false ,
103+ trace_id : rpcSpan . trace_id ,
104+ } ,
105+ {
106+ name : 'durable_object_storage_get ' ,
107+ attributes : expect . objectContaining ( {
108+ 'db.operation.name ' : { value : 'get' , type : 'string' } ,
109+ 'db.system.name' : { value : 'cloudflare.durable_object.storage' , type : 'string' } ,
110+ 'sentry.op' : { value : 'db' , type : 'string' } ,
111+ 'sentry.origin' : { value : 'auto.db.cloudflare.durable_object' , type : 'string' } ,
112+ } ) ,
113+ parent_span_id : rpcSpan . span_id ,
114+ span_id : expect . stringMatching ( / ^ [ a - f 0 - 9 ] { 16 } $ / ) ,
128115 start_timestamp : expect . any ( Number ) ,
129- timestamp : expect . any ( Number ) ,
130- trace_id : expect . stringMatching ( / [ a - f 0 - 9 ] { 32 } / ) ,
131- } ) ,
116+ end_timestamp : expect . any ( Number ) ,
117+ status : 'ok' ,
118+ is_segment : false ,
119+ trace_id : rpcSpan . trace_id ,
120+ } ,
132121 ] ) ;
133122} ) ;
134123
135124test ( 'does not emit db.query spans for the agents runtime `cf_`-prefixed internal tables' , async ( {
136125 page,
137126 baseURL,
138127} ) => {
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- } ) ;
128+ const spansPromise = collectStreamedSpans ( 'cloudflare-agent' , spans =>
129+ spans . some (
130+ span =>
131+ getSpanOp ( span ) === 'http.server' &&
132+ span . is_segment &&
133+ span . attributes [ 'url.path' ] ?. value === '/agents/my-agent/user-123' &&
134+ span . parent_span_id !== undefined ,
135+ ) ,
136+ ) ;
145137
146138 await page . goto ( baseURL ! ) ;
147139
148140 await expect ( page . getByText ( 'Connected' ) ) . toBeVisible ( ) ;
149141 await page . getByRole ( 'button' , { name : 'Call Agent' } ) . click ( ) ;
150142 await expect ( page . getByText ( 'Hello, World!' ) ) . toBeVisible ( ) ;
151143
152- const transaction = await transactionPromise ;
144+ const spans = await spansPromise ;
145+ const requestSpan = spans . find (
146+ span =>
147+ getSpanOp ( span ) === 'http.server' &&
148+ span . is_segment &&
149+ span . attributes [ 'url.path' ] ?. value === '/agents/my-agent/user-123' &&
150+ span . parent_span_id !== undefined ,
151+ ) ! ;
153152
154153 // The agents runtime constantly queries its own `cf_agents_*` / `cf_agent_*` bookkeeping tables.
155154 // 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' && / \b c f _ / . test ( ( span . data ?. [ 'db.query.summary' ] as string ) ?? '' ) ,
155+ const internalTableSpans = spans . filter (
156+ span =>
157+ span . trace_id === requestSpan . trace_id &&
158+ getSpanOp ( span ) === 'db.query' &&
159+ / \b c f _ / . test ( String ( span . attributes [ 'db.query.summary' ] ?. value ?? '' ) ) ,
158160 ) ;
159161
160162 expect ( internalTableSpans ) . toEqual ( [ ] ) ;
161163} ) ;
162164
163165test ( 'creates an rpc span named after the @callable() method' , async ( { page, baseURL } ) => {
164- const transactionPromise = waitForTransaction ( 'cloudflare-agent' , transactionEvent => {
166+ const spansPromise = collectStreamedSpans ( 'cloudflare-agent' , spans => {
167+ const rpcSpan = spans . find ( span => getSpanOp ( span ) === 'rpc' && span . name === 'greet' ) ;
165168 return (
166- transactionEvent . transaction === 'webSocketMessage' &&
167- ( transactionEvent . spans ?? [ ] ) . some ( span => span . op === 'rpc ' && span . description === 'greet' )
169+ ! ! rpcSpan &&
170+ spans . some ( span => span . is_segment && span . name === 'webSocketMessage ' && span . trace_id === rpcSpan . trace_id )
168171 ) ;
169172 } ) ;
170173
@@ -174,19 +177,13 @@ test('creates an rpc span named after the @callable() method', async ({ page, ba
174177 await page . getByRole ( 'button' , { name : 'Call Agent' } ) . click ( ) ;
175178 await expect ( page . getByText ( 'Hello, World!' ) ) . toBeVisible ( ) ;
176179
177- const transaction = await transactionPromise ;
180+ const spans = await spansPromise ;
181+ const rpcSpan = spans . find ( span => getSpanOp ( span ) === 'rpc' && span . name === 'greet' ) ! ;
178182
179- const rpcSpans = ( transaction . spans ?? [ ] ) . filter ( span => span . op === 'rpc' ) ;
183+ const rpcSpans = spans . filter ( span => getSpanOp ( span ) === 'rpc' && span . trace_id === rpcSpan . trace_id ) ;
180184 expect ( rpcSpans ) . toHaveLength ( 1 ) ;
181185
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- ) ;
186+ expect ( rpcSpan . attributes [ 'sentry.op' ] ?. value ) . toBe ( 'rpc' ) ;
187+ expect ( rpcSpan . attributes [ 'sentry.origin' ] ?. value ) . toBe ( 'auto.faas.cloudflare.agents' ) ;
188+ expect ( rpcSpan . attributes [ 'gen_ai.agent.name' ] ?. value ) . toBe ( 'MyBaseAgent' ) ;
192189} ) ;
0 commit comments