Skip to content

✨ Capture XHR request headers#4445

Draft
bdibon wants to merge 2 commits intomainfrom
capture-xhr-headers
Draft

✨ Capture XHR request headers#4445
bdibon wants to merge 2 commits intomainfrom
capture-xhr-headers

Conversation

@bdibon
Copy link
Copy Markdown
Contributor

@bdibon bdibon commented Apr 3, 2026

Motivation

The browser SDK already captures network headers for three out of four request/direction combinations:

Request Headers Response Headers
Fetch ✅ Supported ✅ Supported
XHR ❌ Not supported ✅ Supported

XHR requests account for a significant portion of the network requests we collect. This PR closes the gap.

The root challenge: XMLHttpRequest has no native getter for request headers — once setRequestHeader() is called, headers cannot be read back. This PR solves it by intercepting setRequestHeader at instrumentation time.

Changes

Instrument XMLHttpRequest.prototype.setRequestHeader in xhrObservable.ts, alongside the existing open/send/abort interception. Headers are stored lazily as a Headers object on XhrOpenContext in the existing xhrContexts WeakMap, then passed through RequestCompleteEvent to resourceCollection.ts where the existing filterHeaders security layer applies unchanged (forbidden header pattern, 100-header cap, 128-char value truncation).

Files changed:

  • packages/core/src/browser/xhrObservable.ts — instrument setRequestHeader, extend XhrOpenContext with requestHeaders?: Headers
  • packages/core/test/emulate/mockXhr.ts — add setRequestHeader support to the mock XHR
  • packages/rum-core/src/domain/requestCollection.ts — extend RequestCompleteEvent with requestHeaders?: Headers
  • packages/rum-core/src/domain/resource/resourceCollection.ts — read request.requestHeaders for XHR in getRequestHeaders()

No new feature flags or configuration options. Gated by the existing ExperimentalFeature.TRACK_RESOURCE_HEADERS and trackResourceHeaders config.

Design notes:

  • Headers stored in onPostCall (not pre-call), so phantom headers are never captured when setRequestHeader throws (e.g. called before open(), invalid header name)
  • Headers.append() preserves the spec behavior for duplicate header names (X-Foo: a, b)
  • Tracer-injected DD headers (x-datadog-trace-id etc.) are captured alongside app headers — intentional, filtered by user's trackResourceHeaders config

Bundle size impact (~80 bytes gzipped on rum/rum-slim):

Bundle main (gzip) branch (gzip) Delta
rum 60.95 KiB 61.03 KiB +0.08 KiB
rum_slim 45.93 KiB 46.00 KiB +0.07 KiB
logs 20.80 KiB 20.88 KiB +0.08 KiB
rum_profiler 2.43 KiB 2.43 KiB
rum_recorder 9.26 KiB 9.26 KiB
worker 7.44 KiB 7.44 KiB

Test instructions

cat > sandbox/test-xhr-headers.html << 'EOF'
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Test XHR request headers</title>
    <script src="/datadog-rum.js"></script>
    <script>
      DD_RUM.init({
        clientToken: 'xxx',
        applicationId: 'xxx',
        trackResources: true,
        enableExperimentalFeatures: ['track_resource_headers'],
        trackResourceHeaders: ['x-custom-header', 'x-request-id', 'content-type'],
        proxy: '/proxy',
      })
    </script>
  </head>
  <body>
    <button id="xhr-with-headers" onclick="
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://tools-httpstatus.pickup-services.com/200');
      xhr.setRequestHeader('x-custom-header', 'my-value');
      xhr.setRequestHeader('x-request-id', 'req-123');
      xhr.send();
    ">Send XHR with custom headers</button>

    <button id="xhr-no-headers" onclick="
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://tools-httpstatus.pickup-services.com/200');
      xhr.send();
    ">Send XHR without custom headers</button>

    <button id="xhr-forbidden" onclick="
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://tools-httpstatus.pickup-services.com/200');
      xhr.setRequestHeader('x-custom-header', 'should-appear');
      xhr.setRequestHeader('authorization', 'Bearer secret');
      xhr.send();
    ">Send XHR with forbidden header (authorization)</button>
  </body>
</html>
EOF

yarn dev-server start
yarn dev-server intake clear
agent-browser open http://localhost:8080/test-xhr-headers.html
agent-browser click '#xhr-with-headers'
agent-browser click '#xhr-no-headers'
agent-browser click '#xhr-forbidden'
agent-browser tab new
yarn dev-server intake rum-resources | jq -s '[.[] | select(.resource.type == "xhr") | {url: .resource.url, headers: (.resource.request.headers // null)}]'
rm sandbox/test-xhr-headers.html

Expected output:

[
  {
    "url": "https://tools-httpstatus.pickup-services.com/200",
    "headers": {
      "x-custom-header": "my-value",
      "x-request-id": "req-123"
    }
  },
  {
    "url": "https://tools-httpstatus.pickup-services.com/200",
    "headers": null
  },
  {
    "url": "https://tools-httpstatus.pickup-services.com/200",
    "headers": {
      "x-custom-header": "should-appear"
    }
  }
]
  • Request 1 (with headers): x-custom-header and x-request-id are captured
  • Request 2 (no headers): request.headers is absent (null)
  • Request 3 (forbidden header): authorization is stripped by FORBIDDEN_HEADER_PATTERN; x-custom-header still appears

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

@cit-pr-commenter-54b7da
Copy link
Copy Markdown

cit-pr-commenter-54b7da bot commented Apr 3, 2026

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 178.50 KiB 178.82 KiB +326 B +0.18%
Rum Profiler 6.16 KiB 6.16 KiB 0 B 0.00%
Rum Recorder 27.03 KiB 27.03 KiB 0 B 0.00%
Logs 57.00 KiB 57.24 KiB +251 B +0.43%
Rum Slim 134.32 KiB 134.64 KiB +320 B +0.23%
Worker 23.63 KiB 23.63 KiB 0 B 0.00%
🚀 CPU Performance

Pending...

🧠 Memory Performance

Pending...

🔗 RealWorld

@datadog-official
Copy link
Copy Markdown

datadog-official bot commented Apr 3, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 63.64%
Overall Coverage: 77.42% (+0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 8c0365f | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

bdibon and others added 2 commits April 3, 2026 17:35
Instrument `XMLHttpRequest.prototype.setRequestHeader` to collect request
headers for XHR resources, closing the last gap in network header capture
(Fetch request/response and XHR response headers were already supported).

Headers are stored lazily on the existing `XhrOpenContext` via a `Headers`
object in the `xhrContexts` WeakMap and passed through `RequestCompleteEvent`
to `resourceCollection`, where the existing `filterHeaders` security layer
(forbidden header pattern, 100-header cap, 128-char value truncation) applies
unchanged. Gated by `ExperimentalFeature.TRACK_RESOURCE_HEADERS` and
`trackResourceHeaders` config — no new flags needed.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@bdibon bdibon force-pushed the capture-xhr-headers branch from 2b9abcb to 8c0365f Compare April 3, 2026 15:36
@bdibon bdibon changed the title [RUM-???] ✨ Capture XHR request headers ✨ Capture XHR request headers Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant