Skip to content

Commit 023b07e

Browse files
authored
✨ Use optional MatchHeader name to account for default headers (#4825)
1 parent cb98e86 commit 023b07e

6 files changed

Lines changed: 102 additions & 53 deletions

File tree

packages/browser-rum-core/src/boot/rumPublicApi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,9 @@ export interface RumPublicApi extends PublicApi {
586586
failFeatureOperation: (name: string, failureReason: FailureReason, options?: FeatureOperationOptions) => void
587587

588588
/**
589-
* List of default headers used by the {@link RumInitConfiguration.trackResourceHeaders | trackResourceHeaders} option. See configuration example for extending them.
589+
* List of default headers used by the {@link RumInitConfiguration.trackResourceHeaders | trackResourceHeaders} option.
590+
*
591+
* @deprecated You can now omit `name` from a MatchHeader entry to include default headers.
590592
*/
591593
DEFAULT_TRACKED_RESOURCE_HEADERS: typeof DEFAULT_TRACKED_RESOURCE_HEADERS
592594
}

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

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,42 @@ describe('validateAndBuildRumConfiguration', () => {
431431
expect(result).toEqual(DEFAULT_TRACKED_RESOURCE_HEADERS.map((name) => ({ name })))
432432
})
433433

434+
it('accepts a MatchHeader without name for default headers', () => {
435+
const result = validateAndBuildRumConfiguration({
436+
...DEFAULT_INIT_CONFIGURATION,
437+
trackResourceHeaders: [{}],
438+
})!.trackResourceHeaders
439+
440+
expect(result).toEqual([{}])
441+
})
442+
443+
it('preserves location when name is absent', () => {
444+
const result = validateAndBuildRumConfiguration({
445+
...DEFAULT_INIT_CONFIGURATION,
446+
trackResourceHeaders: [{ location: 'response' }],
447+
})!.trackResourceHeaders
448+
449+
expect(result).toEqual([{ location: 'response' }])
450+
})
451+
452+
it('combines default header matcher with custom matchers in array order', () => {
453+
const result = validateAndBuildRumConfiguration({
454+
...DEFAULT_INIT_CONFIGURATION,
455+
trackResourceHeaders: [{}, { name: 'x-custom' }],
456+
})!.trackResourceHeaders
457+
458+
expect(result).toEqual([{}, { name: 'x-custom' }])
459+
})
460+
461+
it('treats undefined name like absent name', () => {
462+
const result = validateAndBuildRumConfiguration({
463+
...DEFAULT_INIT_CONFIGURATION,
464+
trackResourceHeaders: [{ name: undefined }],
465+
})!.trackResourceHeaders
466+
467+
expect(result).toEqual([{}])
468+
})
469+
434470
it('accepts a MatchHeader with only name', () => {
435471
const result = validateAndBuildRumConfiguration({
436472
...DEFAULT_INIT_CONFIGURATION,
@@ -518,21 +554,26 @@ describe('validateAndBuildRumConfiguration', () => {
518554
})!.trackResourceHeaders
519555

520556
expect(result).toEqual([{ name: 'x-valid' }, { name: 'x-also-valid' }])
521-
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
522-
"trackResourceHeaders[1] should be a MatchHeader object with a 'name' property"
523-
)
557+
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[1] should be a MatchHeader object')
524558
})
525559

526-
it('warns and skips item without name', () => {
560+
it('accepts a MatchHeader without name', () => {
527561
const result = validateAndBuildRumConfiguration({
528562
...DEFAULT_INIT_CONFIGURATION,
529-
trackResourceHeaders: [{ url: 'https://example.com' } as any],
563+
trackResourceHeaders: [{ url: 'https://example.com' }],
564+
})!.trackResourceHeaders
565+
566+
expect(result).toEqual([{ url: 'https://example.com' }])
567+
})
568+
569+
it('warns and skips item with invalid name', () => {
570+
const result = validateAndBuildRumConfiguration({
571+
...DEFAULT_INIT_CONFIGURATION,
572+
trackResourceHeaders: [{ name: 42 as any }],
530573
})!.trackResourceHeaders
531574

532575
expect(result).toEqual([])
533-
expect(displayWarnSpy).toHaveBeenCalledOnceWith(
534-
"trackResourceHeaders[0] should be a MatchHeader object with a 'name' property"
535-
)
576+
expect(displayWarnSpy).toHaveBeenCalledOnceWith('trackResourceHeaders[0].name should be a MatchOption')
536577
})
537578

538579
it('warns and skips item with invalid url', () => {

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

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,7 @@ export const DEFAULT_PROPAGATOR_TYPES: PropagatorType[] = ['tracecontext', 'data
2222

2323
/**
2424
* Default list of headers collected on resource events when {@link RumInitConfiguration.trackResourceHeaders | trackResourceHeaders}
25-
* is set to `true`. Re-exported by the `@datadog/browser-rum` and `@datadog/browser-rum-slim` packages, and exposed on the
26-
* `DD_RUM` global object when the SDK is loaded via the CDN, so it can be referenced when building a custom matcher list.
27-
*
28-
* @example NPM
29-
* ```ts
30-
* import { datadogRum, DEFAULT_TRACKED_RESOURCE_HEADERS } from '@datadog/browser-rum'
31-
*
32-
* datadogRum.init({
33-
* // ...
34-
* trackResourceHeaders: [
35-
* ...DEFAULT_TRACKED_RESOURCE_HEADERS.map((name) => ({ name })),
36-
* { name: 'x-request-id' },
37-
* ],
38-
* })
39-
* ```
40-
* @example CDN
41-
* ```ts
42-
* DD_RUM.init({
43-
* // ...
44-
* trackResourceHeaders: [
45-
* ...DD_RUM.DEFAULT_TRACKED_RESOURCE_HEADERS.map((name) => ({ name })),
46-
* { name: 'x-request-id' },
47-
* ],
48-
* })
49-
* ```
25+
* is set to `true`.
5026
*/
5127
export const DEFAULT_TRACKED_RESOURCE_HEADERS = [
5228
'cache-control',
@@ -283,9 +259,9 @@ export interface RumInitConfiguration extends InitConfiguration {
283259
*
284260
* - `true`: collect {@link DEFAULT_TRACKED_RESOURCE_HEADERS} for all URLs, both directions
285261
* - `MatchHeader[]`: each {@link MatchHeader} targets a header name, with optional URL scope
286-
* (`url`), value extraction (`extractor`), and `location`. By default, both request and
287-
* response headers are captured; set `location` to `'request'` or `'response'` to restrict
288-
* to one.
262+
* (`url`), value extraction (`extractor`), and `location`. When `name` is omitted, the matcher
263+
* applies to {@link DEFAULT_TRACKED_RESOURCE_HEADERS}. By default, both request and response
264+
* headers are captured; set `location` to `'request'` or `'response'` to restrict to one.
289265
*
290266
* Headers whose names match a built-in sensitive-data pattern are always dropped, regardless
291267
* of the configured matchers. The pattern blocks headers whose names contain: `token`, `cookie`,
@@ -296,10 +272,10 @@ export interface RumInitConfiguration extends InitConfiguration {
296272
* @defaultValue false (disabled)
297273
* @example
298274
* // Collect default headers plus custom ones for all URLs
299-
* trackResourceHeaders: [
300-
* ...DEFAULT_TRACKED_RESOURCE_HEADERS.map((h) => ({ name: h })),
301-
* { name: 'x-request-id' },
302-
* ]
275+
* trackResourceHeaders: [{}, { name: 'x-request-id' }]
276+
* @example
277+
* // Collect default headers from responses only
278+
* trackResourceHeaders: [{ location: 'response' }]
303279
* @example
304280
* // URL-scoped rule: capture specific response headers only for calls to /api
305281
* trackResourceHeaders: [{ url: /\/api\//, name: 'cache-control', location: 'response' }]
@@ -386,7 +362,7 @@ export interface GraphQlUrlOption {
386362

387363
export interface MatchHeader {
388364
url?: MatchOption
389-
name: MatchOption
365+
name?: MatchOption
390366
extractor?: RegExp
391367
location?: 'request' | 'response' | 'any'
392368
}
@@ -613,8 +589,12 @@ function validateAndBuildTrackResourceHeaders(initConfiguration: RumInitConfigur
613589
const result: MatchHeader[] = []
614590

615591
option.forEach((item, index) => {
616-
if (!isIndexableObject(item) || !isMatchOption(item.name)) {
617-
display.warn(`trackResourceHeaders[${index}] should be a MatchHeader object with a 'name' property`)
592+
if (!isIndexableObject(item)) {
593+
display.warn(`trackResourceHeaders[${index}] should be a MatchHeader object`)
594+
return
595+
}
596+
if (item.name !== undefined && !isMatchOption(item.name)) {
597+
display.warn(`trackResourceHeaders[${index}].name should be a MatchOption`)
618598
return
619599
}
620600
if (item.url !== undefined && !isMatchOption(item.url)) {
@@ -625,14 +605,18 @@ function validateAndBuildTrackResourceHeaders(initConfiguration: RumInitConfigur
625605
display.warn(`trackResourceHeaders[${index}].extractor should be a RegExp`)
626606
return
627607
}
628-
if (item.location !== undefined && !VALID_HEADER_LOCATIONS.includes(item.location)) {
608+
if (
609+
item.location !== undefined &&
610+
(typeof item.location !== 'string' || !VALID_HEADER_LOCATIONS.includes(item.location))
611+
) {
629612
display.warn(`trackResourceHeaders[${index}].location should be 'request', 'response', or 'any'`)
630613
return
631614
}
632615

616+
const { name, ...rest } = item
633617
result.push({
634-
...item,
635-
name: typeof item.name === 'string' ? item.name.toLowerCase() : item.name,
618+
...rest,
619+
...(name !== undefined ? { name: typeof name === 'string' ? name.toLowerCase() : name } : {}),
636620
})
637621
})
638622

packages/browser-rum-core/src/domain/resource/resourceCollection.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,27 @@ describe('resourceCollection', () => {
749749
expect(event.resource.response!.headers!['content-type']).toBeUndefined()
750750
})
751751

752+
it('should collect default headers when name is absent', () => {
753+
setupResourceCollection({ trackResourceHeaders: [{ location: 'response' }] })
754+
755+
notifyRequest({
756+
request: {
757+
type: RequestType.FETCH,
758+
response: new Response('', {
759+
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache', 'X-Other': 'ignored' },
760+
}),
761+
},
762+
})
763+
764+
const event = rawRumEvents[0].rawRumEvent as RawRumResourceEvent
765+
expect(event.resource.response).toEqual({
766+
headers: {
767+
'content-type': 'text/html',
768+
'cache-control': 'no-cache',
769+
},
770+
})
771+
})
772+
752773
describe('forbidden headers', () => {
753774
const forbiddenHeaders = [
754775
'authorization',

packages/browser-rum-core/src/domain/resource/resourceCollection.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
safeTruncate,
1111
setTimeout,
1212
} from '@datadog/browser-core'
13+
import type { MatchOption } from '@datadog/browser-core'
1314
import type { Duration } from '@datadog/js-core/time'
1415
import { elapsed, relativeToClocks, toServerDuration } from '@datadog/js-core/time'
1516
import { combine } from '@datadog/js-core/util'
@@ -19,6 +20,7 @@ import type { RumResourceEventDomainContext } from '../../domainContext.types'
1920
import type { NetworkHeaders, RawRumResourceEvent, ResourceRequest, ResourceResponse } from '../../rawRumEvent.types'
2021
import { RumEventType } from '../../rawRumEvent.types'
2122
import type { MatchHeader, RumConfiguration } from '../configuration'
23+
import { DEFAULT_TRACKED_RESOURCE_HEADERS } from '../configuration'
2224
import { startEventTracker } from '../eventTracker'
2325
import { extractRegexMatch } from '../extractRegexMatch'
2426
import type { LifeCycle, RawRumEventCollectedData } from '../lifeCycle'
@@ -387,6 +389,8 @@ const FORBIDDEN_HEADER_PATTERN =
387389
/(token|cookie|secret|authorization|(api|secret|access|app).?key|(client|connecting|real).?ip|forwarded)/
388390
const MAX_HEADER_COUNT = 100
389391
const MAX_HEADER_VALUE_LENGTH = 128
392+
const defaultHeadersMatchOption: MatchOption = (headerName) =>
393+
(DEFAULT_TRACKED_RESOURCE_HEADERS as readonly string[]).includes(headerName)
390394

391395
function filterHeaders(headers: Headers, matchers: MatchHeader[]): NetworkHeaders | undefined {
392396
const result: NetworkHeaders = {}
@@ -409,7 +413,7 @@ function filterHeaders(headers: Headers, matchers: MatchHeader[]): NetworkHeader
409413
return
410414
}
411415

412-
const matchHeader = matchers.find((m) => matchList([m.name], lowerName))
416+
const matchHeader = matchers.find((m) => matchList([m.name ?? defaultHeadersMatchOption], lowerName))
413417
if (!matchHeader) {
414418
return
415419
}

test/e2e/scenario/rum/resources.scenario.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,10 @@ test.describe('resource headers with trackResourceHeaders', () => {
476476
expect(fetchEvent!.resource.response?.headers?.['cache-control']).toBeUndefined()
477477
})
478478

479-
createTest('collect default and custom headers using DEFAULT_TRACKED_RESOURCE_HEADERS pattern')
479+
createTest('collect default and custom headers using optional name matcher')
480480
.withRum()
481481
.withRumInit((configuration) => {
482-
configuration.trackResourceHeaders = [
483-
...window.DD_RUM!.DEFAULT_TRACKED_RESOURCE_HEADERS.map((name) => ({ name })),
484-
{ name: 'x-request-id' },
485-
]
482+
configuration.trackResourceHeaders = [{}, { name: 'x-request-id' }]
486483
window.DD_RUM!.init(configuration)
487484
})
488485
.run(async ({ intakeRegistry, flushEvents, page }) => {

0 commit comments

Comments
 (0)