From c52d25d5765bf232add60ab23898f941ccb7d76c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 4 Apr 2026 10:25:12 -0500 Subject: [PATCH] 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> --- cli/src/native/actions.rs | 58 ++++++++++++++++++++++++++++++++++-- cli/src/native/cdp/client.rs | 4 +-- cli/src/native/daemon.rs | 2 +- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index b2070a3..c8f154d 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -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 { 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 })) diff --git a/cli/src/native/cdp/client.rs b/cli/src/native/cdp/client.rs index 621973a..45e0a51 100644 --- a/cli/src/native/cdp/client.rs +++ b/cli/src/native/cdp/client.rs @@ -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(); diff --git a/cli/src/native/daemon.rs b/cli/src/native/daemon.rs index 90a78a1..2aabed0 100644 --- a/cli/src/native/daemon.rs +++ b/cli/src/native/daemon.rs @@ -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 {