Skip to content

Commit 8e3ff81

Browse files
authored
[RUM-15273] ✨ Allow more granularity on trackResourceHeaders (#4421)
1 parent dad72af commit 8e3ff81

16 files changed

Lines changed: 668 additions & 200 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"build:apps": "node scripts/build/build-test-apps.ts",
1818
"build:docs:json": "typedoc --logLevel Verbose --json ./generated-docs.json",
1919
"build:docs:html": "typedoc --out ./generated-docs",
20+
"serve:docs": "yarn build:docs:html && npx http-server ./generated-docs -p 8080 -o",
2021
"pack": "yarn workspaces foreach --all --parallel --topological --include '@datadog/*' exec yarn pack",
2122
"format": "prettier --check .",
2223
"lint": "NODE_OPTIONS='--max-old-space-size=4096' eslint .",
@@ -42,8 +43,7 @@
4243
"json-schemas:sync": "scripts/cli update_submodule && yarn json-schemas:generate",
4344
"json-schemas:generate": "scripts/cli build_json2type && node scripts/generate-schema-types.ts",
4445
"size": "node scripts/show-bundle-size.ts",
45-
"woke": "scripts/cli woke",
46-
"docs:serve": "typedoc && npx http-server ./generated-docs -p 8080 -o"
46+
"woke": "scripts/cli woke"
4747
},
4848
"devDependencies": {
4949
"@eslint/js": "9.39.3",

packages/core/src/tools/matchOption.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ describe('matchList', () => {
2828
expect(matchList(list, 'http://my.domain.com/action', true)).toBe(true)
2929
})
3030

31+
it('should match consistently when regexp has global flag', () => {
32+
const list = [/foo/g]
33+
expect(matchList(list, 'foobar')).toBe(true)
34+
expect(matchList(list, 'foobar')).toBe(true)
35+
expect(matchList(list, 'foobar')).toBe(true)
36+
})
37+
3138
it('should catch error from provided function', () => {
3239
spyOn(display, 'error')
3340
const list = [

packages/core/src/tools/matchOption.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export function matchList(list: MatchOption[], value: string, useStartsWith = fa
1919
if (typeof item === 'function') {
2020
return item(value)
2121
} else if (item instanceof RegExp) {
22+
// Prevent stateful matching
23+
item.lastIndex = 0
2224
return item.test(value)
2325
} else if (typeof item === 'string') {
2426
return useStartsWith ? value.startsWith(item) : item === value

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,60 @@ describe('rum assembly', () => {
265265
})
266266
})
267267

268+
describe('resource headers on Resource events', () => {
269+
it('should allow modification of resource request and response headers', () => {
270+
const { lifeCycle, serverRumEvents } = setupAssemblyTestWithDefaults({
271+
partialConfiguration: {
272+
beforeSend: (event) => {
273+
event.resource!.request!.headers!['x-request-id'] = 'REDACTED'
274+
event.resource!.response!.headers!['x-powered-by'] = 'REDACTED'
275+
},
276+
},
277+
})
278+
279+
notifyRawRumEvent(lifeCycle, {
280+
rawRumEvent: createRawRumEvent(RumEventType.RESOURCE, {
281+
resource: {
282+
request: { headers: { 'x-request-id': 'abc-123', 'content-type': 'application/json' } },
283+
response: { headers: { 'x-powered-by': 'Express', 'content-type': 'text/html' } },
284+
},
285+
}),
286+
})
287+
288+
const resource = (serverRumEvents[0] as RumResourceEvent).resource
289+
expect(resource.request!.headers!['x-request-id']).toBe('REDACTED')
290+
expect(resource.request!.headers!['content-type']).toBe('application/json')
291+
expect(resource.response!.headers!['x-powered-by']).toBe('REDACTED')
292+
expect(resource.response!.headers!['content-type']).toBe('text/html')
293+
})
294+
295+
it('should allow deletion of resource request and response headers', () => {
296+
const { lifeCycle, serverRumEvents } = setupAssemblyTestWithDefaults({
297+
partialConfiguration: {
298+
beforeSend: (event) => {
299+
delete event.resource!.request!.headers!['x-request-id']
300+
delete event.resource!.response!.headers!['x-powered-by']
301+
},
302+
},
303+
})
304+
305+
notifyRawRumEvent(lifeCycle, {
306+
rawRumEvent: createRawRumEvent(RumEventType.RESOURCE, {
307+
resource: {
308+
request: { headers: { 'x-request-id': 'abc-123', 'content-type': 'application/json' } },
309+
response: { headers: { 'x-powered-by': 'Express', 'content-type': 'text/html' } },
310+
},
311+
}),
312+
})
313+
314+
const resource = (serverRumEvents[0] as RumResourceEvent).resource
315+
expect(resource.request!.headers!['x-request-id']).toBeUndefined()
316+
expect(resource.request!.headers!['content-type']).toBe('application/json')
317+
expect(resource.response!.headers!['x-powered-by']).toBeUndefined()
318+
expect(resource.response!.headers!['content-type']).toBe('text/html')
319+
})
320+
})
321+
268322
it('should reject modification of field not sensitive, context or customer provided', () => {
269323
const { lifeCycle, serverRumEvents } = setupAssemblyTestWithDefaults({
270324
partialConfiguration: {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export function startRumAssembly(
6262
[RumEventType.RESOURCE]: {
6363
'resource.url': 'string',
6464
'resource.graphql.variables': 'string',
65+
'resource.request.headers': 'object',
66+
'resource.response.headers': 'object',
6567
...USER_CUSTOMIZABLE_FIELD_PATHS,
6668
...VIEW_MODIFIABLE_FIELD_PATHS,
6769
...ROOT_MODIFIABLE_FIELD_PATHS,

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

Lines changed: 144 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -412,71 +412,164 @@ describe('validateAndBuildRumConfiguration', () => {
412412
})
413413

414414
describe('trackResourceHeaders', () => {
415-
it('defaults to empty array', () => {
416-
expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackResourceHeaders).toEqual([])
417-
})
415+
describe('disabled', () => {
416+
it('defaults to empty array', () => {
417+
expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackResourceHeaders).toEqual([])
418+
})
418419

419-
it('returns default headers when set to true', () => {
420-
expect(
421-
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: true })!
422-
.trackResourceHeaders
423-
).toEqual(DEFAULT_TRACKED_RESOURCE_HEADERS)
420+
it('returns empty array when set to false', () => {
421+
expect(
422+
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: false })!
423+
.trackResourceHeaders
424+
).toEqual([])
425+
})
424426
})
425427

426-
it('returns empty array when set to false', () => {
427-
expect(
428-
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: false })!
429-
.trackResourceHeaders
430-
).toEqual([])
431-
})
428+
describe('enabled with regular usage', () => {
429+
it('returns default headers as MatchHeader[] when set to true', () => {
430+
const result = validateAndBuildRumConfiguration({
431+
...DEFAULT_INIT_CONFIGURATION,
432+
trackResourceHeaders: true,
433+
})!.trackResourceHeaders
432434

433-
it('merges user matchers with defaults when an array is provided', () => {
434-
const result = validateAndBuildRumConfiguration({
435-
...DEFAULT_INIT_CONFIGURATION,
436-
trackResourceHeaders: ['x-custom-header'],
437-
})!.trackResourceHeaders
435+
expect(result).toEqual(DEFAULT_TRACKED_RESOURCE_HEADERS.map((name) => ({ name })))
436+
})
438437

439-
expect(result).toEqual([...DEFAULT_TRACKED_RESOURCE_HEADERS, 'x-custom-header'])
440-
})
438+
it('accepts a MatchHeader with only name', () => {
439+
const result = validateAndBuildRumConfiguration({
440+
...DEFAULT_INIT_CONFIGURATION,
441+
trackResourceHeaders: [{ name: 'x-custom' }],
442+
})!.trackResourceHeaders
441443

442-
it('lowercases string matchers from user input', () => {
443-
const result = validateAndBuildRumConfiguration({
444-
...DEFAULT_INIT_CONFIGURATION,
445-
trackResourceHeaders: ['X-Custom-Header'],
446-
})!.trackResourceHeaders
444+
expect(result).toEqual([{ name: 'x-custom' }])
445+
})
447446

448-
expect(result).toContain('x-custom-header')
449-
})
447+
it('lowercases string name', () => {
448+
const result = validateAndBuildRumConfiguration({
449+
...DEFAULT_INIT_CONFIGURATION,
450+
trackResourceHeaders: [{ name: 'X-Custom-Header' }],
451+
})!.trackResourceHeaders
450452

451-
it('accepts RegExp and function matchers', () => {
452-
const regexpMatcher = /x-custom-.*/i
453-
const fnMatcher = (header: string) => header.startsWith('x-')
453+
expect(result[0].name).toBe('x-custom-header')
454+
})
454455

455-
const result = validateAndBuildRumConfiguration({
456-
...DEFAULT_INIT_CONFIGURATION,
457-
trackResourceHeaders: [regexpMatcher, fnMatcher],
458-
})!.trackResourceHeaders
456+
it('accepts RegExp and function name matchers', () => {
457+
const regexpMatcher = /x-custom-.*/i
458+
const fnMatcher = (header: string) => header.startsWith('x-')
459459

460-
expect(result).toContain(regexpMatcher)
461-
expect(result).toContain(fnMatcher)
462-
})
460+
const result = validateAndBuildRumConfiguration({
461+
...DEFAULT_INIT_CONFIGURATION,
462+
trackResourceHeaders: [{ name: regexpMatcher }, { name: fnMatcher }],
463+
})!.trackResourceHeaders
463464

464-
it('warns and returns empty array for invalid value', () => {
465-
expect(
466-
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: 42 as any })!
467-
.trackResourceHeaders
468-
).toEqual([])
469-
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders should be true or an array of MatchOption')
465+
expect(result).toEqual([{ name: regexpMatcher }, { name: fnMatcher }])
466+
})
467+
468+
it('preserves url', () => {
469+
const result = validateAndBuildRumConfiguration({
470+
...DEFAULT_INIT_CONFIGURATION,
471+
trackResourceHeaders: [{ url: /api/, name: 'x-id' }],
472+
})!.trackResourceHeaders
473+
474+
expect(result).toEqual([{ url: /api/, name: 'x-id' }])
475+
})
476+
477+
it('preserves location', () => {
478+
const result = validateAndBuildRumConfiguration({
479+
...DEFAULT_INIT_CONFIGURATION,
480+
trackResourceHeaders: [{ name: 'x-id', location: 'response' }],
481+
})!.trackResourceHeaders
482+
483+
expect(result).toEqual([{ name: 'x-id', location: 'response' }])
484+
})
485+
486+
it('preserves extractor', () => {
487+
const extractor = /max-age=(\d+)/
488+
const result = validateAndBuildRumConfiguration({
489+
...DEFAULT_INIT_CONFIGURATION,
490+
trackResourceHeaders: [{ name: 'cache-control', extractor }],
491+
})!.trackResourceHeaders
492+
493+
expect(result).toEqual([{ name: 'cache-control', extractor }])
494+
})
470495
})
471496

472-
it('warns and filters invalid items in array', () => {
473-
const result = validateAndBuildRumConfiguration({
474-
...DEFAULT_INIT_CONFIGURATION,
475-
trackResourceHeaders: ['valid-header', 42 as any, 'another-valid'] as any,
476-
})!.trackResourceHeaders
497+
describe('enabled with incorrect usage', () => {
498+
it('warns and returns empty array for invalid value', () => {
499+
expect(
500+
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: 42 as any })!
501+
.trackResourceHeaders
502+
).toEqual([])
503+
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
504+
'trackResourceHeaders should be true or an array of MatchHeader'
505+
)
506+
})
507+
508+
it('warns and returns empty array when set to an empty array', () => {
509+
expect(
510+
validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackResourceHeaders: [] })!
511+
.trackResourceHeaders
512+
).toEqual([])
513+
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
514+
'trackResourceHeaders is an empty array, no headers will be captured'
515+
)
516+
})
517+
518+
it('keeps valid items and skips invalid ones', () => {
519+
const result = validateAndBuildRumConfiguration({
520+
...DEFAULT_INIT_CONFIGURATION,
521+
trackResourceHeaders: [{ name: 'x-valid' }, 42 as any, { name: 'x-also-valid' }],
522+
})!.trackResourceHeaders
523+
524+
expect(result).toEqual([{ name: 'x-valid' }, { name: 'x-also-valid' }])
525+
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
526+
"trackResourceHeaders[1] should be a MatchHeader object with a 'name' property"
527+
)
528+
})
529+
530+
it('warns and skips item without name', () => {
531+
const result = validateAndBuildRumConfiguration({
532+
...DEFAULT_INIT_CONFIGURATION,
533+
trackResourceHeaders: [{ url: 'https://example.com' } as any],
534+
})!.trackResourceHeaders
477535

478-
expect(result).toEqual([...DEFAULT_TRACKED_RESOURCE_HEADERS, 'valid-header', 'another-valid'])
479-
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[1] should be a string, RegExp, or function')
536+
expect(result).toEqual([])
537+
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
538+
"trackResourceHeaders[0] should be a MatchHeader object with a 'name' property"
539+
)
540+
})
541+
542+
it('warns and skips item with invalid url', () => {
543+
const result = validateAndBuildRumConfiguration({
544+
...DEFAULT_INIT_CONFIGURATION,
545+
trackResourceHeaders: [{ name: 'x-foo', url: 42 as any }],
546+
})!.trackResourceHeaders
547+
548+
expect(result).toEqual([])
549+
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[0].url should be a MatchOption')
550+
})
551+
552+
it('warns and skips item with invalid location', () => {
553+
const result = validateAndBuildRumConfiguration({
554+
...DEFAULT_INIT_CONFIGURATION,
555+
trackResourceHeaders: [{ name: 'x-foo', location: 'invalid' as any }],
556+
})!.trackResourceHeaders
557+
558+
expect(result).toEqual([])
559+
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
560+
"trackResourceHeaders[0].location should be 'request', 'response', or 'any'"
561+
)
562+
})
563+
564+
it('warns and skips item with invalid extractor', () => {
565+
const result = validateAndBuildRumConfiguration({
566+
...DEFAULT_INIT_CONFIGURATION,
567+
trackResourceHeaders: [{ name: 'x-foo', extractor: 'bad' as any }],
568+
})!.trackResourceHeaders
569+
570+
expect(result).toEqual([])
571+
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[0].extractor should be a RegExp')
572+
})
480573
})
481574
})
482575

0 commit comments

Comments
 (0)