feat: add network request detail and filtering for request tracking (#935)
* feat: add network request detail and filtering for request tracking - Add `network request <requestId>` command to view full request/response details including response body via CDP Network.getResponseBody - Add --type, --method, --status filter flags to `network requests` - --type: comma-separated resource types (xhr,fetch,document) - --method: filter by HTTP method - --status: supports exact (200), class (2xx), range (400-499) - Extend TrackedRequest with request_id, post_data, status, response_headers, mime_type fields - Update Network.responseReceived handler to also populate tracked_requests (previously only updated HAR entries) - Add tests for parse commands and matches_status_filter - Update README, SKILL.md, docs, and help text Closes #932 * fix: show request ID and status in network requests output
This commit is contained in:
@@ -270,6 +270,10 @@ agent-browser network route <url> --body <json> # Mock response
|
|||||||
agent-browser network unroute [url] # Remove routes
|
agent-browser network unroute [url] # Remove routes
|
||||||
agent-browser network requests # View tracked requests
|
agent-browser network requests # View tracked requests
|
||||||
agent-browser network requests --filter api # Filter requests
|
agent-browser network requests --filter api # Filter requests
|
||||||
|
agent-browser network requests --type xhr,fetch # Filter by resource type
|
||||||
|
agent-browser network requests --method POST # Filter by HTTP method
|
||||||
|
agent-browser network requests --status 2xx # Filter by status (200, 2xx, 400-499)
|
||||||
|
agent-browser network request <requestId> # View full request/response detail
|
||||||
agent-browser network har start # Start HAR recording
|
agent-browser network har start # Start HAR recording
|
||||||
agent-browser network har stop [output.har] # Stop and save HAR (temp path if omitted)
|
agent-browser network har stop [output.har] # Stop and save HAR (temp path if omitted)
|
||||||
```
|
```
|
||||||
|
|||||||
+72
-2
@@ -2078,7 +2078,7 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
|
|
||||||
/// Parse network interception, request inspection, and HAR recording commands.
|
/// Parse network interception, request inspection, and HAR recording commands.
|
||||||
fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||||
const VALID: &[&str] = &["route", "unroute", "requests", "har"];
|
const VALID: &[&str] = &["route", "unroute", "requests", "request", "har"];
|
||||||
|
|
||||||
match rest.first().copied() {
|
match rest.first().copied() {
|
||||||
Some("route") => {
|
Some("route") => {
|
||||||
@@ -2102,12 +2102,34 @@ fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
let clear = rest.contains(&"--clear");
|
let clear = rest.contains(&"--clear");
|
||||||
let filter_idx = rest.iter().position(|&s| s == "--filter");
|
let filter_idx = rest.iter().position(|&s| s == "--filter");
|
||||||
let filter = filter_idx.and_then(|i| rest.get(i + 1).copied());
|
let filter = filter_idx.and_then(|i| rest.get(i + 1).copied());
|
||||||
|
let type_idx = rest.iter().position(|&s| s == "--type");
|
||||||
|
let rtype = type_idx.and_then(|i| rest.get(i + 1).copied());
|
||||||
|
let method_idx = rest.iter().position(|&s| s == "--method");
|
||||||
|
let method = method_idx.and_then(|i| rest.get(i + 1).copied());
|
||||||
|
let status_idx = rest.iter().position(|&s| s == "--status");
|
||||||
|
let status = status_idx.and_then(|i| rest.get(i + 1).copied());
|
||||||
let mut cmd = json!({ "id": id, "action": "requests", "clear": clear });
|
let mut cmd = json!({ "id": id, "action": "requests", "clear": clear });
|
||||||
if let Some(f) = filter {
|
if let Some(f) = filter {
|
||||||
cmd["filter"] = json!(f);
|
cmd["filter"] = json!(f);
|
||||||
}
|
}
|
||||||
|
if let Some(t) = rtype {
|
||||||
|
cmd["type"] = json!(t);
|
||||||
|
}
|
||||||
|
if let Some(m) = method {
|
||||||
|
cmd["method"] = json!(m);
|
||||||
|
}
|
||||||
|
if let Some(s) = status {
|
||||||
|
cmd["status"] = json!(s);
|
||||||
|
}
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
Some("request") => {
|
||||||
|
let request_id = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "network request".to_string(),
|
||||||
|
usage: "network request <requestId>",
|
||||||
|
})?;
|
||||||
|
Ok(json!({ "id": id, "action": "request_detail", "requestId": request_id }))
|
||||||
|
}
|
||||||
Some("har") => {
|
Some("har") => {
|
||||||
const HAR_VALID: &[&str] = &["start", "stop"];
|
const HAR_VALID: &[&str] = &["start", "stop"];
|
||||||
match rest.get(1).copied() {
|
match rest.get(1).copied() {
|
||||||
@@ -2135,7 +2157,7 @@ fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
}),
|
}),
|
||||||
None => Err(ParseError::MissingArguments {
|
None => Err(ParseError::MissingArguments {
|
||||||
context: "network".to_string(),
|
context: "network".to_string(),
|
||||||
usage: "network <route|unroute|requests|har> [args...]",
|
usage: "network <route|unroute|requests|request|har> [args...]",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2742,6 +2764,54 @@ mod tests {
|
|||||||
assert!(matches!(result, Err(ParseError::MissingArguments { .. })));
|
assert!(matches!(result, Err(ParseError::MissingArguments { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_requests_type_filter() {
|
||||||
|
let cmd =
|
||||||
|
parse_command(&args("network requests --type xhr,fetch"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "requests");
|
||||||
|
assert_eq!(cmd["type"], "xhr,fetch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_requests_method_filter() {
|
||||||
|
let cmd = parse_command(&args("network requests --method POST"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "requests");
|
||||||
|
assert_eq!(cmd["method"], "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_requests_status_filter() {
|
||||||
|
let cmd = parse_command(&args("network requests --status 2xx"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "requests");
|
||||||
|
assert_eq!(cmd["status"], "2xx");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_requests_combined_filters() {
|
||||||
|
let cmd = parse_command(
|
||||||
|
&args("network requests --filter api --type xhr --method GET --status 200"),
|
||||||
|
&default_flags(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cmd["filter"], "api");
|
||||||
|
assert_eq!(cmd["type"], "xhr");
|
||||||
|
assert_eq!(cmd["method"], "GET");
|
||||||
|
assert_eq!(cmd["status"], "200");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_request_detail() {
|
||||||
|
let cmd = parse_command(&args("network request 1234.5"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "request_detail");
|
||||||
|
assert_eq!(cmd["requestId"], "1234.5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_request_detail_requires_id() {
|
||||||
|
let result = parse_command(&args("network request"), &default_flags());
|
||||||
|
assert!(matches!(result, Err(ParseError::MissingArguments { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
// === Screenshot ===
|
// === Screenshot ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+153
-25
@@ -109,6 +109,16 @@ pub struct TrackedRequest {
|
|||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
#[serde(rename = "resourceType")]
|
#[serde(rename = "resourceType")]
|
||||||
pub resource_type: String,
|
pub resource_type: String,
|
||||||
|
#[serde(rename = "requestId")]
|
||||||
|
pub request_id: String,
|
||||||
|
#[serde(rename = "postData", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub post_data: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status: Option<i64>,
|
||||||
|
#[serde(rename = "responseHeaders", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub response_headers: Option<Value>,
|
||||||
|
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mime_type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FetchPausedRequest {
|
pub struct FetchPausedRequest {
|
||||||
@@ -549,7 +559,7 @@ impl DaemonState {
|
|||||||
.unwrap_or("Other")
|
.unwrap_or("Other")
|
||||||
.to_string();
|
.to_string();
|
||||||
self.har_entries.push(HarEntry {
|
self.har_entries.push(HarEntry {
|
||||||
request_id,
|
request_id: request_id.clone(),
|
||||||
wall_time,
|
wall_time,
|
||||||
method: method.clone(),
|
method: method.clone(),
|
||||||
url: url.clone(),
|
url: url.clone(),
|
||||||
@@ -587,11 +597,21 @@ impl DaemonState {
|
|||||||
headers,
|
headers,
|
||||||
timestamp,
|
timestamp,
|
||||||
resource_type,
|
resource_type,
|
||||||
|
request_id,
|
||||||
|
post_data: request
|
||||||
|
.get("postData")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
|
status: None,
|
||||||
|
response_headers: None,
|
||||||
|
mime_type: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Network.responseReceived" if self.har_recording => {
|
"Network.responseReceived"
|
||||||
|
if self.har_recording || self.request_tracking =>
|
||||||
|
{
|
||||||
if let Some(response) = event.params.get("response") {
|
if let Some(response) = event.params.get("response") {
|
||||||
let request_id = event
|
let request_id = event
|
||||||
.params
|
.params
|
||||||
@@ -625,20 +645,39 @@ impl DaemonState {
|
|||||||
.and_then(|v| v.as_i64())
|
.and_then(|v| v.as_i64())
|
||||||
.unwrap_or(-1);
|
.unwrap_or(-1);
|
||||||
let cdp_timing = response.get("timing").cloned();
|
let cdp_timing = response.get("timing").cloned();
|
||||||
if let Some(entry) = self
|
if self.har_recording {
|
||||||
.har_entries
|
if let Some(entry) = self
|
||||||
.iter_mut()
|
.har_entries
|
||||||
.rev()
|
.iter_mut()
|
||||||
.find(|e| e.request_id == request_id)
|
.rev()
|
||||||
{
|
.find(|e| e.request_id == request_id)
|
||||||
entry.status = status;
|
{
|
||||||
entry.status_text = status_text;
|
entry.status = status;
|
||||||
entry.mime_type = mime_type;
|
entry.status_text = status_text;
|
||||||
entry.http_version = http_version;
|
entry.mime_type = mime_type;
|
||||||
entry.response_headers = response_headers;
|
entry.http_version = http_version;
|
||||||
entry.redirect_url = redirect_url;
|
entry.response_headers = response_headers;
|
||||||
entry.response_body_size = encoded_data_length;
|
entry.redirect_url = redirect_url;
|
||||||
entry.cdp_timing = cdp_timing;
|
entry.response_body_size = encoded_data_length;
|
||||||
|
entry.cdp_timing = cdp_timing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.request_tracking {
|
||||||
|
let resp_headers = response.get("headers").cloned();
|
||||||
|
let resp_mime = response
|
||||||
|
.get("mimeType")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from);
|
||||||
|
if let Some(entry) = self
|
||||||
|
.tracked_requests
|
||||||
|
.iter_mut()
|
||||||
|
.rev()
|
||||||
|
.find(|e| e.request_id == request_id)
|
||||||
|
{
|
||||||
|
entry.status = status;
|
||||||
|
entry.mime_type = resp_mime;
|
||||||
|
entry.response_headers = resp_headers;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1051,6 +1090,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
|||||||
"route" => handle_route(cmd, state).await,
|
"route" => handle_route(cmd, state).await,
|
||||||
"unroute" => handle_unroute(cmd, state).await,
|
"unroute" => handle_unroute(cmd, state).await,
|
||||||
"requests" => handle_requests(cmd, state).await,
|
"requests" => handle_requests(cmd, state).await,
|
||||||
|
"request_detail" => handle_request_detail(cmd, state).await,
|
||||||
"credentials" => handle_http_credentials(cmd, state).await,
|
"credentials" => handle_http_credentials(cmd, state).await,
|
||||||
"emulatemedia" => handle_set_media(cmd, state).await,
|
"emulatemedia" => handle_set_media(cmd, state).await,
|
||||||
"auth_save" => handle_auth_save(cmd).await,
|
"auth_save" => handle_auth_save(cmd).await,
|
||||||
@@ -5611,6 +5651,25 @@ async fn handle_unroute(cmd: &Value, state: &mut DaemonState) -> Result<Value, S
|
|||||||
Ok(json!({ "unrouted": label }))
|
Ok(json!({ "unrouted": label }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn matches_status_filter(status: Option<i64>, filter: &str) -> bool {
|
||||||
|
let Some(code) = status else { return false };
|
||||||
|
let f = filter.to_lowercase();
|
||||||
|
if let Ok(exact) = f.parse::<i64>() {
|
||||||
|
return code == exact;
|
||||||
|
}
|
||||||
|
if f.len() == 3 && f.ends_with("xx") {
|
||||||
|
if let Ok(prefix) = f[..1].parse::<i64>() {
|
||||||
|
return code / 100 == prefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((lo, hi)) = f.split_once('-') {
|
||||||
|
if let (Ok(lo), Ok(hi)) = (lo.parse::<i64>(), hi.parse::<i64>()) {
|
||||||
|
return code >= lo && code <= hi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_requests(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
async fn handle_requests(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
if cmd.get("clear").and_then(|v| v.as_bool()).unwrap_or(false) {
|
if cmd.get("clear").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||||
state.tracked_requests.clear();
|
state.tracked_requests.clear();
|
||||||
@@ -5630,19 +5689,88 @@ async fn handle_requests(cmd: &Value, state: &mut DaemonState) -> Result<Value,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let filter = cmd.get("filter").and_then(|v| v.as_str());
|
let filter = cmd.get("filter").and_then(|v| v.as_str());
|
||||||
let requests: Vec<&TrackedRequest> = if let Some(f) = filter {
|
let type_filter = cmd.get("type").and_then(|v| v.as_str());
|
||||||
state
|
let method_filter = cmd.get("method").and_then(|v| v.as_str());
|
||||||
.tracked_requests
|
let status_filter = cmd.get("status").and_then(|v| v.as_str());
|
||||||
.iter()
|
|
||||||
.filter(|r| r.url.contains(f))
|
let type_list: Vec<String> = type_filter
|
||||||
.collect()
|
.map(|t| t.split(',').map(|s| s.trim().to_lowercase()).collect())
|
||||||
} else {
|
.unwrap_or_default();
|
||||||
state.tracked_requests.iter().collect()
|
|
||||||
};
|
let requests: Vec<&TrackedRequest> = state
|
||||||
|
.tracked_requests
|
||||||
|
.iter()
|
||||||
|
.filter(|r| {
|
||||||
|
if let Some(f) = filter {
|
||||||
|
if !r.url.contains(f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !type_list.is_empty() && !type_list.contains(&r.resource_type.to_lowercase()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(m) = method_filter {
|
||||||
|
if !r.method.eq_ignore_ascii_case(m) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(s) = status_filter {
|
||||||
|
if !matches_status_filter(r.status, s) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(json!({ "requests": requests }))
|
Ok(json!({ "requests": requests }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_request_detail(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
|
let request_id = cmd
|
||||||
|
.get("requestId")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("Missing 'requestId' parameter")?;
|
||||||
|
|
||||||
|
let entry = state
|
||||||
|
.tracked_requests
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.request_id == request_id)
|
||||||
|
.ok_or("Request not found")?;
|
||||||
|
|
||||||
|
let mut result = serde_json::to_value(entry).unwrap_or(json!({}));
|
||||||
|
|
||||||
|
if let Some(ref mgr) = state.browser {
|
||||||
|
if let Ok(session_id) = mgr.active_session_id() {
|
||||||
|
if let Ok(body_result) = mgr
|
||||||
|
.client
|
||||||
|
.send_command(
|
||||||
|
"Network.getResponseBody",
|
||||||
|
Some(json!({ "requestId": request_id })),
|
||||||
|
Some(session_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let base64_encoded = body_result
|
||||||
|
.get("base64Encoded")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let body = body_result
|
||||||
|
.get("body")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
if base64_encoded {
|
||||||
|
result["responseBody"] = json!(format!("[base64, {} chars]", body.len()));
|
||||||
|
} else {
|
||||||
|
result["responseBody"] = json!(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_http_credentials(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
async fn handle_http_credentials(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||||
let session_id = mgr.active_session_id()?.to_string();
|
let session_id = mgr.active_session_id()?.to_string();
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ const DOCUMENTED_ACTIONS: &[&str] = &[
|
|||||||
"route",
|
"route",
|
||||||
"unroute",
|
"unroute",
|
||||||
"requests",
|
"requests",
|
||||||
|
"request_detail",
|
||||||
"credentials",
|
"credentials",
|
||||||
"auth_save",
|
"auth_save",
|
||||||
"auth_login",
|
"auth_login",
|
||||||
@@ -545,6 +546,11 @@ async fn test_tracked_request_struct() {
|
|||||||
headers: json!({"Accept": "text/html"}),
|
headers: json!({"Accept": "text/html"}),
|
||||||
timestamp: 12345,
|
timestamp: 12345,
|
||||||
resource_type: "Document".to_string(),
|
resource_type: "Document".to_string(),
|
||||||
|
request_id: "1.1".to_string(),
|
||||||
|
post_data: None,
|
||||||
|
status: Some(200),
|
||||||
|
response_headers: None,
|
||||||
|
mime_type: Some("text/html".to_string()),
|
||||||
};
|
};
|
||||||
let serialized = serde_json::to_value(&tr).unwrap();
|
let serialized = serde_json::to_value(&tr).unwrap();
|
||||||
assert_eq!(serialized["url"], "https://example.com/api");
|
assert_eq!(serialized["url"], "https://example.com/api");
|
||||||
@@ -565,6 +571,11 @@ async fn test_request_tracking_state() {
|
|||||||
headers: json!({}),
|
headers: json!({}),
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
resource_type: "Document".to_string(),
|
resource_type: "Document".to_string(),
|
||||||
|
request_id: "1.1".to_string(),
|
||||||
|
post_data: None,
|
||||||
|
status: None,
|
||||||
|
response_headers: None,
|
||||||
|
mime_type: None,
|
||||||
});
|
});
|
||||||
state.tracked_requests.push(super::actions::TrackedRequest {
|
state.tracked_requests.push(super::actions::TrackedRequest {
|
||||||
url: "https://other.com".to_string(),
|
url: "https://other.com".to_string(),
|
||||||
@@ -572,6 +583,11 @@ async fn test_request_tracking_state() {
|
|||||||
headers: json!({}),
|
headers: json!({}),
|
||||||
timestamp: 2,
|
timestamp: 2,
|
||||||
resource_type: "XHR".to_string(),
|
resource_type: "XHR".to_string(),
|
||||||
|
request_id: "1.2".to_string(),
|
||||||
|
post_data: None,
|
||||||
|
status: None,
|
||||||
|
response_headers: None,
|
||||||
|
mime_type: None,
|
||||||
});
|
});
|
||||||
assert_eq!(state.tracked_requests.len(), 2);
|
assert_eq!(state.tracked_requests.len(), 2);
|
||||||
|
|
||||||
@@ -589,6 +605,30 @@ async fn test_request_tracking_state() {
|
|||||||
assert!(state.tracked_requests.is_empty());
|
assert!(state.tracked_requests.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matches_status_filter() {
|
||||||
|
use super::actions::matches_status_filter;
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
assert!(matches_status_filter(Some(200), "200"));
|
||||||
|
assert!(!matches_status_filter(Some(201), "200"));
|
||||||
|
|
||||||
|
// Class match (Nxx)
|
||||||
|
assert!(matches_status_filter(Some(200), "2xx"));
|
||||||
|
assert!(matches_status_filter(Some(299), "2xx"));
|
||||||
|
assert!(!matches_status_filter(Some(301), "2xx"));
|
||||||
|
assert!(matches_status_filter(Some(404), "4xx"));
|
||||||
|
|
||||||
|
// Range match
|
||||||
|
assert!(matches_status_filter(Some(400), "400-499"));
|
||||||
|
assert!(matches_status_filter(Some(499), "400-499"));
|
||||||
|
assert!(!matches_status_filter(Some(500), "400-499"));
|
||||||
|
|
||||||
|
// None status
|
||||||
|
assert!(!matches_status_filter(None, "200"));
|
||||||
|
assert!(!matches_status_filter(None, "2xx"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_addscript_and_addinitscript_separate_dispatch() {
|
async fn test_addscript_and_addinitscript_separate_dispatch() {
|
||||||
let mut state = DaemonState::new();
|
let mut state = DaemonState::new();
|
||||||
|
|||||||
+16
-1
@@ -384,7 +384,15 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
.get("resourceType")
|
.get("resourceType")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
println!("{} {} ({})", method, url, resource_type);
|
let request_id = req.get("requestId").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let status = req.get("status").and_then(|v| v.as_i64());
|
||||||
|
match status {
|
||||||
|
Some(s) => println!(
|
||||||
|
"[{}] {} {} ({}) {}",
|
||||||
|
request_id, method, url, resource_type, s
|
||||||
|
),
|
||||||
|
None => println!("[{}] {} {} ({})", request_id, method, url, resource_type),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1735,6 +1743,10 @@ Subcommands:
|
|||||||
requests [options] List captured requests
|
requests [options] List captured requests
|
||||||
--clear Clear request log
|
--clear Clear request log
|
||||||
--filter <pattern> Filter by URL pattern
|
--filter <pattern> Filter by URL pattern
|
||||||
|
--type <types> Filter by resource type (comma-separated: xhr,fetch,document)
|
||||||
|
--method <method> Filter by HTTP method (GET, POST, etc.)
|
||||||
|
--status <code> Filter by status (200, 2xx, 400-499)
|
||||||
|
request <requestId> View full request/response detail (including body)
|
||||||
har <start|stop> [path] Record and export a HAR file
|
har <start|stop> [path] Record and export a HAR file
|
||||||
|
|
||||||
Global Options:
|
Global Options:
|
||||||
@@ -1747,7 +1759,10 @@ Examples:
|
|||||||
agent-browser network unroute
|
agent-browser network unroute
|
||||||
agent-browser network requests
|
agent-browser network requests
|
||||||
agent-browser network requests --filter "api"
|
agent-browser network requests --filter "api"
|
||||||
|
agent-browser network requests --type xhr,fetch
|
||||||
|
agent-browser network requests --method POST --status 2xx
|
||||||
agent-browser network requests --clear
|
agent-browser network requests --clear
|
||||||
|
agent-browser network request 1234.5
|
||||||
agent-browser network har start
|
agent-browser network har start
|
||||||
agent-browser network har stop ./capture.har
|
agent-browser network har stop ./capture.har
|
||||||
"##
|
"##
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ agent-browser network unroute [url] # Remove routes
|
|||||||
agent-browser network requests # View tracked requests
|
agent-browser network requests # View tracked requests
|
||||||
agent-browser network requests --clear # Clear request log
|
agent-browser network requests --clear # Clear request log
|
||||||
agent-browser network requests --filter <pat> # Filter by URL pattern
|
agent-browser network requests --filter <pat> # Filter by URL pattern
|
||||||
|
agent-browser network requests --type xhr,fetch # Filter by resource type
|
||||||
|
agent-browser network requests --method POST # Filter by HTTP method
|
||||||
|
agent-browser network requests --status 2xx # Filter by status (200, 2xx, 400-499)
|
||||||
|
agent-browser network request <requestId> # View full request/response detail
|
||||||
agent-browser network har start # Start HAR recording
|
agent-browser network har start # Start HAR recording
|
||||||
agent-browser network har stop [output.har] # Stop and save HAR (temp path if omitted)
|
agent-browser network har stop [output.har] # Stop and save HAR (temp path if omitted)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ agent-browser --download-path ./downloads open <url> # Set default download dir
|
|||||||
|
|
||||||
# Network
|
# Network
|
||||||
agent-browser network requests # Inspect tracked requests
|
agent-browser network requests # Inspect tracked requests
|
||||||
|
agent-browser network requests --type xhr,fetch # Filter by resource type
|
||||||
|
agent-browser network requests --method POST # Filter by HTTP method
|
||||||
|
agent-browser network requests --status 2xx # Filter by status (200, 2xx, 400-499)
|
||||||
|
agent-browser network request <requestId> # View full request/response detail
|
||||||
agent-browser network route "**/api/*" --abort # Block matching requests
|
agent-browser network route "**/api/*" --abort # Block matching requests
|
||||||
agent-browser network har start # Start HAR recording
|
agent-browser network har start # Start HAR recording
|
||||||
agent-browser network har stop ./capture.har # Stop and save HAR file
|
agent-browser network har stop ./capture.har # Stop and save HAR file
|
||||||
|
|||||||
Reference in New Issue
Block a user