use std::io::Write;
use std::path::{Path, PathBuf};
pub(crate) fn ops_metrics_path(repo: &Path) -> PathBuf {
repo.join(".lf")
.join("tmp")
.join("metrics")
.join("ops.jsonl")
}
pub(crate) fn record_ops_metric(repo: &Path, mut event: serde_json::Value) {
let Some(object) = event.as_object_mut() else {
return;
};
object.insert(
"ts".to_string(),
serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
);
let path = ops_metrics_path(repo);
let Some(parent) = path.parent() else {
return;
};
if std::fs::create_dir_all(parent).is_err() {
return;
}
let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
else {
return;
};
if serde_json::to_writer(&mut file, &event).is_ok() {
let _ = writeln!(file);
}
}
#[cfg(test)]
mod tests {
use super::{ops_metrics_path, record_ops_metric};
use std::path::Path;
use std::process::Command;
fn git(repo: &Path, args: &[&str]) {
let ok = Command::new("git")
.args(args)
.current_dir(repo)
.status()
.expect("git runs")
.success();
assert!(ok, "git {args:?} failed");
}
fn porcelain(repo: &Path) -> String {
let out = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(repo)
.output()
.expect("git status runs");
String::from_utf8(out.stdout).expect("utf8")
}
fn ignored(repo: &Path, path: &Path) -> bool {
Command::new("git")
.args(["check-ignore", "--quiet", "--"])
.arg(path)
.current_dir(repo)
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn clean_fixture() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let repo = dir.path();
git(repo, &["init", "-q"]);
git(repo, &["config", "user.email", "test@example.com"]);
git(repo, &["config", "user.name", "Test"]);
std::fs::write(repo.join(".gitignore"), ".lf/tmp/\n").expect("write .gitignore");
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
assert_eq!(porcelain(repo), "", "fixture should start clean");
dir
}
#[test]
fn ops_metrics_path_targets_ignored_tree_not_legacy_tracked_path() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = dir.path();
let path = ops_metrics_path(repo);
assert!(
path.starts_with(repo.join(".lf").join("tmp")),
"telemetry must live under the ignored .lf/tmp/ tree, got {}",
path.display()
);
assert!(
!path.starts_with(repo.join(".lf").join("metrics")),
"telemetry must never target the legacy tracked .lf/metrics/ path, got {}",
path.display()
);
assert_eq!(
path.file_name(),
Some(std::ffi::OsStr::new("ops.jsonl")),
"telemetry file name is stable"
);
}
#[test]
fn recording_telemetry_leaves_a_clean_checkout() {
let dir = clean_fixture();
let repo = dir.path();
record_ops_metric(repo, serde_json::json!({ "op": "rebase", "class": "noop" }));
let path = ops_metrics_path(repo);
assert!(path.exists(), "telemetry file must be written");
assert!(
ignored(repo, &path),
"telemetry path {} must be git-ignored",
path.display()
);
assert_eq!(
porcelain(repo),
"",
"recording telemetry must not dirty a clean worktree"
);
}
#[test]
fn non_object_event_is_dropped_not_written() {
let dir = clean_fixture();
let repo = dir.path();
record_ops_metric(repo, serde_json::json!(["not", "an", "object"]));
let path = ops_metrics_path(repo);
assert!(
!path.exists(),
"a non-object event must not create the file"
);
assert_eq!(porcelain(repo), "", "dropped event must not dirty the tree");
}
}