use serde::Deserialize;
use serde_json::json;
use crate::cdp::client::CdpClient;
#[derive(Debug, Deserialize)]
pub struct ConsoleEntry {
pub level: String,
pub message: String,
pub timestamp: u64,
}
const INTERCEPTOR_JS: &str = r"
if (!window.__chrome_agent_console_installed) {
window.__chrome_agent_console_installed = true;
window.__chrome_agent_console = window.__chrome_agent_console || [];
const __origConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
info: console.info.bind(console),
};
['log','warn','error','info'].forEach(level => {
console[level] = (...args) => {
window.__chrome_agent_console.push({
level,
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '),
timestamp: Date.now(),
});
if (window.__chrome_agent_console.length > 200) window.__chrome_agent_console.shift();
__origConsole[level](...args);
};
});
window.addEventListener('error', (e) => {
window.__chrome_agent_console.push({
level: 'exception',
message: e.message + (e.filename ? ' at ' + e.filename + ':' + e.lineno : ''),
timestamp: Date.now(),
});
});
window.addEventListener('unhandledrejection', (e) => {
window.__chrome_agent_console.push({
level: 'exception',
message: 'Unhandled rejection: ' + String(e.reason),
timestamp: Date.now(),
});
});
} // end guard: __chrome_agent_console_installed
";
pub async fn inject(client: &CdpClient) {
let _ = client
.send(
"Page.addScriptToEvaluateOnNewDocument",
json!({ "source": INTERCEPTOR_JS }),
)
.await;
let guarded = format!(
"if (!window.__chrome_agent_console) {{ {INTERCEPTOR_JS} }}"
);
let _ = client
.send(
"Runtime.evaluate",
json!({ "expression": guarded }),
)
.await;
}
pub async fn run(
client: &CdpClient,
level_filter: Option<&str>,
clear: bool,
limit: usize,
) -> Result<Vec<ConsoleEntry>, crate::BoxError> {
let result: crate::cdp::types::EvaluateResult = client
.call(
"Runtime.evaluate",
json!({
"expression": "JSON.stringify(window.__chrome_agent_console || [])",
"returnByValue": true,
}),
)
.await?;
if let Some(exception) = &result.exception_details {
return Err(format!(
"Failed to read console buffer: {}",
exception
.exception
.as_ref()
.and_then(|e| e.description.as_deref())
.unwrap_or(&exception.text)
)
.into());
}
let raw = result
.result
.value
.as_ref()
.and_then(|v| v.as_str())
.unwrap_or("[]");
let entries: Vec<ConsoleEntry> = serde_json::from_str(raw)
.map_err(|e| format!("Failed to parse console buffer: {e}"))?;
let filtered: Vec<ConsoleEntry> = if let Some(level) = level_filter {
entries.into_iter().filter(|e| e.level == level).collect()
} else {
entries
};
let limited: Vec<ConsoleEntry> = filtered.into_iter().take(limit).collect();
if clear {
let _ = client
.send(
"Runtime.evaluate",
json!({
"expression": "window.__chrome_agent_console = []",
"returnByValue": true,
}),
)
.await;
}
Ok(limited)
}
fn format_time(ts: u64) -> String {
let secs = ts / 1000;
let h = (secs / 3600) % 24;
let m = (secs % 3600) / 60;
let s = secs % 60;
format!("{h:02}:{m:02}:{s:02}")
}
pub fn format_text(entries: &[ConsoleEntry]) -> String {
if entries.is_empty() {
return "No console messages captured.".to_string();
}
entries
.iter()
.map(|e| {
format!(
"[{}] {}: {}",
format_time(e.timestamp),
e.level.to_uppercase(),
e.message
)
})
.collect::<Vec<_>>()
.join("\n")
}