From d4c04de4ff23c55d3a302f86f004b96a7d5ebc32 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:16:40 -0700 Subject: [PATCH 1/3] feat(dashboard): add timing and size columns to network panel Expose transfer size and request duration in the dashboard's network request list. The data was already captured by CDP events for HAR recording but not surfaced in TrackedRequest. This adds encodedDataLength and durationMs fields to TrackedRequest, extends the Network.loadingFinished handler to fire for request_tracking (not just har_recording), and displays Size and Time columns in the dashboard. Co-Authored-By: Claude Opus 4.6 --- .changeset/network-timing-dashboard.md | 5 ++ cli/src/native/actions.rs | 64 +++++++++++++++---- .../src/components/network-panel.tsx | 22 +++++++ 3 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 .changeset/network-timing-dashboard.md 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..3d3110807 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,10 @@ 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 +701,9 @@ impl DaemonState { status: None, response_headers: None, mime_type: None, + encoded_data_length: None, + duration_ms: None, + mono_start, }); } } @@ -754,6 +767,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 +780,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 +798,39 @@ 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/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} From 60be3fae7be6605c549139aaf8a524933c3250ea Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:27:49 -0700 Subject: [PATCH 2/3] style: apply cargo fmt to actions.rs Co-Authored-By: Claude Opus 4.6 --- cli/src/native/actions.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 3d3110807..fc89619ba 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -683,10 +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()); + let mono_start = + event.params.get("timestamp").and_then(|v| v.as_f64()); self.tracked_requests.push(TrackedRequest { url, method, @@ -823,8 +821,7 @@ impl DaemonState { if let Some(len) = encoded_data_length { entry.encoded_data_length = Some(len); } - if let (Some(start), Some(end)) = - (entry.mono_start, timestamp) + if let (Some(start), Some(end)) = (entry.mono_start, timestamp) { let ms = ((end - start) * 1000.0).round(); if ms >= 0.0 { From 836b1ec128a162edf7a6924f8913446e5d379a9a Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:40:48 -0700 Subject: [PATCH 3/3] fix: add new TrackedRequest fields to parity test constructors Co-Authored-By: Claude Opus 4.6 --- cli/src/native/parity_tests.rs | 9 +++++++++ 1 file changed, 9 insertions(+) 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);