diff --git a/.changeset/network-timing-dashboard.md b/.changeset/network-timing-dashboard.md new file mode 100644 index 000000000..30c5607fe --- /dev/null +++ b/.changeset/network-timing-dashboard.md @@ -0,0 +1,5 @@ +--- +"agent-browser": minor +--- + +Add timing and transfer size columns to the dashboard network panel. Each request row now shows the encoded transfer size and total duration computed from CDP `Network.loadingFinished` events. diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 2cf07ea4d..fc89619ba 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -122,6 +122,12 @@ pub struct TrackedRequest { pub response_headers: Option, #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")] pub mime_type: Option, + #[serde(rename = "encodedDataLength", skip_serializing_if = "Option::is_none")] + pub encoded_data_length: Option, + #[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")] + pub duration_ms: Option, + #[serde(skip)] + pub mono_start: Option, } pub struct FetchPausedRequest { @@ -677,6 +683,8 @@ impl DaemonState { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); + let mono_start = + event.params.get("timestamp").and_then(|v| v.as_f64()); self.tracked_requests.push(TrackedRequest { url, method, @@ -691,6 +699,9 @@ impl DaemonState { status: None, response_headers: None, mime_type: None, + encoded_data_length: None, + duration_ms: None, + mono_start, }); } } @@ -754,6 +765,10 @@ impl DaemonState { .get("mimeType") .and_then(|v| v.as_str()) .map(String::from); + let resp_encoded_len = response + .get("encodedDataLength") + .and_then(|v| v.as_i64()) + .filter(|&n| n >= 0); if let Some(entry) = self .tracked_requests .iter_mut() @@ -763,11 +778,14 @@ impl DaemonState { entry.status = status; entry.mime_type = resp_mime; entry.response_headers = resp_headers; + entry.encoded_data_length = resp_encoded_len; } } } } - "Network.loadingFinished" if self.har_recording => { + "Network.loadingFinished" + if self.har_recording || self.request_tracking => + { let request_id = event .params .get("requestId") @@ -778,17 +796,38 @@ impl DaemonState { .params .get("encodedDataLength") .and_then(|v| v.as_i64()); - if let Some(entry) = self - .har_entries - .iter_mut() - .rev() - .find(|e| e.request_id == request_id) - { - if let Some(ts) = timestamp { - entry.loading_finished_timestamp = Some(ts); + if self.har_recording { + if let Some(entry) = self + .har_entries + .iter_mut() + .rev() + .find(|e| e.request_id == request_id) + { + if let Some(ts) = timestamp { + entry.loading_finished_timestamp = Some(ts); + } + if let Some(len) = encoded_data_length { + entry.response_body_size = len; + } } - if let Some(len) = encoded_data_length { - entry.response_body_size = len; + } + if self.request_tracking { + if let Some(entry) = self + .tracked_requests + .iter_mut() + .rev() + .find(|e| e.request_id == request_id) + { + if let Some(len) = encoded_data_length { + entry.encoded_data_length = Some(len); + } + if let (Some(start), Some(end)) = (entry.mono_start, timestamp) + { + let ms = ((end - start) * 1000.0).round(); + if ms >= 0.0 { + entry.duration_ms = Some(ms); + } + } } } } diff --git a/cli/src/native/parity_tests.rs b/cli/src/native/parity_tests.rs index 5ff5d0b13..2b7a013e3 100644 --- a/cli/src/native/parity_tests.rs +++ b/cli/src/native/parity_tests.rs @@ -551,6 +551,9 @@ async fn test_tracked_request_struct() { status: Some(200), response_headers: None, mime_type: Some("text/html".to_string()), + encoded_data_length: None, + duration_ms: None, + mono_start: None, }; let serialized = serde_json::to_value(&tr).unwrap(); assert_eq!(serialized["url"], "https://example.com/api"); @@ -576,6 +579,9 @@ async fn test_request_tracking_state() { status: None, response_headers: None, mime_type: None, + encoded_data_length: None, + duration_ms: None, + mono_start: None, }); state.tracked_requests.push(super::actions::TrackedRequest { url: "https://other.com".to_string(), @@ -588,6 +594,9 @@ async fn test_request_tracking_state() { status: None, response_headers: None, mime_type: None, + encoded_data_length: None, + duration_ms: None, + mono_start: None, }); assert_eq!(state.tracked_requests.len(), 2); diff --git a/packages/dashboard/src/components/network-panel.tsx b/packages/dashboard/src/components/network-panel.tsx index 861e9dfef..f7c12438b 100644 --- a/packages/dashboard/src/components/network-panel.tsx +++ b/packages/dashboard/src/components/network-panel.tsx @@ -25,6 +25,8 @@ interface NetworkRequest { requestId: string; mimeType?: string; timestamp: number; + encodedDataLength?: number; + durationMs?: number; } type TypeFilter = "all" | "xhr" | "doc" | "css" | "js" | "img" | "font" | "other"; @@ -71,6 +73,20 @@ function urlHost(url: string): string { } } +function formatSize(bytes?: number): string { + if (bytes == null) return "—"; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function formatDuration(ms?: number): string { + if (ms == null) return "—"; + if (ms < 1) return "<1ms"; + if (ms < 1000) return `${Math.round(ms)}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + export function NetworkPanel() { const sessionName = useAtomValue(activeSessionNameAtom); @@ -292,6 +308,12 @@ export function NetworkPanel() { {truncateUrl(r.url, 80)} + + {formatSize(r.encodedDataLength)} + + + {formatDuration(r.durationMs)} + {r.resourceType}