@@ -2,64 +2,84 @@ import { afterAll, expect } from 'vitest';
22import { conditionalTest } from '../../../utils' ;
33import { cleanupChildProcesses , createEsmAndCjsTests } from '../../../utils/runner' ;
44
5- // graphql >= 17 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes to
6- // those channels (`subscribeGraphqlDiagnosticChannels`) instead of the vendored OTel patcher. This
7- // suite pins `^17` and asserts the diagnostics-channel path: graphql semconv attributes, redacted
8- // document text, span relationships, and that the legacy OTel path does NOT also fire (no double
9- // instrumentation). graphql 17 requires Node >= 22, so this suite is skipped on older Node.
5+ // GraphQL 17 requires Node >= 22, so this suite is skipped on older Node.
106conditionalTest ( { min : 22 } ) ( 'GraphQL tracing channel Test' , ( ) => {
117 afterAll ( ( ) => {
128 cleanupChildProcesses ( ) ;
139 } ) ;
1410
15- const expectedExecuteSpan = ( description : string , extraData : Record < string , unknown > = { } ) =>
16- expect . objectContaining ( {
17- description,
18- op : 'graphql' ,
19- origin : 'auto.graphql.diagnostic_channel' ,
20- data : expect . objectContaining ( extraData ) ,
21- } ) ;
22-
23- const EXPECTED_TRANSACTION = {
24- // Root span renamed with the (sorted) operation names. useOperationNameForRootSpan defaults to true.
25- transaction : 'Test Transaction (mutation Login, query, query Boom, query GetUser)' ,
26- spans : expect . arrayContaining ( [
27- expect . objectContaining ( { description : 'graphql.parse' , op : 'graphql' } ) ,
28- expect . objectContaining ( { description : 'graphql.validate' , op : 'graphql' } ) ,
29- // anonymous query -> span named after the operation type only
30- expectedExecuteSpan ( 'query' , { 'graphql.operation.type' : 'query' } ) ,
31- expectedExecuteSpan ( 'query GetUser' , {
32- 'graphql.operation.type' : 'query' ,
33- 'graphql.operation.name' : 'GetUser' ,
34- // the inline `42` literal is redacted out of the document
35- 'graphql.document' : 'query GetUser { user(id: *) { name } }' ,
36- } ) ,
37- expectedExecuteSpan ( 'mutation Login' , {
38- 'graphql.operation.type' : 'mutation' ,
39- // the inline email literal must be redacted to `"*"`, so the raw value can never leak
40- 'graphql.document' : 'mutation Login { login(email: "*") }' ,
41- } ) ,
42- ] ) ,
43- } ;
44-
4511 createEsmAndCjsTests (
4612 __dirname ,
4713 'scenario.mjs' ,
4814 'instrument.mjs' ,
4915 ( createTestRunner , test ) => {
5016 test ( 'subscribes to graphql >= 17 diagnostics channels with graphql semconv attributes' , async ( ) => {
51- await createTestRunner ( ) . expect ( { transaction : EXPECTED_TRANSACTION } ) . start ( ) . completed ( ) ;
52- } ) ;
53-
54- test ( 'does not double-instrument: the vendored OTel graphql patcher does not fire on 17' , async ( ) => {
5517 await createTestRunner ( )
5618 . expect ( {
57- transaction : event => {
58- const spans = event . spans || [ ] ;
59- // The vendored OTel path (origin `auto.graphql.graphql`) must be inactive on 17+.
60- expect ( spans . find ( span => span . origin === 'auto.graphql.graphql' ) ) . toBeUndefined ( ) ;
61- // ...while the diagnostics-channel path is active.
62- expect ( spans . find ( span => span . origin === 'auto.graphql.diagnostic_channel' ) ) . toBeDefined ( ) ;
19+ span : container => {
20+ const segment = container . items . find ( span => span . is_segment ) ;
21+ const children = container . items . filter ( span => ! span . is_segment ) ;
22+
23+ expect ( segment ?. name ) . toBe ( 'Test Transaction' ) ;
24+ expect ( segment ?. attributes [ 'sentry.graphql.operation' ] ) . toEqual ( {
25+ value : [ 'query' , 'query GetUser' , 'mutation Login' , 'query Boom' ] ,
26+ type : 'array' ,
27+ } ) ;
28+ const parseSpan = children . find ( span => span . attributes [ 'graphql.processing.type' ] ?. value === 'parse' ) ;
29+ expect ( parseSpan ?. name ) . toBe ( 'GraphQL parse' ) ;
30+ expect ( parseSpan ?. attributes [ 'sentry.op' ] ) . toEqual ( { value : 'graphql' , type : 'string' } ) ;
31+ const validateSpan = children . find (
32+ span => span . attributes [ 'graphql.processing.type' ] ?. value === 'validate' ,
33+ ) ;
34+ expect ( validateSpan ?. name ) . toBe ( 'GraphQL validate' ) ;
35+ expect ( validateSpan ?. attributes [ 'sentry.op' ] ) . toEqual ( { value : 'graphql' , type : 'string' } ) ;
36+ const executeSpan = children . find (
37+ span =>
38+ span . attributes [ 'graphql.processing.type' ] ?. value === 'execute' &&
39+ span . attributes [ 'graphql.operation.name' ] === undefined ,
40+ ) ;
41+ expect ( executeSpan ?. name ) . toBe ( 'GraphQL query' ) ;
42+ expect ( executeSpan ?. attributes [ 'sentry.op' ] ) . toEqual ( { value : 'graphql' , type : 'string' } ) ;
43+ expect ( executeSpan ?. attributes [ 'sentry.origin' ] ) . toEqual ( {
44+ value : 'auto.graphql.diagnostic_channel' ,
45+ type : 'string' ,
46+ } ) ;
47+ expect ( executeSpan ?. attributes [ 'graphql.operation.type' ] ) . toEqual ( { value : 'query' , type : 'string' } ) ;
48+ expect ( executeSpan ?. attributes [ 'graphql.document' ] ) . toEqual ( { value : '{ hello }' , type : 'string' } ) ;
49+ const getUserSpan = children . find (
50+ span =>
51+ span . attributes [ 'graphql.processing.type' ] ?. value === 'execute' &&
52+ span . attributes [ 'graphql.operation.name' ] ?. value === 'GetUser' ,
53+ ) ;
54+ expect ( getUserSpan ?. name ) . toBe ( 'GraphQL query' ) ;
55+ expect ( getUserSpan ?. attributes [ 'sentry.op' ] ) . toEqual ( { value : 'graphql' , type : 'string' } ) ;
56+ expect ( getUserSpan ?. attributes [ 'sentry.origin' ] ) . toEqual ( {
57+ value : 'auto.graphql.diagnostic_channel' ,
58+ type : 'string' ,
59+ } ) ;
60+ expect ( getUserSpan ?. attributes [ 'graphql.operation.type' ] ) . toEqual ( { value : 'query' , type : 'string' } ) ;
61+ expect ( getUserSpan ?. attributes [ 'graphql.operation.name' ] ) . toEqual ( { value : 'GetUser' , type : 'string' } ) ;
62+ expect ( getUserSpan ?. attributes [ 'graphql.document' ] ) . toEqual ( {
63+ value : 'query GetUser { user(id: *) { name } }' ,
64+ type : 'string' ,
65+ } ) ;
66+ const loginSpan = children . find (
67+ span =>
68+ span . attributes [ 'graphql.processing.type' ] ?. value === 'execute' &&
69+ span . attributes [ 'graphql.operation.name' ] ?. value === 'Login' ,
70+ ) ;
71+ expect ( loginSpan ?. name ) . toBe ( 'GraphQL mutation' ) ;
72+ expect ( loginSpan ?. attributes [ 'sentry.op' ] ) . toEqual ( { value : 'graphql' , type : 'string' } ) ;
73+ expect ( loginSpan ?. attributes [ 'sentry.origin' ] ) . toEqual ( {
74+ value : 'auto.graphql.diagnostic_channel' ,
75+ type : 'string' ,
76+ } ) ;
77+ expect ( loginSpan ?. attributes [ 'graphql.operation.name' ] ) . toEqual ( { value : 'Login' , type : 'string' } ) ;
78+ expect ( loginSpan ?. attributes [ 'graphql.operation.type' ] ) . toEqual ( { value : 'mutation' , type : 'string' } ) ;
79+ expect ( loginSpan ?. attributes [ 'graphql.document' ] ) . toEqual ( {
80+ value : 'mutation Login { login(email: "*") }' ,
81+ type : 'string' ,
82+ } ) ;
6383 } ,
6484 } )
6585 . start ( )
@@ -69,14 +89,22 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => {
6989 test ( 'never leaks raw inline literal values into graphql.document' , async ( ) => {
7090 await createTestRunner ( )
7191 . expect ( {
72- transaction : event => {
73- const spans = event . spans || [ ] ;
74- for ( const span of spans ) {
75- const document = span . data ?. [ 'graphql.document' ] ;
76- if ( typeof document === 'string' ) {
77- expect ( document ) . not . toContain ( 'secret@example.com' ) ;
78- }
79- }
92+ span : container => {
93+ const segment = container . items . find ( span => span . is_segment ) ;
94+ expect ( segment ?. name ) . toBe ( 'Test Transaction' ) ;
95+ const spans = container . items . filter ( span => ! span . is_segment ) ;
96+
97+ const loginSpan = spans . find (
98+ span =>
99+ span . attributes [ 'graphql.processing.type' ] ?. value === 'execute' &&
100+ span . attributes [ 'graphql.operation.name' ] ?. value === 'Login' ,
101+ ) ;
102+ expect ( loginSpan ?. attributes [ 'graphql.document' ] ) . toEqual ( {
103+ value : 'mutation Login { login(email: "*") }' ,
104+ type : 'string' ,
105+ } ) ;
106+ const documents = spans . map ( span => span . attributes [ 'graphql.document' ] ?. value ) ;
107+ expect ( documents . join ( '\n' ) ) . not . toContain ( 'secret@example.com' ) ;
80108 } ,
81109 } )
82110 . start ( )
@@ -86,25 +114,39 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => {
86114 test ( 'flags the execute span as errored when a resolver throws' , async ( ) => {
87115 await createTestRunner ( )
88116 . expect ( {
89- transaction : event => {
90- const spans = event . spans || [ ] ;
91- const boomSpan = spans . find ( span => span . description === 'query Boom' ) ;
92- expect ( boomSpan ) . toBeDefined ( ) ;
93- expect ( boomSpan ?. status ) . toBe ( 'internal_error' ) ;
117+ span : container => {
118+ const segment = container . items . find ( span => span . is_segment ) ;
119+ expect ( segment ?. name ) . toBe ( 'Test Transaction' ) ;
120+ const spans = container . items . filter ( span => ! span . is_segment ) ;
121+
122+ const boomSpan = spans . find (
123+ span =>
124+ span . attributes [ 'graphql.processing.type' ] ?. value === 'execute' &&
125+ span . attributes [ 'graphql.operation.name' ] ?. value === 'Boom' ,
126+ ) ;
127+ expect ( boomSpan ?. status ) . toBe ( 'error' ) ;
128+ expect ( boomSpan ?. attributes [ 'sentry.status.message' ] ?. value ) . toBe ( 'internal_error' ) ;
94129 } ,
95130 } )
96131 . start ( )
97132 . completed ( ) ;
98133 } ) ;
99134
100- test ( 'parents the execute span to the surrounding transaction ' , async ( ) => {
135+ test ( 'parents the execute span to the surrounding segment ' , async ( ) => {
101136 await createTestRunner ( )
102137 . expect ( {
103- transaction : event => {
104- const spans = event . spans || [ ] ;
105- const executeSpan = spans . find ( span => span . description === 'query GetUser' ) ;
138+ span : container => {
139+ const segment = container . items . find ( span => span . is_segment ) ;
140+ expect ( segment ?. name ) . toBe ( 'Test Transaction' ) ;
141+ const spans = container . items . filter ( span => ! span . is_segment ) ;
142+
143+ const executeSpan = spans . find (
144+ span =>
145+ span . attributes [ 'graphql.processing.type' ] ?. value === 'execute' &&
146+ span . attributes [ 'graphql.operation.name' ] ?. value === 'GetUser' ,
147+ ) ;
106148 expect ( executeSpan ) . toBeDefined ( ) ;
107- expect ( executeSpan ?. parent_span_id ) . toBe ( event . contexts ?. trace ?. span_id ) ;
149+ expect ( executeSpan ?. parent_span_id ) . toBe ( segment ?. span_id ) ;
108150 } ,
109151 } )
110152 . start ( )
@@ -124,11 +166,14 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => {
124166 test ( 'flags the validate span as errored for an invalid document' , async ( ) => {
125167 await createTestRunner ( )
126168 . expect ( {
127- transaction : event => {
128- const spans = event . spans || [ ] ;
129- const validateSpan = spans . find ( span => span . description === 'graphql.validate' ) ;
130- expect ( validateSpan ) . toBeDefined ( ) ;
131- expect ( validateSpan ?. status ) . toBe ( 'invalid_argument' ) ;
169+ span : container => {
170+ const segment = container . items . find ( span => span . is_segment ) ;
171+ expect ( segment ?. name ) . toBe ( 'Test Transaction' ) ;
172+ const spans = container . items . filter ( span => ! span . is_segment ) ;
173+
174+ const validateSpan = spans . find ( span => span . attributes [ 'graphql.processing.type' ] ?. value === 'validate' ) ;
175+ expect ( validateSpan ?. status ) . toBe ( 'error' ) ;
176+ expect ( validateSpan ?. attributes [ 'sentry.status.message' ] ?. value ) . toBe ( 'invalid_argument' ) ;
132177 } ,
133178 } )
134179 . start ( )
0 commit comments