use crate::browser::views::ConsoleLog;
use crate::error::Result;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct ConsoleManager {
client: Arc<crate::browser::cdp::CdpClient>,
logs: Arc<Mutex<Vec<ConsoleLog>>>,
}
impl ConsoleManager {
pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
Self {
client,
logs: Arc::new(Mutex::new(Vec::new())),
}
}
pub async fn start_listening(&self) -> Result<()> {
let _ = self
.client
.send_command("Runtime.enable", Value::Null)
.await;
let _ = self
.client
.send_command("Log.enable", Value::Null)
.await;
let logs = Arc::clone(&self.logs);
let mut rx = self.client.subscribe_events().await?;
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
if event.method == "Runtime.consoleAPICalled" {
if let Some(log) = parse_console_event(&event.params) {
let mut guard = logs.lock().await;
guard.push(log);
if guard.len() > 1000 {
guard.remove(0);
}
}
} else if event.method == "Log.entryAdded"
&& let Some(entry) = event.params.get("entry")
&& let Some(log) = parse_log_entry(entry)
{
let mut guard = logs.lock().await;
guard.push(log);
if guard.len() > 1000 {
guard.remove(0);
}
}
}
});
Ok(())
}
pub async fn get_logs(&self, clear: bool) -> Vec<ConsoleLog> {
let mut guard = self.logs.lock().await;
let result = guard.clone();
if clear {
guard.clear();
}
result
}
pub async fn get_logs_by_level(&self, level: &str) -> Vec<ConsoleLog> {
let guard = self.logs.lock().await;
guard
.iter()
.filter(|l| l.level.eq_ignore_ascii_case(level))
.cloned()
.collect()
}
pub async fn clear_logs(&self) {
self.logs.lock().await.clear();
}
pub async fn log_count(&self) -> usize {
self.logs.lock().await.len()
}
}
fn parse_console_event(params: &Value) -> Option<ConsoleLog> {
let level = params
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("log")
.to_string();
let args = params.get("args")?;
let text = extract_console_text(args);
let url = params
.get("stackTrace")
.and_then(|st| st.get("callFrames"))
.and_then(|cf| cf.as_array())
.and_then(|arr| arr.first())
.and_then(|frame| frame.get("url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let line = params
.get("stackTrace")
.and_then(|st| st.get("callFrames"))
.and_then(|cf| cf.as_array())
.and_then(|arr| arr.first())
.and_then(|frame| frame.get("lineNumber"))
.and_then(|v| v.as_u64())
.map(|n| n as u32);
let column = params
.get("stackTrace")
.and_then(|st| st.get("callFrames"))
.and_then(|cf| cf.as_array())
.and_then(|arr| arr.first())
.and_then(|frame| frame.get("columnNumber"))
.and_then(|v| v.as_u64())
.map(|n| n as u32);
let timestamp = params
.get("timestamp")
.and_then(|v| v.as_f64())
.or_else(|| Some(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_secs_f64()));
Some(ConsoleLog {
level,
text,
url,
line,
column,
timestamp,
})
}
fn parse_log_entry(entry: &Value) -> Option<ConsoleLog> {
let level = entry
.get("level")
.and_then(|v| v.as_str())
.unwrap_or("log")
.to_string();
let text = entry
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let url = entry
.get("source")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let line = entry
.get("lineNumber")
.and_then(|v| v.as_u64())
.map(|n| n as u32);
let column = entry
.get("columnNumber")
.and_then(|v| v.as_u64())
.map(|n| n as u32);
let timestamp = entry
.get("timestamp")
.and_then(|v| v.as_f64())
.or_else(|| Some(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_secs_f64()));
Some(ConsoleLog {
level,
text,
url,
line,
column,
timestamp,
})
}
fn extract_console_text(args: &Value) -> String {
let arr = match args.as_array() {
Some(a) => a,
None => return String::new(),
};
let parts: Vec<String> = arr
.iter()
.map(|arg| {
if let Some(s) = arg.get("value").and_then(|v| v.as_str()) {
s.to_string()
} else if let Some(s) = arg.as_str() {
s.to_string()
} else {
arg.to_string()
}
})
.collect();
parts.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_console_text() {
let args = serde_json::json!([
{ "type": "string", "value": "hello" },
{ "type": "string", "value": "world" }
]);
assert_eq!(extract_console_text(&args), "hello world");
}
#[test]
fn test_parse_console_event() {
let params = serde_json::json!({
"type": "error",
"args": [
{ "type": "string", "value": "Something failed" }
],
"stackTrace": {
"callFrames": [
{ "url": "https://example.com/app.js", "lineNumber": 42, "columnNumber": 10 }
]
},
"timestamp": 1234567890.0
});
let log = parse_console_event(¶ms).unwrap();
assert_eq!(log.level, "error");
assert_eq!(log.text, "Something failed");
assert_eq!(log.url, Some("https://example.com/app.js".to_string()));
assert_eq!(log.line, Some(42));
assert_eq!(log.column, Some(10));
assert_eq!(log.timestamp, Some(1234567890.0));
}
#[test]
fn test_parse_log_entry() {
let entry = serde_json::json!({
"level": "warning",
"text": "deprecated API",
"source": "https://example.com/old.js",
"lineNumber": 5,
"columnNumber": 1,
"timestamp": 1234567890.0
});
let log = parse_log_entry(&entry).unwrap();
assert_eq!(log.level, "warning");
assert_eq!(log.text, "deprecated API");
assert_eq!(log.url, Some("https://example.com/old.js".to_string()));
assert_eq!(log.line, Some(5));
assert_eq!(log.column, Some(1));
}
}