[GH-ISSUE #3127] [Bug] Browser scenario event collection drops events at a fixed resource budget when response bodies are captured, contrary to the "no caps" contract #1088

Closed
opened 2026-10-15 18:30:58 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @Sergio-763 on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3127

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Interface

CLI

Earliest failure stage

Capture or observation

Observed behavior

With capture.network.response_body: true, a page that performs 40 GET requests returning 60 KB JSON bodies and then 3 small POSTs yields an incomplete capture: 15 events are dropped and the events section is marked missing. With body capture off, the same page produces a complete capture.

Counts from the Evidence (normalized_result.events, completeness):

response_body retained dropped completeness request events network-content events
true 161 15 incomplete, missing_sections: ["events"], equality_eligible: false 44 of 44 29 of 44
false 132 0 complete 44 of 44 n/a

Only the network-content events for GET /data/25 through /data/39 (transactions request-27 to request-41) are missing. The three later small POST transactions retain their content, so what is lost depends on event size, not just order. The omission is disclosed: events.dropped is 15 and limitations contains "Browser event collection reached its resource budget; omitted events and descendant observations are unknown." However, the omitted transactions have no network-content event at all (no per-transaction unavailable state), so the caller cannot tell from the Evidence which content is missing without diffing transaction IDs, and cannot raise or filter the budget: the request accepts no limits object (docs/browser-scenario-contract.md:27-28).

The contract states: "There are no event, frame, worker, popup, WebSocket, DOM-node, accessibility-node, storage-entry, screenshot-count, or cumulative metadata-byte caps. Requested artifacts and observed events are returned inline without application-defined size ceilings." (docs/browser-scenario-contract.md:183-187).

Reproduction

Fixture (server.mjs; prints its port):

import http from "node:http";
const BODY = JSON.stringify({ pad: "x".repeat(60_000) });
const page = (s) => `<!doctype html><meta charset=utf-8><body><script>${s}</script>`;
const server = http.createServer((req, res) => {
  const p = new URL(req.url, "http://x").pathname;
  const send = (t, b) => { res.writeHead(200, { "content-type": t, "cache-control": "no-store" }); res.end(b); };
  if (p === "/events/") return send("text/html", page(
    `(async()=>{for(let i=0;i<40;i++){await fetch("/data/"+i).then(r=>r.text());}
      for(let i=0;i<3;i++){await fetch("/final/"+i,{method:"POST",body:"{}"}).then(r=>r.text());}
      const d=document.createElement("div");d.id="done";document.body.append(d);})()`));
  if (p.startsWith("/data/")) return send("application/json", BODY);
  if (p.startsWith("/final/")) return send("application/json", '{"ok":true}');
  res.writeHead(404); res.end();
});
server.listen(0, "127.0.0.1", () => console.log(server.address().port));

Scenario events.json (set response_body to false for the control):

{
  "browser": { "mode": "launch", "executable_path": "<path to a Chromium-family browser>" },
  "start_url": { "url": "http://127.0.0.1:<port>/events/" },
  "actions": [{ "step_id": "ready", "action": "wait_for", "locator": { "kind": "css", "selector": "#done" }, "state": "attached", "timeout_ms": 60000 }],
  "capture": { "events": ["network"], "network": { "request_body": false, "response_body": true, "header_values": false } }
}
rea capture-browser-scenario events.json --format json

Inspect normalized_result.events.retained, .dropped, normalized_result.completeness and the network-content items. Both configurations were run on revision f3534749e.

Expected behavior

Either the documented behavior holds (events are not capped by an application-defined ceiling), or the contract states the bound and the request lets the caller choose it or narrow what is captured (for example per-family or per-transaction filtering), and every omitted observation is identifiable (a per-transaction unavailable state or the omitted transaction IDs), so a bounded drop is not just a count.

Typed outcome or recovery expected

A typed limit on the Evidence naming which budget was reached and which transactions' content is omitted, plus a documented request option to adjust or narrow it.

Impact and scope

Captures of pages that load a moderate number of mid-size responses cannot be recorded with bodies; they are marked incomplete and ineligible for equality claims even though far less than the host could hold is retained. Capture with body capture off, or of small bodies, is unaffected. The disclosure itself (dropped count, incomplete status, limitation text) works; the gaps are the contradicting contract, the lack of a caller-selectable budget, and non-identifiable omissions.

Analysis or suspected cause

Source-derived, not otherwise verified:

  • src/browser/PlaywrightScenarioEvents.ts:51-53 hardcodes MAX_EVENT_COST = 4 MiB, MAX_CAPTURE_COST = 16 MiB, MAX_EVENTS = 10_000; cost is string length * 8 + 2 (:55-56), so a 60 KB body, once base64-encoded (about 80 K characters), costs about 640 KB of the 16 MiB budget, and about 30 such events (29 were retained here) exhaust it.
  • push (:206-226) increments dropped and returns when items.length >= MAX_EVENTS or retainedCost + cost > MAX_CAPTURE_COST; :233-255 applies the same checks before redaction. Smaller later events still fit, which matches the observation.
  • Related but separate observation (not the subject of this report): src/browser/network/PlaywrightNetworkContent.ts:105-109 marks any body over 64 KiB as unavailable with reason: "body-read-failed" and message "Body exceeds the 64 KiB retained-content budget."; in the same fixture with 150 KB bodies, 40 of 44 bodies were reported this way. That is also a fixed bound not covered by the contract text, and the reason code names a read failure rather than a budget.

REA version

6.4.0 (source revision f3534749e)

Runtime and platform

Node 24.18.0; macOS arm64 (Darwin 25); Brave 155.1.97.56 headless, launch mode with a REA-owned profile

Target and artifact context

Local HTTP fixture page issuing sequential fetches; browser scenario launch mode with network event family and response-body capture.

Originally created by @Sergio-763 on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3127 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Process capture or runtime observation ## Interface CLI ## Earliest failure stage Capture or observation ## Observed behavior With `capture.network.response_body: true`, a page that performs 40 GET requests returning 60 KB JSON bodies and then 3 small POSTs yields an incomplete capture: 15 events are dropped and the `events` section is marked missing. With body capture off, the same page produces a complete capture. Counts from the Evidence (`normalized_result.events`, `completeness`): | response_body | retained | dropped | completeness | request events | network-content events | |---|---|---|---|---|---| | `true` | 161 | 15 | `incomplete`, `missing_sections: ["events"]`, `equality_eligible: false` | 44 of 44 | 29 of 44 | | `false` | 132 | 0 | `complete` | 44 of 44 | n/a | Only the `network-content` events for `GET /data/25` through `/data/39` (transactions `request-27` to `request-41`) are missing. The three later small POST transactions retain their content, so what is lost depends on event size, not just order. The omission is disclosed: `events.dropped` is 15 and `limitations` contains "Browser event collection reached its resource budget; omitted events and descendant observations are unknown." However, the omitted transactions have no `network-content` event at all (no per-transaction `unavailable` state), so the caller cannot tell from the Evidence which content is missing without diffing transaction IDs, and cannot raise or filter the budget: the request accepts no limits object (`docs/browser-scenario-contract.md:27-28`). The contract states: "There are no event, frame, worker, popup, WebSocket, DOM-node, accessibility-node, storage-entry, screenshot-count, or cumulative metadata-byte caps. Requested artifacts and observed events are returned inline without application-defined size ceilings." (`docs/browser-scenario-contract.md:183-187`). ## Reproduction Fixture (`server.mjs`; prints its port): ```js import http from "node:http"; const BODY = JSON.stringify({ pad: "x".repeat(60_000) }); const page = (s) => `<!doctype html><meta charset=utf-8><body><script>${s}</script>`; const server = http.createServer((req, res) => { const p = new URL(req.url, "http://x").pathname; const send = (t, b) => { res.writeHead(200, { "content-type": t, "cache-control": "no-store" }); res.end(b); }; if (p === "/events/") return send("text/html", page( `(async()=>{for(let i=0;i<40;i++){await fetch("/data/"+i).then(r=>r.text());} for(let i=0;i<3;i++){await fetch("/final/"+i,{method:"POST",body:"{}"}).then(r=>r.text());} const d=document.createElement("div");d.id="done";document.body.append(d);})()`)); if (p.startsWith("/data/")) return send("application/json", BODY); if (p.startsWith("/final/")) return send("application/json", '{"ok":true}'); res.writeHead(404); res.end(); }); server.listen(0, "127.0.0.1", () => console.log(server.address().port)); ``` Scenario `events.json` (set `response_body` to `false` for the control): ```json { "browser": { "mode": "launch", "executable_path": "<path to a Chromium-family browser>" }, "start_url": { "url": "http://127.0.0.1:<port>/events/" }, "actions": [{ "step_id": "ready", "action": "wait_for", "locator": { "kind": "css", "selector": "#done" }, "state": "attached", "timeout_ms": 60000 }], "capture": { "events": ["network"], "network": { "request_body": false, "response_body": true, "header_values": false } } } ``` ```sh rea capture-browser-scenario events.json --format json ``` Inspect `normalized_result.events.retained`, `.dropped`, `normalized_result.completeness` and the `network-content` items. Both configurations were run on revision f3534749e. ## Expected behavior Either the documented behavior holds (events are not capped by an application-defined ceiling), or the contract states the bound and the request lets the caller choose it or narrow what is captured (for example per-family or per-transaction filtering), and every omitted observation is identifiable (a per-transaction unavailable state or the omitted transaction IDs), so a bounded drop is not just a count. ## Typed outcome or recovery expected A typed limit on the Evidence naming which budget was reached and which transactions' content is omitted, plus a documented request option to adjust or narrow it. ## Impact and scope Captures of pages that load a moderate number of mid-size responses cannot be recorded with bodies; they are marked incomplete and ineligible for equality claims even though far less than the host could hold is retained. Capture with body capture off, or of small bodies, is unaffected. The disclosure itself (dropped count, incomplete status, limitation text) works; the gaps are the contradicting contract, the lack of a caller-selectable budget, and non-identifiable omissions. ## Analysis or suspected cause Source-derived, not otherwise verified: - `src/browser/PlaywrightScenarioEvents.ts:51-53` hardcodes `MAX_EVENT_COST = 4 MiB`, `MAX_CAPTURE_COST = 16 MiB`, `MAX_EVENTS = 10_000`; cost is `string length * 8 + 2` (`:55-56`), so a 60 KB body, once base64-encoded (about 80 K characters), costs about 640 KB of the 16 MiB budget, and about 30 such events (29 were retained here) exhaust it. - `push` (`:206-226`) increments `dropped` and returns when `items.length >= MAX_EVENTS` or `retainedCost + cost > MAX_CAPTURE_COST`; `:233-255` applies the same checks before redaction. Smaller later events still fit, which matches the observation. - Related but separate observation (not the subject of this report): `src/browser/network/PlaywrightNetworkContent.ts:105-109` marks any body over 64 KiB as `unavailable` with `reason: "body-read-failed"` and message "Body exceeds the 64 KiB retained-content budget."; in the same fixture with 150 KB bodies, 40 of 44 bodies were reported this way. That is also a fixed bound not covered by the contract text, and the reason code names a read failure rather than a budget. ## REA version 6.4.0 (source revision f3534749e) ## Runtime and platform Node 24.18.0; macOS arm64 (Darwin 25); Brave 155.1.97.56 headless, launch mode with a REA-owned profile ## Target and artifact context Local HTTP fixture page issuing sequential fetches; browser scenario `launch` mode with network event family and response-body capture.
bsakaguchi 2026-10-15 18:30:58 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Delivered on main in fffb07419.

Hard-coded capture ceilings and busy content-read slots discarded otherwise selected observations. Budgets are now caller-selectable and recorded explicitly; admitted content jobs queue behind bounded read workers, with identifiable omissions and coverage derived from retained facts.

Validation: Real Chrome fixture: a caller-selected 32 MiB allocation budget captures all 44 selected bodies and 176 events with complete coverage and owned cleanup. Default-budget omissions remain explicit. Queue, timeout and cancellation controls pass. Different coverage levels are not a speedup comparison. Full exact-revision qualification passed.

<!-- gh-comment-id:6177595711 --> @morluto commented on GitHub (Oct 14, 2026): Delivered on main in [fffb07419](https://github.com/morluto/rea/commit/fffb0741936067530d1ce9777461cd53e87398c2). Hard-coded capture ceilings and busy content-read slots discarded otherwise selected observations. Budgets are now caller-selectable and recorded explicitly; admitted content jobs queue behind bounded read workers, with identifiable omissions and coverage derived from retained facts. Validation: Real Chrome fixture: a caller-selected 32 MiB allocation budget captures all 44 selected bodies and 176 events with complete coverage and owned cleanup. Default-budget omissions remain explicit. Queue, timeout and cancellation controls pass. Different coverage levels are not a speedup comparison. Full exact-revision [qualification](https://github.com/morluto/rea/actions/runs/38629313022) passed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1088
No description provided.