use std::fs;
use std::io::Write;
use std::path::Path;
use mesh_llm_events::disclosure_err;
pub const NOTICE_MARKER_FILE: &str = "analytics-notice-shown";
pub const NOTICE: &str = "\
mesh-llm reports anonymous usage data (version, platform, which commands run,
how many nodes and models are in play) so we can see what to improve. It never
includes prompts, completions, model contents, file paths, IP addresses, or
anything about your mesh peers.
Turn it off: mesh-llm analytics disable
What we send: https://meshllm.cloud/docs/pages/analytics/
This notice is shown once.";
#[must_use]
pub fn was_shown(dir: &Path) -> bool {
dir.join(NOTICE_MARKER_FILE).exists()
}
fn mark_shown(dir: &Path) {
let _ = fs::create_dir_all(dir);
let _ = fs::write(dir.join(NOTICE_MARKER_FILE), b"1\n");
}
#[must_use]
pub fn print_notice(dir: &Path) -> bool {
let mut handle = disclosure_err();
show_notice(&mut handle, dir)
}
fn show_notice(sink: &mut dyn Write, dir: &Path) -> bool {
if writeln!(sink, "\n{NOTICE}\n").is_err() || sink.flush().is_err() {
return false;
}
mark_shown(dir);
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
use tempfile::TempDir;
struct FailingSink;
impl Write for FailingSink {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "no reader"))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "no reader"))
}
}
#[test]
fn marker_is_absent_until_the_notice_is_printed() {
let dir = TempDir::new().expect("tempdir");
assert!(!was_shown(dir.path()));
assert!(print_notice(dir.path()), "stderr accepted the notice");
assert!(was_shown(dir.path()));
}
#[test]
fn an_undelivered_notice_is_not_recorded_as_shown() {
let dir = TempDir::new().expect("tempdir");
let mut sink = FailingSink;
assert!(!show_notice(&mut sink, dir.path()));
assert!(
!was_shown(dir.path()),
"a notice that was not delivered must stay pending"
);
}
#[test]
fn a_delivered_notice_is_recorded_and_states_the_opt_out() {
let dir = TempDir::new().expect("tempdir");
let mut sink = Vec::new();
assert!(show_notice(&mut sink, dir.path()));
assert!(was_shown(dir.path()));
let text = String::from_utf8(sink).expect("notice is utf-8");
assert!(
text.contains("mesh-llm analytics disable"),
"delivered notice omits the opt-out: {text}"
);
}
#[test]
fn marker_is_independent_of_the_install_identifier() {
let dir = TempDir::new().expect("tempdir");
crate::load_or_create(dir.path()).expect("create id");
assert!(
!was_shown(dir.path()),
"install id must not mark disclosure"
);
}
#[test]
fn notice_states_the_opt_out_and_the_exclusions() {
assert!(NOTICE.contains("mesh-llm analytics disable"));
assert!(NOTICE.contains("anonymous"));
for excluded in ["prompts", "completions", "file paths", "IP addresses"] {
assert!(NOTICE.contains(excluded), "notice omits {excluded}");
}
}
#[test]
fn notice_has_no_trailing_whitespace() {
for line in NOTICE.lines() {
assert_eq!(line, line.trim_end(), "trailing whitespace in notice");
}
}
}