Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ export class DdSdkReactNative {
const resourceTraceSampleRate =
configuration.rumConfiguration?.resourceTraceSampleRate ||
RUM_DEFAULTS.resourceTraceSampleRate;
const headerCaptureRules =
configuration.rumConfiguration?.headerCaptureRules;
const logEventMapper = configuration.logsConfiguration?.logEventMapper;
const errorEventMapper =
configuration.rumConfiguration?.errorEventMapper;
Expand Down Expand Up @@ -535,10 +537,18 @@ export class DdSdkReactNative {
});
}

if (!trackResources && headerCaptureRules !== undefined) {
InternalLog.log(
'headerCaptureRules is set but trackResources is false. Header capture will be disabled.',
SdkVerbosity.WARN
);
}

if (trackResources) {
DdRumResourceTracking.startTracking({
resourceTraceSampleRate,
firstPartyHosts
firstPartyHosts,
headerCaptureRules
});
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/__tests__/DdSdkReactNative.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ describe('DdSdkReactNative', () => {
match: 'something.fr',
propagatorTypes: ['datadog']
}
]
],
headerCaptureRules: undefined
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"customEndpoint": undefined,
"errorEventMapper": null,
"firstPartyHosts": [],
"headerCaptureRules": undefined,
"initialResourceThreshold": undefined,
"longTaskThresholdMs": 0,
"nativeCrashReportEnabled": false,
Expand Down Expand Up @@ -205,6 +206,7 @@ describe('DdSdkReactNativeConfiguration', () => {
],
},
],
"headerCaptureRules": undefined,
"initialResourceThreshold": 0.123,
"longTaskThresholdMs": 567,
"nativeCrashReportEnabled": true,
Expand Down Expand Up @@ -308,6 +310,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"customEndpoint": undefined,
"errorEventMapper": null,
"firstPartyHosts": [],
"headerCaptureRules": undefined,
"initialResourceThreshold": 0,
"longTaskThresholdMs": false,
"nativeCrashReportEnabled": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('FileBasedConfiguration', () => {
],
},
],
"headerCaptureRules": undefined,
"initialResourceThreshold": 456,
"longTaskThresholdMs": 44,
"nativeCrashReportEnabled": true,
Expand Down Expand Up @@ -167,6 +168,7 @@ describe('FileBasedConfiguration', () => {
],
},
],
"headerCaptureRules": undefined,
"initialResourceThreshold": undefined,
"longTaskThresholdMs": 44,
"nativeCrashReportEnabled": false,
Expand Down Expand Up @@ -231,6 +233,7 @@ describe('FileBasedConfiguration', () => {
"customEndpoint": undefined,
"errorEventMapper": null,
"firstPartyHosts": [],
"headerCaptureRules": undefined,
"initialResourceThreshold": undefined,
"longTaskThresholdMs": 0,
"nativeCrashReportEnabled": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ResourceEventMapper } from '../../rum/eventMappers/resourceEventMa
import type { FirstPartyHost } from '../../rum/types';
import type { LogEventMapper } from '../../types';
import { LOGS_DEFAULTS } from '../features/LogsConfiguration';
import type { HeaderCaptureRule } from '../features/RumConfiguration.type';
import { RUM_DEFAULTS } from '../features/RumConfiguration';
import type { TraceConfiguration } from '../features/TraceConfiguration';

Expand All @@ -24,6 +25,7 @@ export type AutoInstrumentationConfiguration = {
readonly useAccessibilityLabel?: boolean;
readonly actionNameAttribute?: string;
readonly resourceTraceSampleRate?: number;
readonly headerCaptureRules?: 'defaults' | HeaderCaptureRule[];
readonly nativeCrashReportEnabled?: boolean;
readonly nativeLongTaskThresholdMs?: number;
readonly nativeViewTracking?: boolean;
Expand Down Expand Up @@ -55,6 +57,7 @@ export type AutoInstrumentationParameters = {
readonly errorEventMapper: ErrorEventMapper | null;
readonly resourceEventMapper: ResourceEventMapper | null;
readonly firstPartyHosts: FirstPartyHost[];
readonly headerCaptureRules?: 'defaults' | HeaderCaptureRule[];
};
readonly logsConfiguration?: {
readonly logEventMapper: LogEventMapper | null;
Expand Down Expand Up @@ -114,7 +117,8 @@ export const addDefaultValuesToAutoInstrumentationConfiguration = (
RUM_DEFAULTS.nativeViewTracking,
firstPartyHosts:
features.rumConfiguration.firstPartyHosts ||
RUM_DEFAULTS.getFirstPartyHosts()
RUM_DEFAULTS.getFirstPartyHosts(),
headerCaptureRules: features.rumConfiguration.headerCaptureRules
},
logsConfiguration: {
logEventMapper:
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/config/features/RumConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { FirstPartyHost } from '../../rum/types';
import { VitalsUpdateFrequency } from '../types';

import type {
HeaderCaptureRule,
RumConfigurationOptions,
RumConfigurationType
} from './RumConfiguration.type';
Expand Down Expand Up @@ -37,6 +38,10 @@ const DEFAULTS = {
trackInteractions: false,
trackMemoryWarnings: true,
trackNonFatalAnrs: undefined,
headerCaptureRules: undefined as
| 'defaults'
| HeaderCaptureRule[]
| undefined,
trackResources: false,
trackWatchdogTerminations: false,
useAccessibilityLabel: true,
Expand Down Expand Up @@ -112,6 +117,10 @@ export class RumConfiguration implements RumConfigurationType {
// Track non-fatal ANRs enabled
public trackNonFatalAnrs?: boolean = DEFAULTS.trackNonFatalAnrs;

// Header Capture Rules
public headerCaptureRules: 'defaults' | HeaderCaptureRule[] | undefined =
DEFAULTS.headerCaptureRules;

// Track Watchdog Terminations enabled
public trackWatchdogTerminations: boolean =
DEFAULTS.trackWatchdogTerminations;
Expand Down
113 changes: 113 additions & 0 deletions packages/core/src/config/features/RumConfiguration.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,106 @@ export interface RumConfigurationRequired {
trackErrors?: boolean;
}

/**
* Captures a predefined set of caching and content headers from both
* request and response.
*
* Default headers captured:
* - **Response:** cache-control, etag, age, expires, content-type,
* content-encoding, content-length, vary, server-timing, x-cache
* - **Request:** cache-control, content-type
*
* Optionally scoped to specific URLs via `forURLs`.
*/
export type DefaultsRule = {
type: 'defaults';
/**
* URL patterns to scope this rule to. Supports hostname-only
* ('api.example.com') or hostname+path prefix ('api.example.com/v2').
* Omit to match all URLs.
*/
forURLs?: string[];
};

/**
* Captures the specified headers from both request and response.
*
* Use this when the same header names should be captured regardless
* of direction (e.g. 'content-type', 'authorization').
* Optionally scoped to specific URLs via `forURLs`.
*/
export type MatchHeadersRule = {
type: 'matchHeaders';
/**
* Header names to capture from both request and response.
* Preserved as-is; compared case-insensitively at capture time.
*/
headers: string[];
/**
* URL patterns to scope this rule to. Supports hostname-only
* ('api.example.com') or hostname+path prefix ('api.example.com/v2').
* Omit to match all URLs.
*/
forURLs?: string[];
};

/**
* Captures the specified headers from requests only.
*
* Use this when you need to capture request-specific headers
* (e.g. 'authorization', 'x-api-key') without capturing response headers.
* Optionally scoped to specific URLs via `forURLs`.
*/
export type MatchRequestHeadersRule = {
type: 'matchRequestHeaders';
/**
* Request header names to capture.
* Preserved as-is; compared case-insensitively at capture time.
*/
headers: string[];
/**
* URL patterns to scope this rule to. Supports hostname-only
* ('api.example.com') or hostname+path prefix ('api.example.com/v2').
* Omit to match all URLs.
*/
forURLs?: string[];
};

/**
* Captures the specified headers from responses only.
*
* Use this when you need to capture response-specific headers
* (e.g. 'x-request-id', 'x-ratelimit-remaining') without capturing
* request headers.
* Optionally scoped to specific URLs via `forURLs`.
*/
export type MatchResponseHeadersRule = {
type: 'matchResponseHeaders';
/**
* Response header names to capture.
* Preserved as-is; compared case-insensitively at capture time.
*/
headers: string[];
/**
* URL patterns to scope this rule to. Supports hostname-only
* ('api.example.com') or hostname+path prefix ('api.example.com/v2').
* Omit to match all URLs.
*/
forURLs?: string[];
};

/**
* A composable header capture rule.
*
* Discriminated union on the `type` field. Multiple rules can be combined
* in an array -- matching rules are merged additively (union of headers).
*/
export type HeaderCaptureRule =
| DefaultsRule
| MatchHeadersRule
| MatchRequestHeadersRule
| MatchResponseHeadersRule;

/**
* Optional RUM configuration values.
*/
Expand Down Expand Up @@ -71,6 +171,19 @@ export interface RumConfigurationOptions {
*/
firstPartyHosts?: FirstPartyHost[];

/**
* Controls which resource headers the SDK captures on network requests.
*
* - **Omitted** (default): No headers are captured.
* - `'defaults'`: Shortcut equivalent to `[{ type: 'defaults' }]`.
* Captures a predefined set of caching and content headers.
* - `HeaderCaptureRule[]`: An array of composable rules. Multiple rules
* are merged additively -- matching rules contribute their headers.
*
* Requires `trackResources: true` to take effect.
*/
headerCaptureRules?: 'defaults' | HeaderCaptureRule[];

/**
* Initial resource collection threshold in seconds.
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ export {
DdBabelInteractionTracking,
__ddExtractText
};
export type {
HeaderCaptureRule,
DefaultsRule,
MatchHeadersRule,
MatchRequestHeadersRule,
MatchResponseHeadersRule
} from './config/features/RumConfiguration.type';
export type {
Timestamp,
FirstPartyHost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import BigInt from 'big-integer';

import { InternalLog } from '../../../InternalLog';
import type { HeaderCaptureRule } from '../../../config/features/RumConfiguration.type';
import { SdkVerbosity } from '../../../config/types/SdkVerbosity';
import { getGlobalInstance } from '../../../utils/singletonUtils';
import type { FirstPartyHost } from '../../types';

import { DistributedTracingSampling } from './distributedTracing/distributedTracingSampling';
import { firstPartyHostsRegexMapBuilder } from './distributedTracing/firstPartyHosts';
import { compileHeaderCaptureConfig } from './headerCapture/compileHeaderConfig';
import { XHRProxy } from './requestProxy/XHRProxy/XHRProxy';
import type { RequestProxy } from './requestProxy/interfaces/RequestProxy';

Expand Down Expand Up @@ -40,10 +42,12 @@ class RumResourceTracking {
*/
startTracking({
resourceTraceSampleRate,
firstPartyHosts
firstPartyHosts,
headerCaptureRules
}: {
resourceTraceSampleRate: number;
firstPartyHosts: FirstPartyHost[];
headerCaptureRules?: 'defaults' | HeaderCaptureRule[] | undefined;
}): void {
// extra safety to avoid proxying the XHR class twice
if (this._isTracking) {
Expand All @@ -55,11 +59,15 @@ class RumResourceTracking {
}

this._requestProxy = XHRProxy.createWithResourceReporter();
const headerCaptureConfig = compileHeaderCaptureConfig(
headerCaptureRules
);
this._requestProxy.onTrackingStart({
tracingSamplingRate: resourceTraceSampleRate,
firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder(
firstPartyHosts
)
),
headerCaptureConfig
});

InternalLog.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class XMLHttpRequestMock implements XMLHttpRequest {
timeout: number = -1;
upload: XMLHttpRequestUpload = {} as XMLHttpRequestUpload;
withCredentials: boolean = false;
getAllResponseHeaders = jest.fn();
getAllResponseHeaders = jest.fn().mockReturnValue('');
overrideMimeType = jest.fn();
DONE = 4 as const;
HEADERS_RECEIVED = 2 as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type Hostname = { _type: 'Hostname' } & string;
export const NO_MATCH_REGEX = new RegExp('a^');

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
const escapeRegExp = (string: string) => {
export const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
};

Expand Down
Loading
Loading