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:
@@ -562,6 +562,12 @@ impl DaemonState {
|
|||||||
.client
|
.client
|
||||||
.send_command_no_params("Accessibility.enable", Some(iframe_sid.as_str()))
|
.send_command_no_params("Accessibility.enable", Some(iframe_sid.as_str()))
|
||||||
.await;
|
.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 {
|
for sid in &drained.detached_iframe_sessions {
|
||||||
@@ -727,7 +733,17 @@ impl DaemonState {
|
|||||||
false
|
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;
|
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" => {
|
"Page.screencastFrame" => {
|
||||||
// Frame broadcasting and acks are handled in real-time by the
|
// Frame broadcasting and acks are handled in real-time by the
|
||||||
// stream server's background CDP event loop. Here we just
|
// 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::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) => {
|
Err(broadcast::error::TryRecvError::Closed) => {
|
||||||
self.event_rx = None;
|
self.event_rx = None;
|
||||||
break;
|
break;
|
||||||
@@ -5854,6 +5900,14 @@ async fn handle_har_start(state: &mut DaemonState) -> Result<Value, String> {
|
|||||||
mgr.client
|
mgr.client
|
||||||
.send_command_no_params("Network.enable", Some(&session_id))
|
.send_command_no_params("Network.enable", Some(&session_id))
|
||||||
.await?;
|
.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_recording = true;
|
||||||
state.har_entries.clear();
|
state.har_entries.clear();
|
||||||
Ok(json!({ "started": true }))
|
Ok(json!({ "started": true }))
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ impl CdpClient {
|
|||||||
let ws_tx = Arc::new(Mutex::new(ws_tx));
|
let ws_tx = Arc::new(Mutex::new(ws_tx));
|
||||||
|
|
||||||
let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
|
let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
|
||||||
let (event_tx, _) = broadcast::channel(256);
|
let (event_tx, _) = broadcast::channel(4096);
|
||||||
let (raw_tx, _) = broadcast::channel(512);
|
let (raw_tx, _) = broadcast::channel(4096);
|
||||||
|
|
||||||
let pending_clone = pending.clone();
|
let pending_clone = pending.clone();
|
||||||
let event_tx_clone = event_tx.clone();
|
let event_tx_clone = event_tx.clone();
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ async fn run_socket_server(
|
|||||||
let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);
|
let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64);
|
||||||
let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx));
|
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);
|
drain_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
Reference in New Issue
Block a user