tj_core/classifier/
telemetry.rs1use anyhow::Context;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct TelemetryRecord {
9 pub timestamp: String,
10 pub project_hash: String,
11 pub task_id_guess: Option<String>,
12 pub event_type: String,
13 pub confidence: f64,
14 pub status: String,
15 pub error: Option<String>,
16}
17
18pub fn append(metrics_path: impl AsRef<Path>, record: &TelemetryRecord) -> anyhow::Result<()> {
19 if let Some(parent) = metrics_path.as_ref().parent() {
20 std::fs::create_dir_all(parent)?;
21 }
22 let line = serde_json::to_string(record).context("serialize telemetry")?;
23 use std::io::Write;
24 let mut f = std::fs::OpenOptions::new()
25 .create(true)
26 .append(true)
27 .open(&metrics_path)?;
28 writeln!(f, "{line}")?;
29 Ok(())
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35 use tempfile::TempDir;
36
37 #[test]
38 fn append_and_read_back_roundtrip() {
39 let d = TempDir::new().unwrap();
40 let path = d.path().join("metrics.jsonl");
41
42 let r1 = TelemetryRecord {
43 timestamp: "2026-04-30T00:00:00Z".into(),
44 project_hash: "feedface".into(),
45 task_id_guess: Some("tj-x".into()),
46 event_type: "decision".into(),
47 confidence: 0.92,
48 status: "confirmed".into(),
49 error: None,
50 };
51 let r2 = TelemetryRecord {
52 confidence: 0.4,
53 status: "suggested".into(),
54 ..r1.clone()
55 };
56 append(&path, &r1).unwrap();
57 append(&path, &r2).unwrap();
58
59 let body = std::fs::read_to_string(&path).unwrap();
60 assert_eq!(body.lines().count(), 2);
61 }
62}