@@ -16,13 +16,27 @@ interface GraphQLClientOptions {
1616}
1717
1818/** Standard graphql request shape: https://graphql.org/learn/serving-over-http/#post-request-and-body */
19- interface GraphQLRequestPayload {
19+ interface GraphQLStandardRequest {
2020 query : string ;
2121 operationName ?: string ;
2222 variables ?: Record < string , unknown > ;
2323 extensions ?: Record < string , unknown > ;
2424}
2525
26+ /** Persisted operation request */
27+ interface GraphQLPersistedRequest {
28+ operationName : string ;
29+ variables ?: Record < string , unknown > ;
30+ extensions : {
31+ persistedQuery : {
32+ version : number ;
33+ sha256Hash : string ;
34+ } ;
35+ } & Record < string , unknown > ;
36+ }
37+
38+ type GraphQLRequestPayload = GraphQLStandardRequest | GraphQLPersistedRequest ;
39+
2640interface GraphQLOperation {
2741 operationType ?: string ;
2842 operationName ?: string ;
@@ -33,7 +47,7 @@ const INTEGRATION_NAME = 'GraphQLClient';
3347const _graphqlClientIntegration = ( ( options : GraphQLClientOptions ) => {
3448 return {
3549 name : INTEGRATION_NAME ,
36- setup ( client ) {
50+ setup ( client : Client ) {
3751 _updateSpanWithGraphQLData ( client , options ) ;
3852 _updateBreadcrumbWithGraphQLData ( client , options ) ;
3953 } ,
@@ -70,7 +84,17 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption
7084 if ( graphqlBody ) {
7185 const operationInfo = _getGraphQLOperation ( graphqlBody ) ;
7286 span . updateName ( `${ httpMethod } ${ httpUrl } (${ operationInfo } )` ) ;
73- span . setAttribute ( 'graphql.document' , payload ) ;
87+
88+ // Handle standard requests - always capture the query document
89+ if ( isStandardRequest ( graphqlBody ) ) {
90+ span . setAttribute ( 'graphql.document' , graphqlBody . query ) ;
91+ }
92+
93+ // Handle persisted operations - capture hash for debugging
94+ if ( isPersistedRequest ( graphqlBody ) ) {
95+ span . setAttribute ( 'graphql.persistedQuery.sha256Hash' , graphqlBody . extensions . persistedQuery . sha256Hash ) ;
96+ span . setAttribute ( 'graphql.persistedQuery.version' , graphqlBody . extensions . persistedQuery . version ) ;
97+ }
7498 }
7599 }
76100 } ) ;
@@ -96,8 +120,17 @@ function _updateBreadcrumbWithGraphQLData(client: Client, options: GraphQLClient
96120
97121 if ( ! data . graphql && graphqlBody ) {
98122 const operationInfo = _getGraphQLOperation ( graphqlBody ) ;
99- data [ 'graphql.document' ] = graphqlBody . query ;
123+
100124 data [ 'graphql.operation' ] = operationInfo ;
125+
126+ if ( isStandardRequest ( graphqlBody ) ) {
127+ data [ 'graphql.document' ] = graphqlBody . query ;
128+ }
129+
130+ if ( isPersistedRequest ( graphqlBody ) ) {
131+ data [ 'graphql.persistedQuery.sha256Hash' ] = graphqlBody . extensions . persistedQuery . sha256Hash ;
132+ data [ 'graphql.persistedQuery.version' ] = graphqlBody . extensions . persistedQuery . version ;
133+ }
101134 }
102135 }
103136 }
@@ -106,15 +139,24 @@ function _updateBreadcrumbWithGraphQLData(client: Client, options: GraphQLClient
106139
107140/**
108141 * @param requestBody - GraphQL request
109- * @returns A formatted version of the request: 'TYPE NAME' or 'TYPE'
142+ * @returns A formatted version of the request: 'TYPE NAME' or 'TYPE' or 'persisted NAME'
110143 */
111- function _getGraphQLOperation ( requestBody : GraphQLRequestPayload ) : string {
112- const { query : graphqlQuery , operationName : graphqlOperationName } = requestBody ;
144+ export function _getGraphQLOperation ( requestBody : GraphQLRequestPayload ) : string {
145+ // Handle persisted operations
146+ if ( isPersistedRequest ( requestBody ) ) {
147+ return `persisted ${ requestBody . operationName } ` ;
148+ }
113149
114- const { operationName = graphqlOperationName , operationType } = parseGraphQLQuery ( graphqlQuery ) ;
115- const operationInfo = operationName ? `${ operationType } ${ operationName } ` : `${ operationType } ` ;
150+ // Handle standard GraphQL requests
151+ if ( isStandardRequest ( requestBody ) ) {
152+ const { query : graphqlQuery , operationName : graphqlOperationName } = requestBody ;
153+ const { operationName = graphqlOperationName , operationType } = parseGraphQLQuery ( graphqlQuery ) ;
154+ const operationInfo = operationName ? `${ operationType } ${ operationName } ` : `${ operationType } ` ;
155+ return operationInfo ;
156+ }
116157
117- return operationInfo ;
158+ // Fallback for unknown request types
159+ return 'unknown' ;
118160}
119161
120162/**
@@ -168,27 +210,55 @@ export function parseGraphQLQuery(query: string): GraphQLOperation {
168210 } ;
169211}
170212
213+ /**
214+ * Helper to safely check if a value is a non-null object
215+ */
216+ function isObject ( value : unknown ) : value is Record < string , unknown > {
217+ return typeof value === 'object' && value !== null ;
218+ }
219+
220+ /**
221+ * Type guard to check if a request is a standard GraphQL request
222+ */
223+ function isStandardRequest ( payload : unknown ) : payload is GraphQLStandardRequest {
224+ return isObject ( payload ) && typeof payload . query === 'string' ;
225+ }
226+
227+ /**
228+ * Type guard to check if a request is a persisted operation request
229+ */
230+ function isPersistedRequest ( payload : unknown ) : payload is GraphQLPersistedRequest {
231+ return (
232+ isObject ( payload ) &&
233+ typeof payload . operationName === 'string' &&
234+ isObject ( payload . extensions ) &&
235+ isObject ( payload . extensions . persistedQuery )
236+ ) ;
237+ }
238+
171239/**
172240 * Extract the payload of a request if it's GraphQL.
173241 * Exported for tests only.
174242 * @param payload - A valid JSON string
175243 * @returns A POJO or undefined
176244 */
177245export function getGraphQLRequestPayload ( payload : string ) : GraphQLRequestPayload | undefined {
178- let graphqlBody = undefined ;
179246 try {
180- const requestBody = JSON . parse ( payload ) satisfies GraphQLRequestPayload ;
247+ const requestBody = JSON . parse ( payload ) ;
248+
249+ if ( isStandardRequest ( requestBody ) ) {
250+ return requestBody ;
251+ }
181252
182- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
183- const isGraphQLRequest = ! ! requestBody [ 'query' ] ;
184- if ( isGraphQLRequest ) {
185- graphqlBody = requestBody ;
253+ if ( isPersistedRequest ( requestBody ) ) {
254+ return requestBody ;
186255 }
187- } finally {
188- // Fallback to undefined if payload is an invalid JSON (SyntaxError)
189256
190- /* eslint-disable no-unsafe-finally */
191- return graphqlBody ;
257+ // Not a GraphQL request
258+ return undefined ;
259+ } catch {
260+ // Invalid JSON
261+ return undefined ;
192262 }
193263}
194264
0 commit comments