1- import type { Envelope } from '@sentry/core' ;
1+ import type { Envelope , SerializedStreamedSpan } from '@sentry/core' ;
22import { expect , it } from 'vitest' ;
33import { createRunner } from '../../runner' ;
4+ import { getSpansFromEnvelope } from '../../spanUtils' ;
45
5- function envelopeItemType ( envelope : Envelope ) : string | undefined {
6- return envelope [ 1 ] [ 0 ] ?. [ 0 ] ?. type as string | undefined ;
7- }
8-
9- function envelopeItem ( envelope : Envelope ) : Record < string , unknown > {
10- return envelope [ 1 ] [ 0 ] ! [ 1 ] as Record < string , unknown > ;
11- }
12-
13- function findD1Spans ( envelope : Envelope ) : Array < Record < string , unknown > > {
14- if ( envelopeItemType ( envelope ) !== 'transaction' ) return [ ] ;
15- const tx = envelopeItem ( envelope ) ;
16- const spans = ( tx . spans as Array < Record < string , unknown > > ) || [ ] ;
17- return spans . filter ( s => ( s . op as string ) === 'db.query' ) ;
18- }
6+ // `cloudflare.d1.duration` is only an integer when the query happens to take a whole number of
7+ // milliseconds, so the type can't be pinned down.
8+ const NUMBER_ATTRIBUTE = { type : expect . stringMatching ( / ^ (?: i n t e g e r | d o u b l e ) $ / ) , value : expect . any ( Number ) } ;
199
2010it ( 'instruments D1 prepare().all() automatically via env' , async ( { signal } ) => {
2111 const runner = createRunner ( __dirname )
2212 . ignore ( 'event' )
2313 . expect ( ( envelope : Envelope ) => {
24- expect ( envelopeItemType ( envelope ) ) . toBe ( 'transaction' ) ;
25- const d1Spans = findD1Spans ( envelope ) ;
26- expect ( d1Spans . length ) . toBeGreaterThanOrEqual ( 1 ) ;
27-
28- const querySpan = d1Spans . find ( s => s . description === 'SELECT * FROM users WHERE id = ?' ) ;
29- expect ( querySpan ) . toBeDefined ( ) ;
30- expect ( querySpan ) . toEqual ( {
31- data : {
32- 'db.system.name' : 'cloudflare-d1' ,
33- 'db.operation.name' : 'all' ,
34- 'db.query.text' : 'SELECT * FROM users WHERE id = ?' ,
35- 'db.query.summary' : 'SELECT users' ,
36- 'cloudflare.d1.duration' : expect . any ( Number ) ,
37- 'cloudflare.d1.rows_read' : expect . any ( Number ) ,
38- 'cloudflare.d1.rows_written' : expect . any ( Number ) ,
39- 'sentry.op' : 'db.query' ,
40- 'sentry.origin' : 'auto.db.cloudflare.d1' ,
41- } ,
42- description : 'SELECT * FROM users WHERE id = ?' ,
43- op : 'db.query' ,
44- origin : 'auto.db.cloudflare.d1' ,
45- status : 'ok' ,
46- parent_span_id : expect . any ( String ) ,
47- span_id : expect . any ( String ) ,
48- start_timestamp : expect . any ( Number ) ,
49- timestamp : expect . any ( Number ) ,
50- trace_id : expect . any ( String ) ,
51- } ) ;
14+ const spans = getSpansFromEnvelope ( envelope ) ;
15+ const segmentSpan = spans . find ( span => span . is_segment ) ;
16+
17+ // The D1 span is named after its query summary rather than the full query text.
18+ const querySpan = spans . find ( span => span . attributes [ 'db.operation.name' ] ?. value === 'all' ) ;
19+ expect ( querySpan ?. name ) . toBe ( 'SELECT users' ) ;
20+ expect ( querySpan ?. parent_span_id ) . toBe ( segmentSpan ?. span_id ) ;
21+ expect ( querySpan ?. status ) . toBe ( 'ok' ) ;
22+ expect ( querySpan ?. attributes ) . toEqual (
23+ expect . objectContaining ( {
24+ 'sentry.op' : { type : 'string' , value : 'db.query' } ,
25+ 'sentry.origin' : { type : 'string' , value : 'auto.db.cloudflare.d1' } ,
26+ 'db.system.name' : { type : 'string' , value : 'cloudflare-d1' } ,
27+ 'db.operation.name' : { type : 'string' , value : 'all' } ,
28+ 'db.query.text' : { type : 'string' , value : 'SELECT * FROM users WHERE id = ?' } ,
29+ 'db.query.summary' : { type : 'string' , value : 'SELECT users' } ,
30+ 'cloudflare.d1.duration' : NUMBER_ATTRIBUTE ,
31+ 'cloudflare.d1.rows_read' : NUMBER_ATTRIBUTE ,
32+ 'cloudflare.d1.rows_written' : NUMBER_ATTRIBUTE ,
33+ } ) ,
34+ ) ;
5235 } )
5336 . start ( signal ) ;
5437
@@ -58,10 +41,10 @@ it('instruments D1 prepare().all() automatically via env', async ({ signal }) =>
5841
5942it ( 'captures error event when a D1 query references a non-existent table' , async ( { signal } ) => {
6043 const runner = createRunner ( __dirname )
61- . ignore ( 'transaction ' )
44+ . ignore ( 'span ' )
6245 . expect ( ( envelope : Envelope ) => {
63- expect ( envelopeItemType ( envelope ) ) . toBe ( 'event' ) ;
64- const event = envelopeItem ( envelope ) ;
46+ expect ( envelope [ 1 ] [ 0 ] ?. [ 0 ] ?. type ) . toBe ( 'event' ) ;
47+ const event = envelope [ 1 ] [ 0 ] ! [ 1 ] as Record < string , unknown > ;
6548 expect ( event . level ) . toBe ( 'error' ) ;
6649
6750 const values = ( event . exception as { values : Array < Record < string , unknown > > } ) ?. values ;
@@ -102,32 +85,26 @@ it('instruments D1 exec() automatically via env', async ({ signal }) => {
10285 const runner = createRunner ( __dirname )
10386 . ignore ( 'event' )
10487 . expect ( ( envelope : Envelope ) => {
105- expect ( envelopeItemType ( envelope ) ) . toBe ( 'transaction' ) ;
106- const d1Spans = findD1Spans ( envelope ) ;
107-
108- const execSpan = d1Spans . find (
109- s => s . description === 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)' ,
88+ const spans = getSpansFromEnvelope ( envelope ) ;
89+ const segmentSpan = spans . find ( span => span . is_segment ) ;
90+
91+ const execSpan = spans . find ( span => span . attributes [ 'db.operation.name' ] ?. value === 'exec' ) ;
92+ expect ( execSpan ?. name ) . toBe ( 'CREATE TABLE users' ) ;
93+ expect ( execSpan ?. parent_span_id ) . toBe ( segmentSpan ?. span_id ) ;
94+ expect ( execSpan ?. status ) . toBe ( 'ok' ) ;
95+ expect ( execSpan ?. attributes ) . toEqual (
96+ expect . objectContaining ( {
97+ 'sentry.op' : { type : 'string' , value : 'db.query' } ,
98+ 'sentry.origin' : { type : 'string' , value : 'auto.db.cloudflare.d1' } ,
99+ 'db.system.name' : { type : 'string' , value : 'cloudflare-d1' } ,
100+ 'db.operation.name' : { type : 'string' , value : 'exec' } ,
101+ 'db.query.text' : {
102+ type : 'string' ,
103+ value : 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)' ,
104+ } ,
105+ 'db.query.summary' : { type : 'string' , value : 'CREATE TABLE users' } ,
106+ } ) ,
110107 ) ;
111- expect ( execSpan ) . toBeDefined ( ) ;
112- expect ( execSpan ) . toEqual ( {
113- data : {
114- 'db.system.name' : 'cloudflare-d1' ,
115- 'db.operation.name' : 'exec' ,
116- 'db.query.text' : 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)' ,
117- 'db.query.summary' : 'CREATE TABLE users' ,
118- 'sentry.op' : 'db.query' ,
119- 'sentry.origin' : 'auto.db.cloudflare.d1' ,
120- } ,
121- description : 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)' ,
122- op : 'db.query' ,
123- origin : 'auto.db.cloudflare.d1' ,
124- status : 'ok' ,
125- parent_span_id : expect . any ( String ) ,
126- span_id : expect . any ( String ) ,
127- start_timestamp : expect . any ( Number ) ,
128- timestamp : expect . any ( Number ) ,
129- trace_id : expect . any ( String ) ,
130- } ) ;
131108 } )
132109 . start ( signal ) ;
133110
@@ -136,20 +113,30 @@ it('instruments D1 exec() automatically via env', async ({ signal }) => {
136113} ) ;
137114
138115it ( 'instruments D1 withSession().batch() identically to db.batch()' , async ( { signal } ) => {
139- let directBatchSpan : Record < string , unknown > | undefined ;
140- let sessionBatchSpan : Record < string , unknown > | undefined ;
116+ let directBatchSpan : SerializedStreamedSpan | undefined ;
117+ let sessionBatchSpan : SerializedStreamedSpan | undefined ;
141118
142119 const runner = createRunner ( __dirname )
143120 . ignore ( 'event' )
144121 . expect ( ( envelope : Envelope ) => {
145- expect ( envelopeItem ( envelope ) . transaction ) . toBe ( 'GET /batch' ) ;
122+ const spans = getSpansFromEnvelope ( envelope ) ;
123+ // Both routes are raw URLs, so the streamed segment name keeps the method only and the
124+ // request is identified through `url.path`.
125+ expect ( spans . find ( span => span . is_segment ) ?. attributes [ 'url.path' ] ) . toEqual ( {
126+ type : 'string' ,
127+ value : '/batch' ,
128+ } ) ;
146129
147- directBatchSpan = findD1Spans ( envelope ) . find ( s => s . description === 'D1 batch' ) ;
130+ directBatchSpan = spans . find ( span => span . name === 'D1 batch' ) ;
148131 } )
149132 . expect ( ( envelope : Envelope ) => {
150- expect ( envelopeItem ( envelope ) . transaction ) . toBe ( 'GET /with-session/batch' ) ;
133+ const spans = getSpansFromEnvelope ( envelope ) ;
134+ expect ( spans . find ( span => span . is_segment ) ?. attributes [ 'url.path' ] ) . toEqual ( {
135+ type : 'string' ,
136+ value : '/with-session/batch' ,
137+ } ) ;
151138
152- sessionBatchSpan = findD1Spans ( envelope ) . find ( s => s . description === 'D1 batch' ) ;
139+ sessionBatchSpan = spans . find ( span => span . name === 'D1 batch' ) ;
153140 } )
154141 . unordered ( )
155142 . start ( signal ) ;
@@ -161,16 +148,19 @@ it('instruments D1 withSession().batch() identically to db.batch()', async ({ si
161148 expect ( directBatchSpan ) . toBeDefined ( ) ;
162149 expect ( sessionBatchSpan ) . toBeDefined ( ) ;
163150
164- const normalize = ( span : Record < string , unknown > ) : Record < string , unknown > => {
151+ // Ids and timestamps differ between the two requests, everything else must match.
152+ const normalize = ( span : SerializedStreamedSpan ) : Record < string , unknown > => {
165153 const {
166154 span_id : _spanId ,
167155 parent_span_id : _parentSpanId ,
168156 start_timestamp : _start ,
169- timestamp : _end ,
157+ end_timestamp : _end ,
170158 trace_id : _traceId ,
159+ attributes,
171160 ...rest
172161 } = span ;
173- return rest ;
162+ const { 'sentry.segment.id' : _segmentId , ...restAttributes } = attributes ;
163+ return { ...rest , attributes : restAttributes } ;
174164 } ;
175165
176166 expect ( normalize ( sessionBatchSpan ! ) ) . toEqual ( normalize ( directBatchSpan ! ) ) ;
@@ -180,30 +170,25 @@ it('instruments D1 batch() automatically via env', async ({ signal }) => {
180170 const runner = createRunner ( __dirname )
181171 . ignore ( 'event' )
182172 . expect ( ( envelope : Envelope ) => {
183- expect ( envelopeItemType ( envelope ) ) . toBe ( 'transaction' ) ;
184- const d1Spans = findD1Spans ( envelope ) ;
185-
186- const batchSpan = d1Spans . find ( s => s . description === 'D1 batch' ) ;
187- expect ( batchSpan ) . toBeDefined ( ) ;
188- expect ( batchSpan ) . toEqual ( {
189- data : {
190- 'db.system.name' : 'cloudflare-d1' ,
191- 'db.operation.name' : 'batch' ,
192- 'db.query.text' : 'INSERT INTO users (name) VALUES (?)\nINSERT INTO users (name) VALUES (?)' ,
193- 'db.operation.batch.size' : 2 ,
194- 'sentry.op' : 'db.query' ,
195- 'sentry.origin' : 'auto.db.cloudflare.d1' ,
196- } ,
197- description : 'D1 batch' ,
198- op : 'db.query' ,
199- origin : 'auto.db.cloudflare.d1' ,
200- status : 'ok' ,
201- parent_span_id : expect . any ( String ) ,
202- span_id : expect . any ( String ) ,
203- start_timestamp : expect . any ( Number ) ,
204- timestamp : expect . any ( Number ) ,
205- trace_id : expect . any ( String ) ,
206- } ) ;
173+ const spans = getSpansFromEnvelope ( envelope ) ;
174+ const segmentSpan = spans . find ( span => span . is_segment ) ;
175+
176+ const batchSpan = spans . find ( span => span . name === 'D1 batch' ) ;
177+ expect ( batchSpan ?. parent_span_id ) . toBe ( segmentSpan ?. span_id ) ;
178+ expect ( batchSpan ?. status ) . toBe ( 'ok' ) ;
179+ expect ( batchSpan ?. attributes ) . toEqual (
180+ expect . objectContaining ( {
181+ 'sentry.op' : { type : 'string' , value : 'db.query' } ,
182+ 'sentry.origin' : { type : 'string' , value : 'auto.db.cloudflare.d1' } ,
183+ 'db.system.name' : { type : 'string' , value : 'cloudflare-d1' } ,
184+ 'db.operation.name' : { type : 'string' , value : 'batch' } ,
185+ 'db.query.text' : {
186+ type : 'string' ,
187+ value : 'INSERT INTO users (name) VALUES (?)\nINSERT INTO users (name) VALUES (?)' ,
188+ } ,
189+ 'db.operation.batch.size' : { type : 'integer' , value : 2 } ,
190+ } ) ,
191+ ) ;
207192 } )
208193 . start ( signal ) ;
209194
0 commit comments