Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9e68beb
✨ Add support for GraphQL URL configuration and metadata extraction
rgaignault Aug 28, 2025
70f376f
mend
rgaignault Aug 28, 2025
2318ebb
🎨 prettier
rgaignault Aug 28, 2025
079d919
update rum-event format
rgaignault Sep 1, 2025
686c276
extract function computeGraphQlData / utilize truncate function fro…
rgaignault Sep 2, 2025
681805d
Merge branch 'main' into romanG/graph-ql-tracking
rgaignault Sep 2, 2025
de96eb7
update path
rgaignault Sep 2, 2025
f396a31
update path
rgaignault Sep 2, 2025
a85a3d0
nit and return definition
rgaignault Sep 8, 2025
68cec67
remove trim for query
rgaignault Sep 8, 2025
80c4168
✨ Add XHR support
rgaignault Sep 8, 2025
39fdc09
Merge branch 'main' into romanG/graph-ql-tracking
rgaignault Sep 8, 2025
2768024
Trim body
rgaignault Sep 10, 2025
0e6b39f
Shorter function, isNonEmptyArray function, unify naming
rgaignault Sep 12, 2025
f83758f
Add E2E test
rgaignault Sep 15, 2025
ec12f1c
Add a warning for non supported Request object
rgaignault Sep 15, 2025
5bcdde4
Add a feature flag for graphql tracking
rgaignault Sep 15, 2025
9b53cf1
Rum event format
rgaignault Sep 15, 2025
d8ff71f
Merge branch 'main' into romanG/graph-ql-tracking
rgaignault Sep 15, 2025
e35f041
update e2e test
rgaignault Sep 15, 2025
81ad1e3
🥜 Nitpick, Mapping and logic extraction
rgaignault Sep 16, 2025
4590f6b
🐛 Missing test and warning for all types
rgaignault Sep 16, 2025
bde794f
🐛 Add missing test for warning
rgaignault Sep 16, 2025
ccb914e
🐛 Improve warning detection + remove unecesseray undefined
rgaignault Sep 16, 2025
93b9f6a
🐛 prettier
rgaignault Sep 16, 2025
7996ecb
🐛 update e2e expectation value for warning
rgaignault Sep 16, 2025
d389992
Remove warning for saving bytes
rgaignault Sep 16, 2025
d09d9fc
nitpick : Inline requestBody
rgaignault Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/src/browser/xhrObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface XhrStartContext extends Omit<XhrOpenContext, 'state'> {
isAborted: boolean
xhr: XMLHttpRequest
handlingStack?: string
body?: unknown
}

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

function sendXhr(
{ target: xhr, handlingStack }: InstrumentedMethodCall<XMLHttpRequest, 'send'>,
{ target: xhr, parameters: [body], handlingStack }: InstrumentedMethodCall<XMLHttpRequest, 'send'>,
configuration: Configuration,
observable: Observable<XhrContext>
) {
Expand All @@ -87,6 +88,7 @@ function sendXhr(
startContext.isAborted = false
startContext.xhr = xhr
startContext.handlingStack = handlingStack
startContext.body = body

let hasBeenReported = false

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum ExperimentalFeature {
WRITABLE_RESOURCE_GRAPHQL = 'writable_resource_graphql',
EARLY_REQUEST_COLLECTION = 'early_request_collection',
USE_TREE_WALKER_FOR_ACTION_NAME = 'use_tree_walker_for_action_name',
GRAPHQL_TRACKING = 'graphql_tracking',
FEATURE_OPERATION_VITAL = 'feature_operation_vital',
SHORT_SESSION_INVESTIGATION = 'short_session_investigation',
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/tools/utils/arrayUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ export function removeItem<T>(array: T[], item: T) {
array.splice(index, 1)
}
}
export function isNonEmptyArray<T>(value: unknown): value is T[] {
return Array.isArray(value) && value.length > 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,60 @@ describe('validateAndBuildRumConfiguration', () => {
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackFeatureFlagsForEvents should be an array')
})
})

describe('allowedGraphQlUrls', () => {
it('defaults to an empty array', () => {
const configuration = validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!
expect(configuration.allowedGraphQlUrls).toEqual([])
})

it('should accept string URLs', () => {
const configuration = validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedGraphQlUrls: ['https://api.example.com/graphql', '/graphql'],
})!
expect(configuration.allowedGraphQlUrls).toEqual([
{ match: 'https://api.example.com/graphql', trackPayload: false },
{ match: '/graphql', trackPayload: false },
])
})

it('should accept MatchOption objects', () => {
const configuration = validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedGraphQlUrls: [{ match: /\/graphql$/i }, { match: 'https://api.example.com/graphql' }],
})!
expect(configuration.allowedGraphQlUrls).toEqual([
{ match: /\/graphql$/i, trackPayload: false },
{ match: 'https://api.example.com/graphql', trackPayload: false },
])
})

it('should accept function matchers', () => {
const customMatcher = (url: string) => url.includes('graphql')
const configuration = validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedGraphQlUrls: [{ match: customMatcher }],
})!
expect(configuration.allowedGraphQlUrls).toEqual([{ match: customMatcher, trackPayload: false }])
})

it('should accept GraphQL options with trackPayload', () => {
const configuration = validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedGraphQlUrls: [{ match: '/graphql', trackPayload: true }],
})!
expect(configuration.allowedGraphQlUrls).toEqual([{ match: '/graphql', trackPayload: true }])
})

it('should reject invalid values', () => {
validateAndBuildRumConfiguration({
...DEFAULT_INIT_CONFIGURATION,
allowedGraphQlUrls: 'not-an-array' as any,
})
expect(displayWarnSpy).toHaveBeenCalledOnceWith('allowedGraphQlUrls should be an array')
})
})
})

describe('serializeRumConfiguration', () => {
Expand All @@ -523,6 +577,7 @@ describe('serializeRumConfiguration', () => {
workerUrl: './worker.js',
compressIntakeRequests: true,
allowedTracingUrls: ['foo'],
allowedGraphQlUrls: ['bar'],
traceSampleRate: 50,
traceContextInjection: TraceContextInjection.ALL,
defaultPrivacyLevel: 'allow',
Expand All @@ -546,7 +601,12 @@ describe('serializeRumConfiguration', () => {

type MapRumInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
? MapInitConfigurationKey<Key>
: Key extends 'workerUrl' | 'allowedTracingUrls' | 'excludedActivityUrls' | 'remoteConfigurationProxy'
: Key extends
| 'workerUrl'
| 'allowedTracingUrls'
| 'excludedActivityUrls'
| 'remoteConfigurationProxy'
| 'allowedGraphQlUrls'
? `use_${CamelToSnakeCase<Key>}`
: Key extends 'trackLongTasks'
? 'track_long_task' // oops
Expand All @@ -557,7 +617,9 @@ describe('serializeRumConfiguration', () => {
// By specifying the type here, we can ensure that serializeConfiguration is returning an
// object containing all expected properties.
const serializedConfiguration: ExtractTelemetryConfiguration<
MapRumInitConfigurationKey<keyof RumInitConfiguration> | 'selected_tracing_propagators'
| MapRumInitConfigurationKey<keyof RumInitConfiguration>
| 'selected_tracing_propagators'
| 'use_track_graph_ql_payload'
> = serializeRumConfiguration(exhaustiveRumInitConfiguration)

expect(serializedConfiguration).toEqual({
Expand All @@ -567,6 +629,8 @@ describe('serializeRumConfiguration', () => {
trace_context_injection: TraceContextInjection.ALL,
propagate_trace_baggage: true,
use_allowed_tracing_urls: true,
use_allowed_graph_ql_urls: true,
use_track_graph_ql_payload: false,
selected_tracing_propagators: ['tracecontext', 'datadog'],
use_excluded_activity_urls: true,
track_user_interactions: true,
Expand Down
68 changes: 63 additions & 5 deletions packages/rum-core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
validateAndBuildConfiguration,
isSampleRate,
isNumber,
isNonEmptyArray,
} from '@datadog/browser-core'
import type { RumEventDomainContext } from '../../domainContext.types'
import type { RumEvent } from '../../rumEvent.types'
Expand Down Expand Up @@ -220,12 +221,24 @@ export interface RumInitConfiguration extends InitConfiguration {
* @defaultValue 0
*/
profilingSampleRate?: number | undefined

/**
* A list of GraphQL endpoint URLs to track and enrich with GraphQL-specific metadata.
*
* @category Data Collection
*/
allowedGraphQlUrls?: Array<MatchOption | GraphQlUrlOption> | undefined
}

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

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

export interface GraphQlUrlOption {
match: MatchOption
trackPayload?: boolean
}

export interface RumConfiguration extends Configuration {
// Built from init configuration
actionNameAttribute: string | undefined
Expand Down Expand Up @@ -254,6 +267,7 @@ export interface RumConfiguration extends Configuration {
trackFeatureFlagsForEvents: FeatureFlagsForEvents[]
profilingSampleRate: number
propagateTraceBaggage: boolean
allowedGraphQlUrls: GraphQlUrlOption[]
}

export function validateAndBuildRumConfiguration(
Expand Down Expand Up @@ -288,6 +302,8 @@ export function validateAndBuildRumConfiguration(
return
}

const allowedGraphQlUrls = validateAndBuildGraphQlOptions(initConfiguration)

const baseConfiguration = validateAndBuildConfiguration(initConfiguration)
if (!baseConfiguration) {
return
Expand Down Expand Up @@ -329,6 +345,7 @@ export function validateAndBuildRumConfiguration(
trackFeatureFlagsForEvents: initConfiguration.trackFeatureFlagsForEvents || [],
profilingSampleRate: initConfiguration.profilingSampleRate ?? 0,
propagateTraceBaggage: !!initConfiguration.propagateTraceBaggage,
allowedGraphQlUrls,
...baseConfiguration,
}
}
Expand Down Expand Up @@ -372,7 +389,7 @@ function validateAndBuildTracingOptions(initConfiguration: RumInitConfiguration)
function getSelectedTracingPropagators(configuration: RumInitConfiguration): PropagatorType[] {
const usedTracingPropagators = new Set<PropagatorType>()

if (Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0) {
if (isNonEmptyArray(configuration.allowedTracingUrls)) {
configuration.allowedTracingUrls.forEach((option) => {
if (isMatchOption(option)) {
DEFAULT_PROPAGATOR_TYPES.forEach((propagatorType) => usedTracingPropagators.add(propagatorType))
Expand All @@ -386,6 +403,47 @@ function getSelectedTracingPropagators(configuration: RumInitConfiguration): Pro
return Array.from(usedTracingPropagators)
}

/**
* Build GraphQL options from configuration
*/
function validateAndBuildGraphQlOptions(initConfiguration: RumInitConfiguration): GraphQlUrlOption[] {
if (!initConfiguration.allowedGraphQlUrls) {
return []
}

if (!Array.isArray(initConfiguration.allowedGraphQlUrls)) {
display.warn('allowedGraphQlUrls should be an array')
return []
}

const graphQlOptions: GraphQlUrlOption[] = []

initConfiguration.allowedGraphQlUrls.forEach((option) => {
if (isMatchOption(option)) {
graphQlOptions.push({ match: option, trackPayload: false })
} else if (option && typeof option === 'object' && 'match' in option && isMatchOption(option.match)) {
graphQlOptions.push({
match: option.match,
trackPayload: !!option.trackPayload,
})
}
})

return graphQlOptions
}

function hasGraphQlPayloadTracking(allowedGraphQlUrls: RumInitConfiguration['allowedGraphQlUrls']): boolean {
return (
isNonEmptyArray(allowedGraphQlUrls) &&
allowedGraphQlUrls.some((option) => {
if (typeof option === 'object' && 'trackPayload' in option) {
return !!option.trackPayload
}
return false
})
)
}

export function serializeRumConfiguration(configuration: RumInitConfiguration) {
const baseSerializedConfiguration = serializeConfiguration(configuration)

Expand All @@ -396,13 +454,13 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration) {
trace_context_injection: configuration.traceContextInjection,
propagate_trace_baggage: configuration.propagateTraceBaggage,
action_name_attribute: configuration.actionNameAttribute,
use_allowed_tracing_urls:
Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0,
use_allowed_tracing_urls: isNonEmptyArray(configuration.allowedTracingUrls),
use_allowed_graph_ql_urls: isNonEmptyArray(configuration.allowedGraphQlUrls),
use_track_graph_ql_payload: hasGraphQlPayloadTracking(configuration.allowedGraphQlUrls),
selected_tracing_propagators: getSelectedTracingPropagators(configuration),
default_privacy_level: configuration.defaultPrivacyLevel,
enable_privacy_for_action_name: configuration.enablePrivacyForActionName,
use_excluded_activity_urls:
Array.isArray(configuration.excludedActivityUrls) && configuration.excludedActivityUrls.length > 0,
use_excluded_activity_urls: isNonEmptyArray(configuration.excludedActivityUrls),
use_worker_url: !!configuration.workerUrl,
compress_intake_requests: configuration.compressIntakeRequests,
track_views_manually: configuration.trackViewsManually,
Expand Down
3 changes: 3 additions & 0 deletions packages/rum-core/src/domain/requestCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface RequestCompleteEvent {
error?: Error
isAborted: boolean
handlingStack?: string
body?: unknown
}

let nextRequestIndex = 1
Expand Down Expand Up @@ -108,6 +109,7 @@ export function trackXhr(lifeCycle: LifeCycle, configuration: RumConfiguration,
xhr: context.xhr,
isAborted: context.isAborted,
handlingStack: context.handlingStack,
body: context.body,
})
break
}
Expand Down Expand Up @@ -153,6 +155,7 @@ export function trackFetch(lifeCycle: LifeCycle, tracer: Tracer) {
input: context.input,
isAborted: context.isAborted,
handlingStack: context.handlingStack,
body: context.init?.body,
})
})
break
Expand Down
Loading