use crate::confidence::Stage;
use crate::inject::Rec;
use serde_json::json;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
static CONFIG_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn init(config_enabled: bool) {
CONFIG_ENABLED.store(config_enabled, std::sync::atomic::Ordering::Relaxed);
}
pub fn enabled() -> bool {
CONFIG_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
|| matches!(
std::env::var("SKI_TELEMETRY").ok().as_deref(),
Some("1") | Some("true") | Some("yes") | Some("on")
)
}
pub fn record_recommend(
session_id: &str,
prompt: &str,
stage: Stage,
considered: &[(String, f32)],
recs: &[Rec],
injected: &[(String, f32)],
abstained: Option<&str>,
) {
if !enabled() {
return;
}
let considered: Vec<_> = considered
.iter()
.map(|(id, s)| json!({ "id": id, "score": s }))
.collect();
let candidates: Vec<_> = recs
.iter()
.map(|r| json!({ "id": r.id, "confidence": r.confidence }))
.collect();
let injected: Vec<_> = injected
.iter()
.map(|(id, c)| json!({ "id": id, "confidence": c }))
.collect();
let mut ev = json!({
"ts": now_ms(),
"kind": "recommend",
"session": session_id,
"prompt": prompt,
"stage": stage_str(stage),
"considered": considered,
"candidates": candidates,
"injected": injected,
});
if let Some(reason) = abstained {
ev["abstained"] = json!(reason);
}
append(&ev);
}
pub fn record_use(session_id: &str, skill_id: &str, via: &str, prompt: &str) {
if !enabled() {
return;
}
let mut ev = json!({
"ts": now_ms(),
"kind": "use",
"session": session_id,
"skill": skill_id,
"via": via,
});
if !prompt.is_empty() {
ev["prompt"] = json!(prompt);
}
append(&ev);
}
fn append(ev: &serde_json::Value) {
let path = crate::paths::telemetry_path();
let _ = (|| -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut f = OpenOptions::new().create(true).append(true).open(&path)?;
writeln!(f, "{ev}")?;
Ok(())
})();
}
fn stage_str(stage: Stage) -> &'static str {
match stage {
Stage::Cosine => "cosine",
Stage::Rerank => "rerank",
Stage::Lexical => "lexical",
}
}
fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_by_default() {
std::env::remove_var("SKI_TELEMETRY");
assert!(!enabled());
record_use("s", "pdf", "skill", "");
}
#[test]
fn stage_strings() {
assert_eq!(stage_str(Stage::Cosine), "cosine");
assert_eq!(stage_str(Stage::Rerank), "rerank");
}
}