Skip to main content

ai_agent/bridge/
debug_utils.rs

1//! Debug utilities for bridge operations.
2//!
3//! Translated from openclaudecode/src/bridge/debugUtils.ts
4
5use once_cell::sync::Lazy;
6use regex::Regex;
7use serde_json::Value;
8
9// =============================================================================
10// CONSTANTS
11// =============================================================================
12
13const DEBUG_MSG_LIMIT: usize = 2000;
14
15static SECRET_FIELD_NAMES: [&str; 5] = [
16    "session_ingress_token",
17    "environment_secret",
18    "access_token",
19    "secret",
20    "token",
21];
22
23static SECRET_PATTERN: Lazy<Regex> = Lazy::new(|| {
24    let pattern = format!(r#""({})"\s*:\s*"([^"]*)""#, SECRET_FIELD_NAMES.join("|"));
25    Regex::new(&pattern).unwrap()
26});
27
28const REDACT_MIN_LENGTH: usize = 16;
29
30// =============================================================================
31// SECRET REDACTION
32// =============================================================================
33
34/// Redact secrets from a string, replacing values with [REDACTED] or a partial reveal.
35pub fn redact_secrets(s: &str) -> String {
36    SECRET_PATTERN
37        .replace_all(s, |caps: &regex::Captures| {
38            let field = caps.get(1).map(|m| m.as_str()).unwrap_or("");
39            let value = caps.get(2).map(|m| m.as_str()).unwrap_or("");
40            if value.len() < REDACT_MIN_LENGTH {
41                return format!(r#""{}":"[REDACTED]""#, field);
42            }
43            let redacted = format!("{}...{}", &value[..8], &value[value.len() - 4..]);
44            format!(r#""{}":"{}""#, field, redacted)
45        })
46        .to_string()
47}
48
49/// Truncate a string for debug logging, collapsing newlines.
50pub fn debug_truncate(s: &str) -> String {
51    let flat = s.replace('\n', "\\n");
52    if flat.len() <= DEBUG_MSG_LIMIT {
53        return flat;
54    }
55    format!("{}... ({} chars)", &flat[..DEBUG_MSG_LIMIT], flat.len())
56}
57
58/// Truncate a JSON-serializable value for debug logging.
59pub fn debug_body(data: &str) -> String {
60    let raw = if let Ok(parsed) = serde_json::from_str::<Value>(data) {
61        serde_json::to_string(&parsed).unwrap_or_else(|_| data.to_string())
62    } else {
63        data.to_string()
64    };
65    let s = redact_secrets(&raw);
66    if s.len() <= DEBUG_MSG_LIMIT {
67        return s;
68    }
69    format!("{}... ({} chars)", &s[..DEBUG_MSG_LIMIT], s.len())
70}
71
72// =============================================================================
73// ERROR EXTRACTION
74// =============================================================================
75
76/// Get the error message from any error type.
77fn error_message(err: &dyn std::error::Error) -> String {
78    err.to_string()
79}
80
81/// Extract a descriptive error message from an axios error (or any error).
82/// For HTTP errors, appends the server's response body message if available.
83pub fn describe_axios_error(err: &serde_json::Value) -> String {
84    let msg = if let Some(err_str) = err.get("message").and_then(|v| v.as_str()) {
85        err_str.to_string()
86    } else {
87        "Unknown error".to_string()
88    };
89
90    if let Some(response) = err.get("response").and_then(|v| v.as_object()) {
91        if let Some(data) = response.get("data").and_then(|v| v.as_object()) {
92            let detail = data.get("message").and_then(|v| v.as_str()).or_else(|| {
93                data.get("error")
94                    .and_then(|v| v.get("message"))
95                    .and_then(|v| v.as_str())
96            });
97
98            if let Some(detail) = detail {
99                return format!("{}: {}", msg, detail);
100            }
101        }
102    }
103    msg
104}
105
106/// Extract the HTTP status code from an axios error, if present.
107/// Returns None for non-HTTP errors (e.g. network failures).
108pub fn extract_http_status(err: &serde_json::Value) -> Option<u16> {
109    let response = err.get("response")?;
110    let status = response.get("status")?;
111    status.as_u64().map(|v| v as u16)
112}
113
114/// Pull a human-readable message out of an API error response body.
115/// Checks `data.message` first, then `data.error.message`.
116pub fn extract_error_detail(data: &serde_json::Value) -> Option<String> {
117    if let Some(msg) = data.get("message").and_then(|v| v.as_str()) {
118        return Some(msg.to_string());
119    }
120    if let Some(error) = data.get("error").and_then(|v| v.as_object()) {
121        if let Some(msg) = error.get("message").and_then(|v| v.as_str()) {
122            return Some(msg.to_string());
123        }
124    }
125    None
126}
127
128// =============================================================================
129// ANALYTICS (STUB)
130// =============================================================================
131
132/// Log a bridge init skip — debug message + `tengu_bridge_repl_skipped`
133/// analytics event.
134pub fn log_bridge_skip(reason: &str, debug_msg: Option<&str>, v2: Option<bool>) {
135    if let Some(msg) = debug_msg {
136        eprintln!("[bridge:debug] {}", msg);
137    }
138    // Analytics event would be logged here
139    let mut event = serde_json::json!({ "reason": reason });
140    if let Some(v2_val) = v2 {
141        event["v2"] = serde_json::json!(v2_val);
142    }
143    eprintln!("[bridge:analytics] tengu_bridge_repl_skipped: {}", event);
144}