agent_first_http/sdk/fetch/artifacts/
network.rs1use std::collections::BTreeMap;
6use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10use crate::sdk::fetch::writer;
11use crate::shared::artifacts::{Artifact, ArtifactPaths};
12use crate::shared::error::{Error, ErrorCode};
13use crate::shared::redact;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct NetworkLog {
17 pub schema_version: u32,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub main_request_id: Option<String>,
20 pub entries: Vec<NetworkEntry>,
21 pub summary: NetworkSummary,
22}
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct NetworkSummary {
26 pub requests_total: usize,
27 pub failed_total: usize,
28 pub captured_body_files: usize,
29 pub redacted: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct NetworkEntry {
34 pub request_id: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub redirect_from_request_id: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub frame_id: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub loader_id: Option<String>,
41 pub resource_type: String,
42 pub url: String,
43 pub method: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub initiator: Option<serde_json::Value>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub status: Option<u16>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub mime_type: Option<String>,
50 pub request_headers: BTreeMap<String, String>,
51 pub response_headers: BTreeMap<String, String>,
52 #[serde(default, skip_serializing_if = "is_false")]
53 pub request_post_data_present: bool,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub request_post_data_size_bytes: Option<usize>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub body_file: Option<PathBuf>,
58 pub timing: NetworkTiming,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub failure: Option<String>,
61 pub hints: BTreeMap<String, serde_json::Value>,
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct NetworkTiming {
66 pub start_ms: u64,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub end_ms: Option<u64>,
69}
70
71pub async fn write(paths: &ArtifactPaths, log: &NetworkLog) -> Result<PathBuf, Error> {
72 let target = paths.file_for(Artifact::Network);
73 let bytes = serde_json::to_vec_pretty(log).map_err(|e| {
74 Error::new(
75 ErrorCode::InternalError,
76 format!("serialize network log: {e}"),
77 )
78 })?;
79 writer::write_bytes(&target, &bytes).await?;
80 Ok(target)
81}
82
83pub fn redact_headers(map: &mut BTreeMap<String, String>, enabled: bool) {
86 if !enabled {
87 return;
88 }
89 for (name, value) in map.iter_mut() {
90 if redact::should_redact(name) {
91 *value = redact::REDACTED_VALUE.to_string();
92 }
93 }
94}
95
96fn is_false(value: &bool) -> bool {
97 !*value
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn redact_replaces_credentials_only() {
106 let mut map = BTreeMap::new();
107 map.insert("Authorization".to_string(), "Bearer xyz".to_string());
108 map.insert("Cookie".to_string(), "session=abc".to_string());
109 map.insert("X-Api-Token".to_string(), "tok".to_string());
110 map.insert("Content-Type".to_string(), "application/json".to_string());
111 redact_headers(&mut map, true);
112 assert_eq!(map["Authorization"], "[redacted]");
113 assert_eq!(map["Cookie"], "[redacted]");
114 assert_eq!(map["X-Api-Token"], "[redacted]");
115 assert_eq!(map["Content-Type"], "application/json");
116 }
117
118 #[test]
119 fn redact_disabled_passes_everything_through() {
120 let mut map = BTreeMap::new();
121 map.insert("Authorization".to_string(), "Bearer xyz".to_string());
122 redact_headers(&mut map, false);
123 assert_eq!(map["Authorization"], "Bearer xyz");
124 }
125}