Skip to content

Commit d508410

Browse files
authored
⚗️ [RUM-11434] GraphQL support (#3805)
* ✨ Add support for GraphQL URL configuration and metadata extraction * mend * 🎨 prettier * update rum-event format * extract function computeGraphQlData / utilize truncate function from repo / * update path * update path * nit and return definition * remove trim for query * ✨ Add XHR support * Trim body * Shorter function, isNonEmptyArray function, unify naming * Add E2E test * Add a warning for non supported Request object * Add a feature flag for graphql tracking * Rum event format * update e2e test * 🥜 Nitpick, Mapping and logic extraction * 🐛 Missing test and warning for all types * 🐛 Add missing test for warning * 🐛 Improve warning detection + remove unecesseray undefined * 🐛 prettier * 🐛 update e2e expectation value for warning * Remove warning for saving bytes * nitpick : Inline requestBody
1 parent 1498fe5 commit d508410

File tree

13 files changed

+656
-8
lines changed

13 files changed

+656
-8
lines changed

packages/core/src/browser/xhrObservable.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface XhrStartContext extends Omit<XhrOpenContext, 'state'> {
2020
isAborted: boolean
2121
xhr: XMLHttpRequest
2222
handlingStack?: string
23+
body?: unknown
2324
}
2425

2526
export interface XhrCompleteContext extends Omit<XhrStartContext, 'state'> {
@@ -72,7 +73,7 @@ function openXhr({ target: xhr, parameters: [method, url] }: InstrumentedMethodC
7273
}
7374

7475
function sendXhr(
75-
{ target: xhr, handlingStack }: InstrumentedMethodCall<XMLHttpRequest, 'send'>,
76+
{ target: xhr, parameters: [body], handlingStack }: InstrumentedMethodCall<XMLHttpRequest, 'send'>,
7677
configuration: Configuration,
7778
observable: Observable<XhrContext>
7879
) {
@@ -87,6 +88,7 @@ function sendXhr(
8788
startContext.isAborted = false
8889
startContext.xhr = xhr
8990
startContext.handlingStack = handlingStack
91+
startContext.body = body
9092

9193
let hasBeenReported = false
9294

packages/core/src/tools/experimentalFeatures.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export enum ExperimentalFeature {
1818
WRITABLE_RESOURCE_GRAPHQL = 'writable_resource_graphql',
1919
EARLY_REQUEST_COLLECTION = 'early_request_collection',
2020
USE_TREE_WALKER_FOR_ACTION_NAME = 'use_tree_walker_for_action_name',
21+
GRAPHQL_TRACKING = 'graphql_tracking',
2122
FEATURE_OPERATION_VITAL = 'feature_operation_vital',
2223
SHORT_SESSION_INVESTIGATION = 'short_session_investigation',
2324
}

packages/core/src/tools/utils/arrayUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ export function removeItem<T>(array: T[], item: T) {
1010
array.splice(index, 1)
1111
}
1212
}
13+
export function isNonEmptyArray<T>(value: unknown): value is T[] {
14+
return Array.isArray(value) && value.length > 0
15+
}

packages/rum-core/src/domain/configuration/configuration.spec.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,60 @@ describe('validateAndBuildRumConfiguration', () => {
511511
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackFeatureFlagsForEvents should be an array')
512512
})
513513
})
514+
515+
describe('allowedGraphQlUrls', () => {
516+
it('defaults to an empty array', () => {
517+
const configuration = validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!
518+
expect(configuration.allowedGraphQlUrls).toEqual([])
519+
})
520+
521+
it('should accept string URLs', () => {
522+
const configuration = validateAndBuildRumConfiguration({
523+
...DEFAULT_INIT_CONFIGURATION,
524+
allowedGraphQlUrls: ['https://api.example.com/graphql', '/graphql'],
525+
})!
526+
expect(configuration.allowedGraphQlUrls).toEqual([
527+
{ match: 'https://api.example.com/graphql', trackPayload: false },
528+
{ match: '/graphql', trackPayload: false },
529+
])
530+
})
531+
532+
it('should accept MatchOption objects', () => {
533+
const configuration = validateAndBuildRumConfiguration({
534+
...DEFAULT_INIT_CONFIGURATION,
535+
allowedGraphQlUrls: [{ match: /\/graphql$/i }, { match: 'https://api.example.com/graphql' }],
536+
})!
537+
expect(configuration.allowedGraphQlUrls).toEqual([
538+
{ match: /\/graphql$/i, trackPayload: false },
539+
{ match: 'https://api.example.com/graphql', trackPayload: false },
540+
])
541+
})
542+
543+
it('should accept function matchers', () => {
544+
const customMatcher = (url: string) => url.includes('graphql')
545+
const configuration = validateAndBuildRumConfiguration({
546+
...DEFAULT_INIT_CONFIGURATION,
547+
allowedGraphQlUrls: [{ match: customMatcher }],
548+
})!
549+
expect(configuration.allowedGraphQlUrls).toEqual([{ match: customMatcher, trackPayload: false }])
550+
})
551+
552+
it('should accept GraphQL options with trackPayload', () => {
553+
const configuration = validateAndBuildRumConfiguration({
554+
...DEFAULT_INIT_CONFIGURATION,
555+
allowedGraphQlUrls: [{ match: '/graphql', trackPayload: true }],
556+
})!
557+
expect(configuration.allowedGraphQlUrls).toEqual([{ match: '/graphql', trackPayload: true }])
558+
})
559+
560+
it('should reject invalid values', () => {
561+
validateAndBuildRumConfiguration({
562+
...DEFAULT_INIT_CONFIGURATION,
563+
allowedGraphQlUrls: 'not-an-array' as any,
564+
})
565+
expect(displayWarnSpy).toHaveBeenCalledOnceWith('allowedGraphQlUrls should be an array')
566+
})
567+
})
514568
})
515569

516570
describe('serializeRumConfiguration', () => {
@@ -523,6 +577,7 @@ describe('serializeRumConfiguration', () => {
523577
workerUrl: './worker.js',
524578
compressIntakeRequests: true,
525579
allowedTracingUrls: ['foo'],
580+
allowedGraphQlUrls: ['bar'],
526581
traceSampleRate: 50,
527582
traceContextInjection: TraceContextInjection.ALL,
528583
defaultPrivacyLevel: 'allow',
@@ -546,7 +601,12 @@ describe('serializeRumConfiguration', () => {
546601

547602
type MapRumInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
548603
? MapInitConfigurationKey<Key>
549-
: Key extends 'workerUrl' | 'allowedTracingUrls' | 'excludedActivityUrls' | 'remoteConfigurationProxy'
604+
: Key extends
605+
| 'workerUrl'
606+
| 'allowedTracingUrls'
607+
| 'excludedActivityUrls'
608+
| 'remoteConfigurationProxy'
609+
| 'allowedGraphQlUrls'
550610
? `use_${CamelToSnakeCase<Key>}`
551611
: Key extends 'trackLongTasks'
552612
? 'track_long_task' // oops
@@ -557,7 +617,9 @@ describe('serializeRumConfiguration', () => {
557617
// By specifying the type here, we can ensure that serializeConfiguration is returning an
558618
// object containing all expected properties.
559619
const serializedConfiguration: ExtractTelemetryConfiguration<
560-
MapRumInitConfigurationKey<keyof RumInitConfiguration> | 'selected_tracing_propagators'
620+
| MapRumInitConfigurationKey<keyof RumInitConfiguration>
621+
| 'selected_tracing_propagators'
622+
| 'use_track_graph_ql_payload'
561623
> = serializeRumConfiguration(exhaustiveRumInitConfiguration)
562624

563625
expect(serializedConfiguration).toEqual({
@@ -567,6 +629,8 @@ describe('serializeRumConfiguration', () => {
567629
trace_context_injection: TraceContextInjection.ALL,
568630
propagate_trace_baggage: true,
569631
use_allowed_tracing_urls: true,
632+
use_allowed_graph_ql_urls: true,
633+
use_track_graph_ql_payload: false,
570634
selected_tracing_propagators: ['tracecontext', 'datadog'],
571635
use_excluded_activity_urls: true,
572636
track_user_interactions: true,

packages/rum-core/src/domain/configuration/configuration.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
validateAndBuildConfiguration,
1111
isSampleRate,
1212
isNumber,
13+
isNonEmptyArray,
1314
} from '@datadog/browser-core'
1415
import type { RumEventDomainContext } from '../../domainContext.types'
1516
import type { RumEvent } from '../../rumEvent.types'
@@ -220,12 +221,24 @@ export interface RumInitConfiguration extends InitConfiguration {
220221
* @defaultValue 0
221222
*/
222223
profilingSampleRate?: number | undefined
224+
225+
/**
226+
* A list of GraphQL endpoint URLs to track and enrich with GraphQL-specific metadata.
227+
*
228+
* @category Data Collection
229+
*/
230+
allowedGraphQlUrls?: Array<MatchOption | GraphQlUrlOption> | undefined
223231
}
224232

225233
export type HybridInitConfiguration = Omit<RumInitConfiguration, 'applicationId' | 'clientToken'>
226234

227235
export type FeatureFlagsForEvents = 'vital' | 'action' | 'long_task' | 'resource'
228236

237+
export interface GraphQlUrlOption {
238+
match: MatchOption
239+
trackPayload?: boolean
240+
}
241+
229242
export interface RumConfiguration extends Configuration {
230243
// Built from init configuration
231244
actionNameAttribute: string | undefined
@@ -254,6 +267,7 @@ export interface RumConfiguration extends Configuration {
254267
trackFeatureFlagsForEvents: FeatureFlagsForEvents[]
255268
profilingSampleRate: number
256269
propagateTraceBaggage: boolean
270+
allowedGraphQlUrls: GraphQlUrlOption[]
257271
}
258272

259273
export function validateAndBuildRumConfiguration(
@@ -288,6 +302,8 @@ export function validateAndBuildRumConfiguration(
288302
return
289303
}
290304

305+
const allowedGraphQlUrls = validateAndBuildGraphQlOptions(initConfiguration)
306+
291307
const baseConfiguration = validateAndBuildConfiguration(initConfiguration)
292308
if (!baseConfiguration) {
293309
return
@@ -329,6 +345,7 @@ export function validateAndBuildRumConfiguration(
329345
trackFeatureFlagsForEvents: initConfiguration.trackFeatureFlagsForEvents || [],
330346
profilingSampleRate: initConfiguration.profilingSampleRate ?? 0,
331347
propagateTraceBaggage: !!initConfiguration.propagateTraceBaggage,
348+
allowedGraphQlUrls,
332349
...baseConfiguration,
333350
}
334351
}
@@ -372,7 +389,7 @@ function validateAndBuildTracingOptions(initConfiguration: RumInitConfiguration)
372389
function getSelectedTracingPropagators(configuration: RumInitConfiguration): PropagatorType[] {
373390
const usedTracingPropagators = new Set<PropagatorType>()
374391

375-
if (Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0) {
392+
if (isNonEmptyArray(configuration.allowedTracingUrls)) {
376393
configuration.allowedTracingUrls.forEach((option) => {
377394
if (isMatchOption(option)) {
378395
DEFAULT_PROPAGATOR_TYPES.forEach((propagatorType) => usedTracingPropagators.add(propagatorType))
@@ -386,6 +403,47 @@ function getSelectedTracingPropagators(configuration: RumInitConfiguration): Pro
386403
return Array.from(usedTracingPropagators)
387404
}
388405

406+
/**
407+
* Build GraphQL options from configuration
408+
*/
409+
function validateAndBuildGraphQlOptions(initConfiguration: RumInitConfiguration): GraphQlUrlOption[] {
410+
if (!initConfiguration.allowedGraphQlUrls) {
411+
return []
412+
}
413+
414+
if (!Array.isArray(initConfiguration.allowedGraphQlUrls)) {
415+
display.warn('allowedGraphQlUrls should be an array')
416+
return []
417+
}
418+
419+
const graphQlOptions: GraphQlUrlOption[] = []
420+
421+
initConfiguration.allowedGraphQlUrls.forEach((option) => {
422+
if (isMatchOption(option)) {
423+
graphQlOptions.push({ match: option, trackPayload: false })
424+
} else if (option && typeof option === 'object' && 'match' in option && isMatchOption(option.match)) {
425+
graphQlOptions.push({
426+
match: option.match,
427+
trackPayload: !!option.trackPayload,
428+
})
429+
}
430+
})
431+
432+
return graphQlOptions
433+
}
434+
435+
function hasGraphQlPayloadTracking(allowedGraphQlUrls: RumInitConfiguration['allowedGraphQlUrls']): boolean {
436+
return (
437+
isNonEmptyArray(allowedGraphQlUrls) &&
438+
allowedGraphQlUrls.some((option) => {
439+
if (typeof option === 'object' && 'trackPayload' in option) {
440+
return !!option.trackPayload
441+
}
442+
return false
443+
})
444+
)
445+
}
446+
389447
export function serializeRumConfiguration(configuration: RumInitConfiguration) {
390448
const baseSerializedConfiguration = serializeConfiguration(configuration)
391449

@@ -396,13 +454,13 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration) {
396454
trace_context_injection: configuration.traceContextInjection,
397455
propagate_trace_baggage: configuration.propagateTraceBaggage,
398456
action_name_attribute: configuration.actionNameAttribute,
399-
use_allowed_tracing_urls:
400-
Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0,
457+
use_allowed_tracing_urls: isNonEmptyArray(configuration.allowedTracingUrls),
458+
use_allowed_graph_ql_urls: isNonEmptyArray(configuration.allowedGraphQlUrls),
459+
use_track_graph_ql_payload: hasGraphQlPayloadTracking(configuration.allowedGraphQlUrls),
401460
selected_tracing_propagators: getSelectedTracingPropagators(configuration),
402461
default_privacy_level: configuration.defaultPrivacyLevel,
403462
enable_privacy_for_action_name: configuration.enablePrivacyForActionName,
404-
use_excluded_activity_urls:
405-
Array.isArray(configuration.excludedActivityUrls) && configuration.excludedActivityUrls.length > 0,
463+
use_excluded_activity_urls: isNonEmptyArray(configuration.excludedActivityUrls),
406464
use_worker_url: !!configuration.workerUrl,
407465
compress_intake_requests: configuration.compressIntakeRequests,
408466
track_views_manually: configuration.trackViewsManually,

packages/rum-core/src/domain/requestCollection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface RequestCompleteEvent {
5959
error?: Error
6060
isAborted: boolean
6161
handlingStack?: string
62+
body?: unknown
6263
}
6364

6465
let nextRequestIndex = 1
@@ -108,6 +109,7 @@ export function trackXhr(lifeCycle: LifeCycle, configuration: RumConfiguration,
108109
xhr: context.xhr,
109110
isAborted: context.isAborted,
110111
handlingStack: context.handlingStack,
112+
body: context.body,
111113
})
112114
break
113115
}
@@ -153,6 +155,7 @@ export function trackFetch(lifeCycle: LifeCycle, tracer: Tracer) {
153155
input: context.input,
154156
isAborted: context.isAborted,
155157
handlingStack: context.handlingStack,
158+
body: context.init?.body,
156159
})
157160
})
158161
break

0 commit comments

Comments
 (0)