Skip to content

Commit ffa2717

Browse files
[skip ci] Merge branch into staging-38
2 parents 4808594 + d508410 commit ffa2717

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
@@ -255,6 +268,7 @@ export interface RumConfiguration extends Configuration {
255268
trackFeatureFlagsForEvents: FeatureFlagsForEvents[]
256269
profilingSampleRate: number
257270
propagateTraceBaggage: boolean
271+
allowedGraphQlUrls: GraphQlUrlOption[]
258272
}
259273

260274
export function validateAndBuildRumConfiguration(
@@ -289,6 +303,8 @@ export function validateAndBuildRumConfiguration(
289303
return
290304
}
291305

306+
const allowedGraphQlUrls = validateAndBuildGraphQlOptions(initConfiguration)
307+
292308
const baseConfiguration = validateAndBuildConfiguration(initConfiguration)
293309
if (!baseConfiguration) {
294310
return
@@ -331,6 +347,7 @@ export function validateAndBuildRumConfiguration(
331347
trackFeatureFlagsForEvents: initConfiguration.trackFeatureFlagsForEvents || [],
332348
profilingSampleRate: initConfiguration.profilingSampleRate ?? 0,
333349
propagateTraceBaggage: !!initConfiguration.propagateTraceBaggage,
350+
allowedGraphQlUrls,
334351
...baseConfiguration,
335352
}
336353
}
@@ -374,7 +391,7 @@ function validateAndBuildTracingOptions(initConfiguration: RumInitConfiguration)
374391
function getSelectedTracingPropagators(configuration: RumInitConfiguration): PropagatorType[] {
375392
const usedTracingPropagators = new Set<PropagatorType>()
376393

377-
if (Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0) {
394+
if (isNonEmptyArray(configuration.allowedTracingUrls)) {
378395
configuration.allowedTracingUrls.forEach((option) => {
379396
if (isMatchOption(option)) {
380397
DEFAULT_PROPAGATOR_TYPES.forEach((propagatorType) => usedTracingPropagators.add(propagatorType))
@@ -388,6 +405,47 @@ function getSelectedTracingPropagators(configuration: RumInitConfiguration): Pro
388405
return Array.from(usedTracingPropagators)
389406
}
390407

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

@@ -398,13 +456,13 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration) {
398456
trace_context_injection: configuration.traceContextInjection,
399457
propagate_trace_baggage: configuration.propagateTraceBaggage,
400458
action_name_attribute: configuration.actionNameAttribute,
401-
use_allowed_tracing_urls:
402-
Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0,
459+
use_allowed_tracing_urls: isNonEmptyArray(configuration.allowedTracingUrls),
460+
use_allowed_graph_ql_urls: isNonEmptyArray(configuration.allowedGraphQlUrls),
461+
use_track_graph_ql_payload: hasGraphQlPayloadTracking(configuration.allowedGraphQlUrls),
403462
selected_tracing_propagators: getSelectedTracingPropagators(configuration),
404463
default_privacy_level: configuration.defaultPrivacyLevel,
405464
enable_privacy_for_action_name: configuration.enablePrivacyForActionName,
406-
use_excluded_activity_urls:
407-
Array.isArray(configuration.excludedActivityUrls) && configuration.excludedActivityUrls.length > 0,
465+
use_excluded_activity_urls: isNonEmptyArray(configuration.excludedActivityUrls),
408466
use_worker_url: !!configuration.workerUrl,
409467
compress_intake_requests: configuration.compressIntakeRequests,
410468
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)