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 responses_total: usize,
28 pub finished_total: usize,
29 pub failed_total: usize,
30 pub incomplete_total: usize,
31 pub inflight_total_at_capture: usize,
32 pub pending_by_resource_type: BTreeMap<String, usize>,
33 pub captured_body_files: usize,
34 pub redacted: bool,
35}
36
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum NetworkEntryState {
40 #[default]
41 Pending,
42 Responded,
43 Finished,
44 Failed,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct NetworkEntry {
49 pub request_id: String,
50 pub state: NetworkEntryState,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub redirect_from_request_id: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub frame_id: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub loader_id: Option<String>,
57 pub resource_type: String,
58 pub url: String,
59 pub method: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub initiator: Option<serde_json::Value>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub status: Option<u16>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub mime_type: Option<String>,
66 pub request_headers: BTreeMap<String, String>,
67 pub response_headers: BTreeMap<String, String>,
68 #[serde(default, skip_serializing_if = "is_false")]
69 pub request_post_data_present: bool,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub request_post_data_size_bytes: Option<usize>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub body_file: Option<PathBuf>,
74 pub timing: NetworkTiming,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub failure: Option<String>,
77 pub hints: BTreeMap<String, serde_json::Value>,
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct NetworkTiming {
82 pub start_ms: u64,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub end_ms: Option<u64>,
85}
86
87pub async fn write(paths: &ArtifactPaths, log: &NetworkLog) -> Result<PathBuf, Error> {
88 let target = paths.file_for(Artifact::Network);
89 let bytes = serde_json::to_vec_pretty(log).map_err(|e| {
90 Error::new(
91 ErrorCode::InternalError,
92 format!("serialize network log: {e}"),
93 )
94 })?;
95 writer::write_bytes(&target, &bytes).await?;
96 Ok(target)
97}
98
99pub fn redact_headers(map: &mut BTreeMap<String, String>, enabled: bool) {
102 if !enabled {
103 return;
104 }
105 for (name, value) in map.iter_mut() {
106 if redact::should_redact(name) {
107 *value = redact::REDACTED_VALUE.to_string();
108 }
109 }
110}
111
112fn is_false(value: &bool) -> bool {
113 !*value
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn redact_replaces_credentials_only() {
122 let mut map = BTreeMap::new();
123 map.insert("Authorization".to_string(), "Bearer xyz".to_string());
124 map.insert("Cookie".to_string(), "session=abc".to_string());
125 map.insert("X-Api-Token".to_string(), "tok".to_string());
126 map.insert("Content-Type".to_string(), "application/json".to_string());
127 redact_headers(&mut map, true);
128 assert_eq!(map["Authorization"], "[redacted]");
129 assert_eq!(map["Cookie"], "[redacted]");
130 assert_eq!(map["X-Api-Token"], "[redacted]");
131 assert_eq!(map["Content-Type"], "application/json");
132 }
133
134 #[test]
135 fn redact_disabled_passes_everything_through() {
136 let mut map = BTreeMap::new();
137 map.insert("Authorization".to_string(), "Bearer xyz".to_string());
138 redact_headers(&mut map, false);
139 assert_eq!(map["Authorization"], "Bearer xyz");
140 }
141}