Skip to content

Commit cbf7b31

Browse files
Merge branch 'staging-37' into beltran.bulbarella/RUM-11247_2
2 parents 5844f86 + 635a7b7 commit cbf7b31

File tree

9 files changed

+493
-3
lines changed

9 files changed

+493
-3
lines changed

packages/core/src/domain/telemetry/telemetryEvent.types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & {
205205
* Whether the allowed tracing urls list is used
206206
*/
207207
use_allowed_tracing_urls?: boolean
208+
/**
209+
* Whether the allowed GraphQL urls list is used
210+
*/
211+
use_allowed_graph_ql_urls?: boolean
212+
/**
213+
* Whether GraphQL payload tracking is used for at least one GraphQL endpoint
214+
*/
215+
use_track_graph_ql_payload?: boolean
208216
/**
209217
* A list of selected tracing propagators
210218
*/
@@ -443,6 +451,18 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & {
443451
* The id of the remote configuration
444452
*/
445453
remote_configuration_id?: string
454+
/**
455+
* Whether a proxy is used for remote configuration
456+
*/
457+
use_remote_configuration_proxy?: boolean
458+
/**
459+
* The percentage of sessions with Profiling enabled
460+
*/
461+
profiling_sample_rate?: number
462+
/**
463+
* Whether trace baggage is propagated to child spans
464+
*/
465+
propagate_trace_baggage?: boolean
446466
[k: string]: unknown
447467
}
448468
[k: string]: unknown

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

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

521575
describe('validateAndBuildRumConfiguration errorStack threading', () => {
@@ -541,6 +595,7 @@ describe('serializeRumConfiguration', () => {
541595
workerUrl: './worker.js',
542596
compressIntakeRequests: true,
543597
allowedTracingUrls: ['foo'],
598+
allowedGraphQlUrls: ['bar'],
544599
traceSampleRate: 50,
545600
traceContextInjection: TraceContextInjection.ALL,
546601
defaultPrivacyLevel: 'allow',
@@ -564,7 +619,7 @@ describe('serializeRumConfiguration', () => {
564619

565620
type MapRumInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
566621
? MapInitConfigurationKey<Key>
567-
: Key extends 'workerUrl' | 'allowedTracingUrls' | 'excludedActivityUrls'
622+
: Key extends 'workerUrl' | 'allowedTracingUrls' | 'excludedActivityUrls' | 'allowedGraphQlUrls'
568623
? `use_${CamelToSnakeCase<Key>}`
569624
: Key extends 'trackLongTasks'
570625
? 'track_long_task' // oops
@@ -579,7 +634,9 @@ describe('serializeRumConfiguration', () => {
579634
// By specifying the type here, we can ensure that serializeConfiguration is returning an
580635
// object containing all expected properties.
581636
const serializedConfiguration: ExtractTelemetryConfiguration<
582-
MapRumInitConfigurationKey<keyof RumInitConfiguration> | 'selected_tracing_propagators'
637+
| MapRumInitConfigurationKey<keyof RumInitConfiguration>
638+
| 'selected_tracing_propagators'
639+
| 'use_track_graph_ql_payload'
583640
> = serializeRumConfiguration(exhaustiveRumInitConfiguration)
584641

585642
expect(serializedConfiguration).toEqual({
@@ -588,6 +645,8 @@ describe('serializeRumConfiguration', () => {
588645
trace_sample_rate: 50,
589646
trace_context_injection: TraceContextInjection.ALL,
590647
use_allowed_tracing_urls: true,
648+
use_allowed_graph_ql_urls: true,
649+
use_track_graph_ql_payload: false,
591650
selected_tracing_propagators: ['tracecontext', 'datadog'],
592651
use_excluded_activity_urls: true,
593652
track_user_interactions: true,

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,24 @@ export interface RumInitConfiguration extends InitConfiguration {
220220
* @defaultValue 0
221221
*/
222222
profilingSampleRate?: number | undefined
223+
224+
/**
225+
* A list of GraphQL endpoint URLs to track and enrich with GraphQL-specific metadata.
226+
*
227+
* @category Data Collection
228+
*/
229+
allowedGraphQlUrls?: Array<MatchOption | GraphQlUrlOption> | undefined
223230
}
224231

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

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

236+
export interface GraphQlUrlOption {
237+
match: MatchOption
238+
trackPayload?: boolean
239+
}
240+
229241
export interface RumConfiguration extends Configuration {
230242
// Built from init configuration
231243
actionNameAttribute: string | undefined
@@ -254,6 +266,7 @@ export interface RumConfiguration extends Configuration {
254266
trackFeatureFlagsForEvents: FeatureFlagsForEvents[]
255267
profilingSampleRate: number
256268
propagateTraceBaggage: boolean
269+
allowedGraphQlUrls: GraphQlUrlOption[]
257270
}
258271

259272
export function validateAndBuildRumConfiguration(
@@ -289,6 +302,8 @@ export function validateAndBuildRumConfiguration(
289302
return
290303
}
291304

305+
const allowedGraphQlUrls = validateAndBuildGraphQlOptions(initConfiguration)
306+
292307
const baseConfiguration = validateAndBuildConfiguration(initConfiguration, errorStack)
293308
if (!baseConfiguration) {
294309
return
@@ -330,6 +345,7 @@ export function validateAndBuildRumConfiguration(
330345
trackFeatureFlagsForEvents: initConfiguration.trackFeatureFlagsForEvents || [],
331346
profilingSampleRate: initConfiguration.profilingSampleRate ?? 0,
332347
propagateTraceBaggage: !!initConfiguration.propagateTraceBaggage,
348+
allowedGraphQlUrls,
333349
...baseConfiguration,
334350
}
335351
}
@@ -387,6 +403,35 @@ function getSelectedTracingPropagators(configuration: RumInitConfiguration): Pro
387403
return Array.from(usedTracingPropagators)
388404
}
389405

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+
390435
export function serializeRumConfiguration(configuration: RumInitConfiguration) {
391436
const baseSerializedConfiguration = serializeConfiguration(configuration)
392437

@@ -398,6 +443,16 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration) {
398443
action_name_attribute: configuration.actionNameAttribute,
399444
use_allowed_tracing_urls:
400445
Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0,
446+
use_allowed_graph_ql_urls:
447+
Array.isArray(configuration.allowedGraphQlUrls) && configuration.allowedGraphQlUrls.length > 0,
448+
use_track_graph_ql_payload:
449+
Array.isArray(configuration.allowedGraphQlUrls) &&
450+
configuration.allowedGraphQlUrls.some((option) => {
451+
if (typeof option === 'object' && 'trackPayload' in option) {
452+
return option.trackPayload === true
453+
}
454+
return false
455+
}),
401456
selected_tracing_propagators: getSelectedTracingPropagators(configuration),
402457
default_privacy_level: configuration.defaultPrivacyLevel,
403458
enable_privacy_for_action_name: configuration.enablePrivacyForActionName,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { mockRumConfiguration } from '../../../test'
2+
import { extractGraphQlMetadata, findGraphQlConfiguration } from './graphql'
3+
4+
describe('GraphQL detection and metadata extraction', () => {
5+
describe('findGraphQlConfiguration', () => {
6+
it('should detect GraphQL requests matching string URLs', () => {
7+
const configuration = mockRumConfiguration({
8+
allowedGraphQlUrls: [
9+
{ match: 'http://localhost/graphql', trackPayload: false },
10+
{ match: 'https://api.example.com/graphql', trackPayload: false },
11+
],
12+
})
13+
14+
expect(findGraphQlConfiguration('https://api.example.com/graphql', configuration)).toBeTruthy()
15+
expect(findGraphQlConfiguration('http://localhost/api', configuration)).toBeUndefined()
16+
})
17+
18+
it('should detect GraphQL requests matching regex patterns', () => {
19+
const configuration = mockRumConfiguration({
20+
allowedGraphQlUrls: [{ match: /\/graphql$/i, trackPayload: false }],
21+
})
22+
23+
expect(findGraphQlConfiguration('/api/graphql', configuration)).toBeTruthy()
24+
expect(findGraphQlConfiguration('/graphql/admin', configuration)).toBeUndefined()
25+
})
26+
27+
it('should detect GraphQL requests matching function matchers', () => {
28+
const configuration = mockRumConfiguration({
29+
allowedGraphQlUrls: [{ match: (url: string) => url.includes('gql'), trackPayload: false }],
30+
})
31+
32+
expect(findGraphQlConfiguration('/api/gql', configuration)).toBeTruthy()
33+
expect(findGraphQlConfiguration('/gql-endpoint', configuration)).toBeTruthy()
34+
expect(findGraphQlConfiguration('/api/rest', configuration)).toBeUndefined()
35+
})
36+
})
37+
38+
describe('extractGraphQlMetadata', () => {
39+
it('should extract query operation type and name', () => {
40+
const requestBody = JSON.stringify({
41+
query: 'query GetUser { user { id name } }',
42+
operationName: 'GetUser',
43+
variables: { id: '123' },
44+
})
45+
46+
const result = extractGraphQlMetadata(requestBody, true)
47+
48+
expect(result).toEqual({
49+
operationType: 'query',
50+
operationName: 'GetUser',
51+
variables: '{"id":"123"}',
52+
payload: 'query GetUser { user { id name } }',
53+
})
54+
})
55+
56+
it('should handle empty variables object', () => {
57+
const requestBody = JSON.stringify({
58+
query: 'query GetUser { user { id name } }',
59+
operationName: 'GetUser',
60+
variables: {},
61+
})
62+
63+
const result = extractGraphQlMetadata(requestBody, true)
64+
65+
expect(result).toEqual({
66+
operationType: 'query',
67+
operationName: 'GetUser',
68+
variables: '{}',
69+
payload: 'query GetUser { user { id name } }',
70+
})
71+
})
72+
73+
it('should handle null variables', () => {
74+
const requestBody = JSON.stringify({
75+
query: 'query GetUser { user { id name } }',
76+
operationName: 'GetUser',
77+
variables: null,
78+
})
79+
80+
const result = extractGraphQlMetadata(requestBody, true)
81+
82+
expect(result).toEqual({
83+
operationType: 'query',
84+
operationName: 'GetUser',
85+
variables: undefined,
86+
payload: 'query GetUser { user { id name } }',
87+
})
88+
})
89+
90+
it('should return undefined for invalid JSON', () => {
91+
const result = extractGraphQlMetadata('not valid json', true)
92+
expect(result).toBeUndefined()
93+
})
94+
95+
it('should return undefined for non-GraphQL request body', () => {
96+
const requestBody = JSON.stringify({ data: 'some data' })
97+
const result = extractGraphQlMetadata(requestBody, true)
98+
expect(result).toBeUndefined()
99+
})
100+
})
101+
102+
describe('payload truncation', () => {
103+
it('should not truncate payload under 32KB', () => {
104+
const shortQuery = 'query GetUser { user { id } }'
105+
const requestBody = JSON.stringify({
106+
query: shortQuery,
107+
})
108+
109+
const result = extractGraphQlMetadata(requestBody, true)
110+
111+
expect(result?.payload).toBe(shortQuery)
112+
})
113+
114+
it('should truncate payload over 32KB', () => {
115+
const longQuery = `query LongQuery { ${'a'.repeat(33000)} }`
116+
const requestBody = JSON.stringify({
117+
query: longQuery,
118+
})
119+
120+
const result = extractGraphQlMetadata(requestBody, true)
121+
122+
expect(result?.payload?.length).toBe(32768 + 3)
123+
expect(result?.payload?.endsWith('...')).toBe(true)
124+
expect(result?.payload?.startsWith('query LongQuery {')).toBe(true)
125+
})
126+
127+
it('should not include payload when trackPayload is false', () => {
128+
const requestBody = JSON.stringify({
129+
query: 'query GetUser { user { id name } }',
130+
operationName: 'GetUser',
131+
variables: { id: '123' },
132+
})
133+
134+
const result = extractGraphQlMetadata(requestBody, false)
135+
136+
expect(result).toEqual({
137+
operationType: 'query',
138+
operationName: 'GetUser',
139+
variables: '{"id":"123"}',
140+
payload: undefined,
141+
})
142+
})
143+
})
144+
})

0 commit comments

Comments
 (0)