agent_first_http/sdk/fetch/artifacts/
storage.rs1use std::path::PathBuf;
6
7use serde::Serialize;
8
9use crate::sdk::cdp::ws_client::Connection;
10use crate::sdk::fetch::writer;
11use crate::shared::artifacts::ArtifactPaths;
12use crate::shared::error::{Error, ErrorCode};
13
14const SIZE_CAP_BYTES: usize = 256 * 1024;
16
17#[derive(Serialize)]
18pub struct StorageSnapshot {
19 pub schema_version: u32,
20 pub url: String,
21 pub local_storage: serde_json::Value,
22 pub session_storage: serde_json::Value,
23 pub indexed_db_names: Vec<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub truncated: Option<TruncationNote>,
27}
28
29#[derive(Serialize)]
30pub struct TruncationNote {
31 pub reason: String,
32 pub cap_bytes: usize,
33}
34
35pub async fn capture(
37 conn: &Connection,
38 session_id: &str,
39 url: &str,
40) -> Result<StorageSnapshot, Error> {
41 let ls = eval_json(
43 conn,
44 session_id,
45 "(function(){ var o={}; for(var i=0;i<localStorage.length;i++){ \
46 var k=localStorage.key(i); o[k]=localStorage.getItem(k); } \
47 return JSON.stringify(o); })()",
48 )
49 .await
50 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
51
52 let ss = eval_json(
54 conn,
55 session_id,
56 "(function(){ var o={}; for(var i=0;i<sessionStorage.length;i++){ \
57 var k=sessionStorage.key(i); o[k]=sessionStorage.getItem(k); } \
58 return JSON.stringify(o); })()",
59 )
60 .await
61 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
62
63 let idb_names = eval_str_array(
65 conn,
66 session_id,
67 "indexedDB.databases().then(dbs => JSON.stringify(dbs.map(d => d.name || '')))",
68 )
69 .await
70 .unwrap_or_default();
71
72 let mut snapshot = StorageSnapshot {
73 schema_version: 1,
74 url: url.to_string(),
75 local_storage: ls,
76 session_storage: ss,
77 indexed_db_names: idb_names,
78 truncated: None,
79 };
80
81 if let Ok(json) = serde_json::to_string(&snapshot)
83 && json.len() > SIZE_CAP_BYTES
84 {
85 snapshot.local_storage = serde_json::Value::Null;
86 snapshot.session_storage = serde_json::Value::Null;
87 snapshot.indexed_db_names = Vec::new();
88 snapshot.truncated = Some(TruncationNote {
89 reason: format!(
90 "serialized size {} bytes exceeded cap {}; values omitted",
91 json.len(),
92 SIZE_CAP_BYTES
93 ),
94 cap_bytes: SIZE_CAP_BYTES,
95 });
96 }
97
98 Ok(snapshot)
99}
100
101pub async fn write(paths: &ArtifactPaths, snapshot: &StorageSnapshot) -> Result<PathBuf, Error> {
102 let target = paths.file_for(crate::shared::artifacts::Artifact::Storage);
103 writer::ensure_dir(&paths.root).await?;
104 let json = serde_json::to_vec_pretty(snapshot)
105 .map_err(|e| Error::new(ErrorCode::InternalError, format!("storage: serialize: {e}")))?;
106 writer::write_bytes(&target, &json).await?;
107 Ok(target)
108}
109
110async fn eval_json(
111 conn: &Connection,
112 session_id: &str,
113 expr: &str,
114) -> Result<serde_json::Value, Error> {
115 let r = conn
116 .send(
117 "Runtime.evaluate",
118 &serde_json::json!({
119 "expression": expr,
120 "returnByValue": true,
121 "awaitPromise": true,
122 }),
123 Some(session_id),
124 )
125 .await?;
126 let s = r["result"]["value"].as_str().unwrap_or("{}");
127 serde_json::from_str(s).map_err(|e| {
128 Error::new(
129 ErrorCode::ArtifactCaptureFailed,
130 format!("storage: parse json: {e}"),
131 )
132 })
133}
134
135async fn eval_str_array(
136 conn: &Connection,
137 session_id: &str,
138 expr: &str,
139) -> Result<Vec<String>, Error> {
140 let r = conn
141 .send(
142 "Runtime.evaluate",
143 &serde_json::json!({
144 "expression": expr,
145 "returnByValue": true,
146 "awaitPromise": true,
147 }),
148 Some(session_id),
149 )
150 .await?;
151 let s = r["result"]["value"].as_str().unwrap_or("[]");
152 serde_json::from_str::<Vec<serde_json::Value>>(s)
153 .map(|arr| {
154 arr.into_iter()
155 .filter_map(|v| v.as_str().map(str::to_string))
156 .collect()
157 })
158 .map_err(|e| {
159 Error::new(
160 ErrorCode::ArtifactCaptureFailed,
161 format!("storage: parse idb names: {e}"),
162 )
163 })
164}