Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1545b55
feat: first implementation of http headers capture
marco-saia-datadog Feb 24, 2026
3c7953e
pr-review(tests): improved unit tests and fixed lint issues
marco-saia-datadog Jun 8, 2026
41e2771
chore(docs): update header capture readme
marco-saia-datadog Jun 8, 2026
3878412
fix(pr, docs): code doc improvements
marco-saia-datadog Jun 8, 2026
74cf4d1
feat(header-capture): discard headers on status == 0 requests
marco-saia-datadog Jun 8, 2026
80dd1f9
Potential fix for pull request finding
marco-saia-datadog Jun 8, 2026
512a5a3
fix(pr, header-capture): bytes length enforcing + lint fixes
marco-saia-datadog Jun 9, 2026
48cf474
chore(header-capture): add debug internal logs
marco-saia-datadog Jun 9, 2026
9226604
chore(pr, minor): remove duplicate logic and comment improvement
marco-saia-datadog Jun 9, 2026
b7149cd
fix(pr, header-capture): lowered complexity of enforceSizeLimits
marco-saia-datadog Jun 9, 2026
1efd1bd
fix(pr, header-capture): improved accuracy of utf8ByteLength
marco-saia-datadog Jun 9, 2026
d904f21
feat(pr, header-capture): minor improvement to null check
marco-saia-datadog Jun 9, 2026
3c60eef
Minor null check improvement
marco-saia-datadog Jun 9, 2026
e1f97c2
feat(pr, header-capture): trackResourceHeaders parameter
marco-saia-datadog Jul 8, 2026
519dbcf
feat(header-capture): use a single headerCaptureRules option
marco-saia-datadog Oct 26, 2026
3837e82
feat(header-capture): capture headers on Fetch resources
marco-saia-datadog Oct 26, 2026
cfb89d6
feat(header-capture): support headerCaptureRules in file-based config…
marco-saia-datadog Oct 26, 2026
d841489
fix(header-capture): address review feedback
marco-saia-datadog Oct 26, 2026
1c5da83
fix(header-capture): address second round of review feedback
marco-saia-datadog Oct 26, 2026
1f6045d
fix(header-capture): address previously missed review issues
marco-saia-datadog Oct 26, 2026
76d3ef3
fix(header-capture): exclude header processing from XHR timings
marco-saia-datadog Oct 26, 2026
2f62560
fix(header-capture): harden URL scope matching and sensitive headers
marco-saia-datadog Oct 26, 2026
eba139a
fix(header-capture): strip tabs and newlines before URL scope matching
marco-saia-datadog Oct 26, 2026
aa10a1c
fix(header-capture): match root scopes and encoded hostnames
marco-saia-datadog Oct 26, 2026
ab1c073
fix(header-capture): canonicalize equivalent URLs and filter before s…
marco-saia-datadog Oct 26, 2026
b2f1344
chore: remove accidentally committed .vscode/settings.json changes
marco-saia-datadog Oct 26, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,5 @@ packages/react-native-babel-plugin/svg-map.json

# Session Replay assets
packages/react-native-session-replay/assets/*

.planning
3 changes: 2 additions & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export default function App() {

`trackFetchResources` tracks calls made through the Expo-installed global
`fetch`. Direct imports retained from `expo/fetch` are not intercepted. XHR
tracking remains enabled for clients such as axios.
tracking remains enabled for clients such as axios. Resource header capture
(`headerCaptureRules`) applies to both XHR and Fetch resources.

GraphQL error extraction (via `DatadogLink({ trackErrors: true })`) for
requests made through `expo/fetch` requires Expo SDK 56 or later, as older
Expand Down
119 changes: 119 additions & 0 deletions packages/core/datadog-configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,125 @@
"description": "The app hang threshold in seconds for non-fatal app hangs on iOS.",
"type": "number"
},
"headerCaptureRules": {
"description": "Controls which HTTP request and response headers are captured on RUM resource events. Omit to disable header capture (default). Use `\"defaults\"` to capture a predefined set of caching and content headers on all URLs, or an array of composable rules. Sensitive headers (cookies, authorization, tokens, ...) and SDK tracing headers are never captured. Only takes effect when resource tracking is enabled.",
"oneOf": [
{
"const": "defaults"
},
{
"type": "array",
"items": {
"oneOf": [
{
"description": "Captures the predefined set of caching and content headers from requests and responses.",
"type": "object",
"properties": {
"type": {
"const": "defaults"
},
"forURLs": {
"description": "URL patterns to scope this rule to. Supports hostname-only (`api.example.com`) or hostname + path prefix (`api.example.com/v2`). Omit (or use `[\"*\"]`) to match all URLs.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"description": "Captures the given headers from both requests and responses.",
"type": "object",
"properties": {
"type": {
"const": "matchHeaders"
},
"headers": {
"description": "Header names to capture. Compared case-insensitively; the casing provided here is used in the reported attributes.",
"type": "array",
"items": {
"type": "string"
}
},
"forURLs": {
"description": "URL patterns to scope this rule to. Supports hostname-only (`api.example.com`) or hostname + path prefix (`api.example.com/v2`). Omit (or use `[\"*\"]`) to match all URLs.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"type",
"headers"
],
"additionalProperties": false
},
{
"description": "Captures the given headers from requests only.",
"type": "object",
"properties": {
"type": {
"const": "matchRequestHeaders"
},
"headers": {
"description": "Header names to capture. Compared case-insensitively; the casing provided here is used in the reported attributes.",
"type": "array",
"items": {
"type": "string"
}
},
"forURLs": {
"description": "URL patterns to scope this rule to. Supports hostname-only (`api.example.com`) or hostname + path prefix (`api.example.com/v2`). Omit (or use `[\"*\"]`) to match all URLs.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"type",
"headers"
],
"additionalProperties": false
},
{
"description": "Captures the given headers from responses only.",
"type": "object",
"properties": {
"type": {
"const": "matchResponseHeaders"
},
"headers": {
"description": "Header names to capture. Compared case-insensitively; the casing provided here is used in the reported attributes.",
"type": "array",
"items": {
"type": "string"
}
},
"forURLs": {
"description": "URL patterns to scope this rule to. Supports hostname-only (`api.example.com`) or hostname + path prefix (`api.example.com/v2`). Omit (or use `[\"*\"]`) to match all URLs.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"type",
"headers"
],
"additionalProperties": false
}
]
}
}
]
},
"initialResourceThreshold": {
"description": "The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS).",
"type": "number"
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,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 @@ -553,11 +555,19 @@ export class DdSdkReactNative {
});
}

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

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

Expand Down
123 changes: 122 additions & 1 deletion packages/core/src/__tests__/DdSdkReactNative.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { version as reactNativeVersion } from 'react-native/package.json';
import { NativeModules } from 'react-native';

import { DdSdkReactNative } from '../DdSdkReactNative';
import { InternalLog } from '../InternalLog';
import type { DdSdkNativeConfiguration } from '../config/features/CoreConfigurationNative';
import { CoreConfiguration } from '../config/features/CoreConfiguration';
import { LogsConfiguration } from '../config/features/LogsConfiguration';
Expand Down Expand Up @@ -813,10 +814,130 @@ describe('DdSdkReactNative', () => {
propagatorTypes: ['datadog']
}
],
trackFetchResources: true
trackFetchResources: true,
headerCaptureRules: undefined
Comment thread
marco-saia-datadog marked this conversation as resolved.
});
});

it('does not enable header capture when headerCaptureRules is omitted', async () => {
// GIVEN
const fakeAppId = '1';
const fakeClientToken = '2';
const fakeEnvName = 'env';
const configuration = new CoreConfiguration(
fakeClientToken,
fakeEnvName
);
configuration.rumConfiguration = new RumConfiguration(
fakeAppId,
false,
true
);

NativeModules.DdSdk.initialize.mockResolvedValue(null);

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
expect(DdRumResourceTracking.startTracking).toHaveBeenCalledWith(
expect.objectContaining({
headerCaptureRules: undefined
})
);
});

it('enables header capture with the default headerCaptureRules preset', async () => {
// GIVEN
const fakeAppId = '1';
const fakeClientToken = '2';
const fakeEnvName = 'env';
const configuration = new CoreConfiguration(
fakeClientToken,
fakeEnvName
);
configuration.rumConfiguration = new RumConfiguration(
fakeAppId,
false,
true
);
configuration.rumConfiguration.headerCaptureRules = 'defaults';

NativeModules.DdSdk.initialize.mockResolvedValue(null);

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
expect(DdRumResourceTracking.startTracking).toHaveBeenCalledWith(
expect.objectContaining({
headerCaptureRules: 'defaults'
})
);
});

it('enables header capture with custom headerCaptureRules', async () => {
// GIVEN
const fakeAppId = '1';
const fakeClientToken = '2';
const fakeEnvName = 'env';
const configuration = new CoreConfiguration(
fakeClientToken,
fakeEnvName
);
configuration.rumConfiguration = new RumConfiguration(
fakeAppId,
false,
true
);
configuration.rumConfiguration.headerCaptureRules = [
{ type: 'matchHeaders', headers: ['x-request-id'] }
];

NativeModules.DdSdk.initialize.mockResolvedValue(null);

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
expect(DdRumResourceTracking.startTracking).toHaveBeenCalledWith(
expect.objectContaining({
headerCaptureRules: [
{ type: 'matchHeaders', headers: ['x-request-id'] }
]
})
);
});

it('logs a warning and disables header capture when headerCaptureRules is set but trackResources is false', async () => {
// GIVEN
const fakeAppId = '1';
const fakeClientToken = '2';
const fakeEnvName = 'env';
const configuration = new CoreConfiguration(
fakeClientToken,
fakeEnvName
);
configuration.rumConfiguration = new RumConfiguration(
fakeAppId,
false,
false
);
configuration.rumConfiguration.headerCaptureRules = 'defaults';

NativeModules.DdSdk.initialize.mockResolvedValue(null);

// WHEN
await DdSdkReactNative.initialize(configuration);

// THEN
expect(DdRumResourceTracking.startTracking).not.toHaveBeenCalled();
expect(InternalLog.log).toHaveBeenCalledWith(
'headerCaptureRules is set but trackResources is false. Header capture will be disabled.',
SdkVerbosity.WARN
);
});

it('enables error tracking feature when initialize { error tracking config enabled }', async () => {
// GIVEN
const fakeAppId = '1';
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 @@ -212,6 +213,7 @@ describe('DdSdkReactNativeConfiguration', () => {
],
},
],
"headerCaptureRules": undefined,
"initialResourceThreshold": 0.123,
"longTaskThresholdMs": 567,
"nativeCrashReportEnabled": true,
Expand Down Expand Up @@ -320,6 +322,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"customEndpoint": undefined,
"errorEventMapper": null,
"firstPartyHosts": [],
"headerCaptureRules": undefined,
"initialResourceThreshold": 0,
"longTaskThresholdMs": false,
"nativeCrashReportEnabled": false,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/FileBasedConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ export const getJSONConfiguration = (
buildFirstPartyHosts(
configuration.rumConfiguration.firstPartyHosts
) || RUM_DEFAULTS.getFirstPartyHosts(),
// Validated when compiled at resource tracking start
headerCaptureRules:
configuration.rumConfiguration.headerCaptureRules,
useAccessibilityLabel:
configuration.rumConfiguration.useAccessibilityLabel,
trackInteractions:
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/FileBasedConfiguration.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { FirstPartyHost } from '../rum/types';

import type { CoreConfigurationOptions } from './features/CoreConfiguration.type';
import type { HeaderCaptureRule } from './features/RumConfiguration.type';
import type {
BatchProcessingLevel,
BatchSize,
Expand Down Expand Up @@ -40,6 +41,7 @@ export interface JsonConfiguration extends CoreConfigurationOptions {
actionNameAttribute?: string;
appHangThreshold?: number;
firstPartyHosts?: FirstPartyHost[];
headerCaptureRules?: 'defaults' | HeaderCaptureRule[];
initialResourceThreshold?: number;
trackMemoryWarnings?: boolean;
nativeCrashReportEnabled?: boolean;
Expand Down
Loading
Loading