You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds opt-in HTTP request and response header capture to RUM resource events. With resource tracking enabled (trackResources: true), set the headerCaptureRules option in your RUM configuration to choose which headers get captured. headerCaptureRules is the only switch: there is no separate on/off flag.
headerCaptureRules accepts:
Omitted (default): no headers are captured and the capture path adds no overhead.
'defaults': captures a predefined set of 10 response headers (cache-control, etag, age, expires, content-type, content-encoding, content-length, vary, server-timing, x-cache) and 2 request headers (cache-control, content-type) on all URLs.
HeaderCaptureRule[]: an array of composable rules, each with a type: 'defaults', 'matchHeaders', 'matchRequestHeaders' or 'matchResponseHeaders'. A rule can be scoped to specific URLs with forURLs:
A hostname (api.example.com) matches itself and its subdomains, never a hostname that only appears in the path, query, fragment or userinfo of another URL.
A hostname + path prefix (api.example.com/v2) matches whole path segments: /v2 and /v2/users, but not /v22.
Hostnames match case-insensitively; path prefixes match case-sensitively.
Omitting forURLs, or including '*', makes the rule catch-all.
Rule precedence: the header sets of all matching rules are merged. However, if at least one scoped rule (with forURLs) matches a URL, catch-all rules are ignored for that URL. This lets you narrow capture on specific hosts. For example, [{ type: 'defaults' }, { type: 'matchResponseHeaders', headers: ['x-request-id'], forURLs: ['api.example.com'] }] captures only x-request-id on api.example.com and the default headers everywhere else.
Captured headers appear as _dd.request_headers and _dd.response_headers context attributes on stopResource events. These attributes are omitted entirely when there is nothing to report.
Supported clients and configuration
XHR (trackResources: true): covers React Native's built-in fetch, axios and any other XMLHttpRequest-based client.
Global Fetch (trackFetchResources: true): covers the Expo-installed global fetch. When the global fetch is backed by XHR, headers are reported once, on the Fetch resource. Direct imports retained from expo/fetch are not intercepted.
File-based configuration: headerCaptureRules is supported in the JSON configuration and described in datadog-configuration.schema.json. Rules are validated at runtime, and malformed rules are skipped with a warning instead of preventing resource tracking from starting.
Security model
Two layers of non-overridable filtering are always applied, regardless of configuration:
Sensitive header blocklist: headers matching a compiled pattern (token, cookie, secret, authorization, password, credential, bearer, API/access key variants, forwarding and IP headers) are blocked at capture time and never stored in memory.
SDK header exclusion: the distributed tracing headers injected by the SDK (Datadog, W3C/OTel, B3) and the x-datadog-tracked-by header are always excluded.
Implementation pipeline
The feature is a pipeline applied per request, shared by the XHR and Fetch proxies (requestProxy/common/headerCapture.ts):
compileHeaderConfig.ts: validates headerCaptureRules and compiles it into pre-built RegExp + Set<string> objects once at SDK initialization, so there is no per-request config traversal.
captureHeaders.ts: accumulates user-set request headers (after security filtering), then filters request and response headers by the rules matching the request URL.
parseResponseHeaders.ts: parses the raw XHR getAllResponseHeaders() string. Fetch reads headers from the Response object.
enforceSizeLimits.ts: caps each header value (128 bytes), the number of headers (100) and the total payload (2048 bytes, UTF-8) before attributes are forwarded to the native layer.
Configuration is snapshotted per request when the request starts, so in-flight requests are never affected by config changes. Captured headers are finalized when the request completes, before any asynchronous GraphQL body parsing, and the request header store is then released.
Motivation
Caching and content negotiation issues are among the most common and hardest-to-diagnose categories of network problems in mobile apps. Headers like Cache-Control, ETag, Age, Server-Timing and Vary carry the information needed to understand whether resources are being served from cache, how long they live, and whether CDN or server configuration is correct. None of this was previously visible in RUM resource events without custom instrumentation.
This feature makes that class of debugging available out of the box, while keeping security as a first-class constraint: sensitive headers are hard-blocked at the SDK level and cannot be enabled through misconfiguration.
Additional Notes
Header capture is disabled by default and opt-in only: leave headerCaptureRules unset and existing integrations are unaffected.
headerCaptureRules requires trackResources: true. If it is set while trackResources is false, the SDK logs a warning and captures nothing.
Requests that fail without a response (aborted, network error, XHR status 0) never report headers.
Fetch Headers lowercase header names, so on Fetch resources custom request headers use the casing from the rule's headers array rather than the casing passed to fetch.
Full design and behavior reference: packages/core/src/rum/instrumentation/resourceTracking/headerCapture/README.md
Review checklist (to be filled by reviewers)
Feature or bugfix MUST have appropriate tests
Make sure you discussed the feature or bugfix with the maintaining team in an Issue
Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)
If this PR is auto-generated, please make sure also to manually update the code related to the change
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds opt-in HTTP request/response header capture to RUM resource events (via XHR interception), including configuration compilation, security filtering, and size limiting before forwarding captured headers as _dd.request_headers / _dd.response_headers attributes on stopResource.
Changes:
Introduces headerCaptureRules RUM config (defaults shortcut + composable rules), compiles it once at init, and plumbs it through resource tracking.
Captures request headers from setRequestHeader, parses response headers from getAllResponseHeaders(), applies sensitive/tracing header exclusion, then enforces payload size limits.
Adds extensive unit/integration tests plus a feature README; exposes rule types from the package entrypoint.
Reviewed changes
Copilot reviewed 35 out of 36 changed files in this pull request and generated 7 comments.
- Exclude backslashes from forURLs subdomain matching, since URL parsers
treat them as path separators (host-scope bypass)
- Treat any forURLs array containing '*' as catch-all
- Snapshot Fetch captured headers before awaiting GraphQL body parsing
- Stringify request header values before capture so non-string values
no longer drop the resource event
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
trackResourceHeaders flag is missing from configuration paths
packages/core/src/DdSdkReactNative.tsx:522
The advertised trackResourceHeaders opt-in is absent from the configuration types and initialization path: flag-only opt-in cannot enable default capture, while configured rules enable capture regardless of the flag. Thread the false-by-default flag through public, file-based, and async configurations, gate capture on it, and use 'defaults' when enabled with rules omitted. If rules alone are the intended opt-in, reconcile the PR description with that API instead.
- Stop the XHR resource timer before finalizing captured headers, so
header parsing and size enforcement do not inflate the duration
- Stringify XHR header values once and use the same string for sending
and capture
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This regex rejects an XHR URL containing userinfo, such as https://user:pass@api.example.com/users, even though its hostname is api.example.com. With catch-all defaults and a restrictive scoped rule for that host, this incorrectly captures defaults instead of applying the restriction. Allow optional userinfo before the hostname while still rejecting hostnames appearing only before @, and add a positive userinfo test alongside the existing spoofing tests.
Handle non-string rule types without crashing compilation
A malformed JSON rule such as {"type":{"toString":null}} makes String(type) throw because the object cannot be converted to a primitive. The exception escapes rule compilation and prevents resource tracking from starting instead of skipping the invalid rule. Check that type is a string before interpolating it, and add this case to the malformed-rule tests.
Canonicalize URL dot segments before scope matching
URL matching leaves dot segments untouched: https://api.example.com/public/../private (also /public/%2e%2e/private) matches a scope for api.example.com/public, although the destination path is /private. This can capture headers outside the configured path scope; conversely, /v1/../v2/users misses a restrictive /v2 scope and can fall back to broader catch-all rules. Canonicalize the pathname using the client's URL semantics before matching, preserving path case, and add regression tests for literal and encoded dot segments.
- Block application-key credentials (e.g. DD-APPLICATION-KEY)
- Allow userinfo before the host in forURLs matching, while still
rejecting hostnames that only appear in the userinfo
- Resolve dot segments (including %2e encodings) and backslashes in the
path before scope matching
- Skip rules with a non-string type instead of throwing while building
the warning message
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new truncation logic handles multibyte characters and surrogate pairs, but all enforceSizeLimits tests use ASCII; its byte-budget helper also counts UTF-16 code units with .length. The utf8ByteLength tests do not exercise truncation. Please add 2-, 3-, and 4-byte boundary cases around 128 bytes, including an emoji crossing the boundary and lone surrogates, and verify the combined budget using UTF-8 byte counts.
- Remove ASCII tabs, CRs and LFs and trim leading/trailing control
characters and spaces before normalizing URLs, as URL parsers do
- Add UTF-8 boundary tests for value truncation (2-, 3- and 4-byte
characters, lone surrogates) and for the total byte budget
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A root-path scope such as forURLs: ['api.example.com/'] requires a literal slash here, but the request context preserves inputs such as https://api.example.com or https://api.example.com?view=home. Both requests use / as their HTTP path, yet this rule fails to match; capture is either omitted or falls back to broader catch-all rules. Treat the root prefix as matching an omitted path as well, and cover both forms in the URL-matching tests.
Canonicalize encoded hostnames before URL scope matching
https://%61pi.example.com/v2/users resolves to api.example.com, but lowercasing leaves %61pi intact, so a scoped api.example.com/v2 rule does not match. With a broader catch-all rule, this captures headers that the scoped rule should suppress. Canonicalize the hostname using HTTP URL semantics before matching, and add a regression covering encoded hostnames with scoped-over-catch-all precedence.
- Treat an omitted URL path as '/', so root scopes such as
'api.example.com/' match 'https://api.example.com?view=home'
- Decode percent-encoded hostname characters before scope matching, so
scoped rules are not bypassed in favor of catch-all rules
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Equivalent HTTP URL spellings can miss restrictive scoped rules and select broader catch-all capture: https://api.example.com/v2/users (U+FF0E, including its percent-encoded form) is not normalized to api.example.com, and extra scheme slashes such as https:///api.example.com/v2/users are retained at lines 38–48. For clients that resolve these spellings to https://api.example.com/v2/users, headers outside the intended scoped set can therefore be reported. Use URL canonicalization compatible with the supported clients, normalize configured hostnames consistently, and add precedence tests for these equivalent spellings.
Blocked response headers such as Set-Cookie are copied into an SDK-owned record before filtering, contrary to the stated filter-before-storage guarantee; they are excluded from the eventual event. Apply isHeaderAllowed(name) before extracting/storing values here and before assignment in requestProxy/common/headerCapture.ts:110–112. Update both adapters' tests to verify that blocked headers never enter their output records.
…torage
- Normalize hostnames like HTTP clients do before scope matching:
decode percent-encoded UTF-8, apply NFKC, map IDNA label separators
(U+3002, U+FF0E, U+FF61) to '.', and apply the same normalization to
configured forURLs hostnames
- Collapse extra slashes after http(s): before matching
- Drop blocked headers while parsing XHR response headers and copying
Fetch headers, so they never enter SDK-owned records
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NFKC plus lowercasing does not normalize internationalized hostnames consistently: a scope of bücher.example misses https://xn--bcher-kva.example/users, and IDNA-ignored characters such as the soft hyphen in api.exam\u00adple.com remain in the matching hostname. With a restrictive scoped rule and a broader catch-all rule, these misses capture headers that the scoped rule excludes. Use client-compatible IDNA normalization for both configured and request hostnames, disable capture when normalization cannot be performed safely, and add scoped-precedence tests for ignored characters and Unicode/punycode equivalents.
Equivalent hostnames such as bücher.example and xn--bcher-kva.example remain different here, so a restrictive scoped rule can be missed and broader catch-all headers captured instead. NFKC also leaves U+00AD (soft hyphen) intact, although HTTP hostname processing removes it. Canonicalize configured and request hostnames with the same HTTP-compatible IDNA implementation, and add scoped-precedence regression tests for Unicode/punycode spellings and ignored characters.
Normalize empty ports to preserve scoped rule matching
https://api.example.com:/v2/users is a valid spelling of https://api.example.com/v2/users, but retaining the empty port makes the compiled scope miss it and apply broader catch-all rules instead. Drop the lone : during normalization and add a scoped-precedence regression test for this spelling.
Review effort: Balanced
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds opt-in HTTP request and response header capture to RUM resource events. With resource tracking enabled (
trackResources: true), set theheaderCaptureRulesoption in your RUM configuration to choose which headers get captured.headerCaptureRulesis the only switch: there is no separate on/off flag.headerCaptureRulesaccepts:'defaults': captures a predefined set of 10 response headers (cache-control,etag,age,expires,content-type,content-encoding,content-length,vary,server-timing,x-cache) and 2 request headers (cache-control,content-type) on all URLs.HeaderCaptureRule[]: an array of composable rules, each with atype:'defaults','matchHeaders','matchRequestHeaders'or'matchResponseHeaders'. A rule can be scoped to specific URLs withforURLs:api.example.com) matches itself and its subdomains, never a hostname that only appears in the path, query, fragment or userinfo of another URL.api.example.com/v2) matches whole path segments:/v2and/v2/users, but not/v22.forURLs, or including'*', makes the rule catch-all.Rule precedence: the header sets of all matching rules are merged. However, if at least one scoped rule (with
forURLs) matches a URL, catch-all rules are ignored for that URL. This lets you narrow capture on specific hosts. For example,[{ type: 'defaults' }, { type: 'matchResponseHeaders', headers: ['x-request-id'], forURLs: ['api.example.com'] }]captures onlyx-request-idonapi.example.comand the default headers everywhere else.Captured headers appear as
_dd.request_headersand_dd.response_headerscontext attributes onstopResourceevents. These attributes are omitted entirely when there is nothing to report.Supported clients and configuration
trackResources: true): covers React Native's built-infetch, axios and any other XMLHttpRequest-based client.trackFetchResources: true): covers the Expo-installed globalfetch. When the globalfetchis backed by XHR, headers are reported once, on the Fetch resource. Direct imports retained fromexpo/fetchare not intercepted.headerCaptureRulesis supported in the JSON configuration and described indatadog-configuration.schema.json. Rules are validated at runtime, and malformed rules are skipped with a warning instead of preventing resource tracking from starting.Security model
Two layers of non-overridable filtering are always applied, regardless of configuration:
token,cookie,secret,authorization,password,credential,bearer, API/access key variants, forwarding and IP headers) are blocked at capture time and never stored in memory.x-datadog-tracked-byheader are always excluded.Implementation pipeline
The feature is a pipeline applied per request, shared by the XHR and Fetch proxies (
requestProxy/common/headerCapture.ts):compileHeaderConfig.ts: validatesheaderCaptureRulesand compiles it into pre-builtRegExp+Set<string>objects once at SDK initialization, so there is no per-request config traversal.captureHeaders.ts: accumulates user-set request headers (after security filtering), then filters request and response headers by the rules matching the request URL.parseResponseHeaders.ts: parses the raw XHRgetAllResponseHeaders()string. Fetch reads headers from theResponseobject.enforceSizeLimits.ts: caps each header value (128 bytes), the number of headers (100) and the total payload (2048 bytes, UTF-8) before attributes are forwarded to the native layer.Configuration is snapshotted per request when the request starts, so in-flight requests are never affected by config changes. Captured headers are finalized when the request completes, before any asynchronous GraphQL body parsing, and the request header store is then released.
Motivation
Caching and content negotiation issues are among the most common and hardest-to-diagnose categories of network problems in mobile apps. Headers like
Cache-Control,ETag,Age,Server-TimingandVarycarry the information needed to understand whether resources are being served from cache, how long they live, and whether CDN or server configuration is correct. None of this was previously visible in RUM resource events without custom instrumentation.This feature makes that class of debugging available out of the box, while keeping security as a first-class constraint: sensitive headers are hard-blocked at the SDK level and cannot be enabled through misconfiguration.
Additional Notes
headerCaptureRulesunset and existing integrations are unaffected.headerCaptureRulesrequirestrackResources: true. If it is set whiletrackResourcesisfalse, the SDK logs a warning and captures nothing.Headerslowercase header names, so on Fetch resources custom request headers use the casing from the rule'sheadersarray rather than the casing passed tofetch.packages/core/src/rum/instrumentation/resourceTracking/headerCapture/README.mdReview checklist (to be filled by reviewers)