Fix HAR capture missing API requests under heavy traffic (#1135)

The CDP event broadcast buffer (256 events) was too small for pages with
many concurrent API requests, causing silent event drops. Modern SPAs
routinely fire 100+ API calls during page load, generating 300+ CDP
network events that would overflow the buffer between drain cycles.

Changes:
- Increase CDP broadcast buffer from 256 to 4096 (event channel) and
  512 to 4096 (raw channel)
- Reduce background drain interval from 500ms to 100ms
- Handle Network.loadingFailed events in HAR recording
- Enable Network.enable on cross-origin iframe sessions during HAR
  recording and request tracking
- Allow Network events from iframe sessions through the session filter
- Log a warning when buffer overflow occurs instead of silently dropping

Fixes #1128

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-04-04 10:25:12 -05:00
committed by GitHub
co-authored by ctate
parent 0a36666587
commit c52d25d576
3 changed files with 59 additions and 5 deletions
+56 -2
View File
@@ -562,6 +562,12 @@ impl DaemonState {
.client
.send_command_no_params("Accessibility.enable", Some(iframe_sid.as_str()))
.await;
if self.har_recording || self.request_tracking {
let _ = mgr
.client
.send_command_no_params("Network.enable", Some(iframe_sid.as_str()))
.await;
}
}
}
for sid in &drained.detached_iframe_sessions {
@@ -727,7 +733,17 @@ impl DaemonState {
false
};
if !session_matches {
// Allow Network events from cross-origin iframe sessions
// when HAR recording or request tracking is active.
let iframe_network_event = !session_matches
&& (self.har_recording || self.request_tracking)
&& event.method.starts_with("Network.")
&& event
.session_id
.as_ref()
.is_some_and(|sid| self.iframe_sessions.values().any(|v| v == sid));
if !session_matches && !iframe_network_event {
continue;
}
@@ -963,6 +979,33 @@ impl DaemonState {
}
}
}
"Network.loadingFailed" if self.har_recording => {
let request_id = event
.params
.get("requestId")
.and_then(|v| v.as_str())
.unwrap_or("");
let timestamp = event.params.get("timestamp").and_then(|v| v.as_f64());
let error_text = event
.params
.get("errorText")
.and_then(|v| v.as_str())
.unwrap_or("Failed");
if let Some(entry) = self
.har_entries
.iter_mut()
.rev()
.find(|e| e.request_id == request_id)
{
if entry.status.is_none() {
entry.status = Some(0);
entry.status_text = error_text.to_string();
}
if let Some(ts) = timestamp {
entry.loading_finished_timestamp = Some(ts);
}
}
}
"Page.screencastFrame" => {
// Frame broadcasting and acks are handled in real-time by the
// stream server's background CDP event loop. Here we just
@@ -1008,7 +1051,10 @@ impl DaemonState {
}
}
Err(broadcast::error::TryRecvError::Empty) => break,
Err(broadcast::error::TryRecvError::Lagged(_)) => continue,
Err(broadcast::error::TryRecvError::Lagged(n)) => {
eprintln!("[agent-browser] Warning: CDP event buffer overflowed, {} events dropped. Network requests may be missing from HAR output.", n);
continue;
}
Err(broadcast::error::TryRecvError::Closed) => {
self.event_rx = None;
break;
@@ -5854,6 +5900,14 @@ async fn handle_har_start(state: &mut DaemonState) -> Result<Value, String> {
mgr.client
.send_command_no_params("Network.enable", Some(&session_id))
.await?;
// Also enable Network on cross-origin iframe sessions so their
// requests are captured in the HAR output.
for iframe_sid in state.iframe_sessions.values() {
let _ = mgr
.client
.send_command_no_params("Network.enable", Some(iframe_sid.as_str()))
.await;
}
state.har_recording = true;
state.har_entries.clear();
Ok(json!({ "started": true }))
+2 -2
View File
@@ -87,8 +87,8 @@ impl CdpClient {
let ws_tx = Arc::new(Mutex::new(ws_tx));
let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
let (event_tx, _) = broadcast::channel(256);
let (raw_tx, _) = broadcast::channel(512);
let (event_tx, _) = broadcast::channel(4096);
let (raw_tx, _) = broadcast::channel(4096);
let pending_clone = pending.clone();
let event_tx_clone = event_tx.clone();
+1 -1
View File
@@ -156,7 +156,7 @@ async fn run_socket_server(
let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);
let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx));
let mut drain_interval = tokio::time::interval(Duration::from_millis(500));
let mut drain_interval = tokio::time::interval(Duration::from_millis(100));
drain_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {