11import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils' ;
2- import { getBodyString } from '@sentry-internal/replay' ;
2+ import { FetchHint , getBodyString , XhrHint } from '@sentry-internal/replay' ;
33import {
44 SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ,
55 SEMANTIC_ATTRIBUTE_SENTRY_OP ,
66 SEMANTIC_ATTRIBUTE_URL_FULL ,
77 defineIntegration ,
88 spanToJSON ,
99} from '@sentry/core' ;
10- import type { Client , HandlerDataFetch , HandlerDataXhr , IntegrationFn } from '@sentry/types' ;
11- import { getGraphQLRequestPayload , isString , parseGraphQLQuery , stringMatchesSomePattern } from '@sentry/utils' ;
10+ import type { Client , IntegrationFn } from '@sentry/types' ;
11+ import { isString , stringMatchesSomePattern } from '@sentry/utils' ;
1212
1313interface GraphQLClientOptions {
1414 endpoints : Array < string | RegExp > ;
@@ -35,34 +35,37 @@ const _graphqlClientIntegration = ((options: GraphQLClientOptions) => {
3535} ) satisfies IntegrationFn ;
3636
3737function _updateSpanWithGraphQLData ( client : Client , options : GraphQLClientOptions ) : void {
38- client . on ( 'beforeOutgoingRequestSpan' , ( span , handlerData ) => {
38+ client . on ( 'beforeOutgoingRequestSpan' , ( span , hint ) => {
3939 const spanJSON = spanToJSON ( span ) ;
4040
4141 const spanAttributes = spanJSON . data || { } ;
4242 const spanOp = spanAttributes [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] ;
4343
4444 const isHttpClientSpan = spanOp === 'http.client' ;
4545
46- if ( isHttpClientSpan ) {
47- const httpUrl = spanAttributes [ SEMANTIC_ATTRIBUTE_URL_FULL ] || spanAttributes [ 'http.url' ] ;
48- const httpMethod = spanAttributes [ SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ] || spanAttributes [ 'http.method' ] ;
46+ if ( ! isHttpClientSpan ) {
47+ return ;
48+ }
4949
50- if ( ! isString ( httpUrl ) || ! isString ( httpMethod ) ) {
51- return ;
52- }
50+ const httpUrl = spanAttributes [ SEMANTIC_ATTRIBUTE_URL_FULL ] || spanAttributes [ 'http.url' ] ;
51+ const httpMethod = spanAttributes [ SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ] || spanAttributes [ 'http.method' ] ;
5352
54- const { endpoints } = options ;
55- const isTracedGraphqlEndpoint = stringMatchesSomePattern ( httpUrl , endpoints ) ;
56- const payload = _getRequestPayloadXhrOrFetch ( handlerData ) ;
53+ if ( ! isString ( httpUrl ) || ! isString ( httpMethod ) ) {
54+ return ;
55+ }
5756
58- if ( isTracedGraphqlEndpoint && payload ) {
59- const graphqlBody = getGraphQLRequestPayload ( payload ) as GraphQLRequestPayload ;
60- const operationInfo = _getGraphQLOperation ( graphqlBody ) ;
57+ const { endpoints } = options ;
58+ const isTracedGraphqlEndpoint = stringMatchesSomePattern ( httpUrl , endpoints ) ;
59+ const payload = _getRequestPayloadXhrOrFetch ( hint ) ;
6160
62- span . updateName ( `${ httpMethod } ${ httpUrl } (${ operationInfo } )` ) ;
63- span . setAttribute ( 'graphql.document' , payload ) ;
64- }
61+ if ( isTracedGraphqlEndpoint && payload ) {
62+ const graphqlBody = getGraphQLRequestPayload ( payload ) as GraphQLRequestPayload ;
63+ const operationInfo = _getGraphQLOperation ( graphqlBody ) ;
64+
65+ span . updateName ( `${ httpMethod } ${ httpUrl } (${ operationInfo } )` ) ;
66+ span . setAttribute ( 'graphql.document' , payload ) ;
6567 }
68+
6669 } ) ;
6770}
6871
@@ -113,26 +116,95 @@ function _getGraphQLOperation(requestBody: GraphQLRequestPayload): string {
113116}
114117
115118/**
116- * Get the request body/payload based on the shape of the HandlerData
117- * @param handlerData - Xhr or Fetch HandlerData
119+ * Get the request body/payload based on the shape of the hint
120+ * TODO: export for test?
118121 */
119- function _getRequestPayloadXhrOrFetch ( handlerData : HandlerDataXhr | HandlerDataFetch ) : string | undefined {
120- const isXhr = 'xhr' in handlerData ;
121- const isFetch = 'fetchData' in handlerData ;
122+ function _getRequestPayloadXhrOrFetch ( hint : XhrHint | FetchHint ) : string | undefined {
123+ const isXhr = 'xhr' in hint ;
124+ const isFetch = ! isXhr
122125
123126 let body : string | undefined ;
124127
125128 if ( isXhr ) {
126- const sentryXhrData = ( handlerData as HandlerDataXhr ) . xhr [ SENTRY_XHR_DATA_KEY ] ;
129+ const sentryXhrData = hint . xhr [ SENTRY_XHR_DATA_KEY ] ;
127130 body = sentryXhrData && getBodyString ( sentryXhrData . body ) [ 0 ] ;
128131 } else if ( isFetch ) {
129- const sentryFetchData = ( handlerData as HandlerDataFetch ) . fetchData ;
130- body = getBodyString ( sentryFetchData . body ) [ 0 ] ;
132+ const sentryFetchData = parseFetchPayload ( hint . input ) ;
133+ body = getBodyString ( sentryFetchData ) [ 0 ] ;
131134 }
132135
133136 return body ;
134137}
135138
139+ function hasProp < T extends string > ( obj : unknown , prop : T ) : obj is Record < string , string > {
140+ return ! ! obj && typeof obj === 'object' && ! ! ( obj as Record < string , string > ) [ prop ] ;
141+ }
142+
143+ /**
144+ * Parses the fetch arguments to extract the request payload.
145+ * Exported for tests only.
146+ */
147+ export function parseFetchPayload ( fetchArgs : unknown [ ] ) : string | undefined {
148+ if ( fetchArgs . length === 2 ) {
149+ const options = fetchArgs [ 1 ] ;
150+ return hasProp ( options , 'body' ) ? String ( options . body ) : undefined ;
151+ }
152+
153+ const arg = fetchArgs [ 0 ] ;
154+ return hasProp ( arg , 'body' ) ? String ( arg . body ) : undefined ;
155+ }
156+
157+ interface GraphQLOperation {
158+ operationType : string | undefined ;
159+ operationName : string | undefined ;
160+ }
161+
162+ /**
163+ * Extract the name and type of the operation from the GraphQL query.
164+ * @param query
165+ */
166+ export function parseGraphQLQuery ( query : string ) : GraphQLOperation {
167+ const queryRe = / ^ (?: \s * ) ( q u e r y | m u t a t i o n | s u b s c r i p t i o n ) (?: \s * ) ( \w + ) (?: \s * ) [ { ( ] / ;
168+
169+ const matched = query . match ( queryRe ) ;
170+
171+ if ( matched ) {
172+ return {
173+ operationType : matched [ 1 ] ,
174+ operationName : matched [ 2 ] ,
175+ } ;
176+ }
177+ return {
178+ operationType : undefined ,
179+ operationName : undefined ,
180+ } ;
181+ }
182+
183+ /**
184+ * Extract the payload of a request if it's GraphQL.
185+ * Exported for tests only.
186+ * @param payload - A valid JSON string
187+ * @returns A POJO or undefined
188+ */
189+ export function getGraphQLRequestPayload ( payload : string ) : unknown | undefined {
190+ let graphqlBody = undefined ;
191+ try {
192+ const requestBody = JSON . parse ( payload ) ;
193+
194+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
195+ const isGraphQLRequest = ! ! requestBody [ 'query' ] ;
196+
197+ if ( isGraphQLRequest ) {
198+ graphqlBody = requestBody ;
199+ }
200+ } finally {
201+ // Fallback to undefined if payload is an invalid JSON (SyntaxError)
202+
203+ /* eslint-disable no-unsafe-finally */
204+ return graphqlBody ;
205+ }
206+ }
207+
136208/**
137209 * GraphQL Client integration for the browser.
138210 */
0 commit comments