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' ;
33import {
44 SDK_VERSION ,
55 SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT ,
@@ -14,213 +14,112 @@ import {
1414 SENTRY_TRACE_LIFECYCLE ,
1515} from '@sentry/conventions/attributes' ;
1616import { 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 ( / ^ [ \d a - 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 ( / ^ (?: i n t e g e r | d o u b l e ) $ / ) , 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 ( / ^ [ \d a - 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 ( / ^ (?: i n t e g e r | d o u b l e ) $ / ) , 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} ) ;
0 commit comments