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