use std::fs::OpenOptions;
use std::io::ErrorKind;
use std::os::unix::fs::OpenOptionsExt as _;
use std::path::PathBuf;
use argonauts_core::Paths;
use argonauts_core::create_private_dir_all;
use argonauts_core::settings::Settings;
use super::event::FIELDS_VERSION;
pub const LINE: &str = "Sends one anonymous usage report a day, never your code or prompts \
(PRIVACY.md) ยท argonauts telemetry off stops it";
pub const RUN_LINE: &str = "argonauts: one anonymous usage report a day, never your code or \
prompts (PRIVACY.md). argonauts telemetry off stops it";
pub const STOP_COMMAND: &str = "argonauts telemetry off";
pub(crate) fn marker(paths: &Paths) -> PathBuf {
paths
.home()
.join("state")
.join(format!("telemetry-notice-v{FIELDS_VERSION}"))
}
pub(crate) fn shown(paths: &Paths) -> bool {
marker(paths).exists()
}
pub(crate) fn claim(paths: &Paths, settings: &Settings) -> bool {
claim_in(paths, settings, &|name| std::env::var(name).ok())
}
pub(crate) fn claim_in(
paths: &Paths,
settings: &Settings,
var: &dyn Fn(&str) -> Option<String>,
) -> bool {
if !settings.telemetry.enabled || quiet(var) {
return false;
}
let marker = marker(paths);
let made = marker
.parent()
.map_or(Ok(()), create_private_dir_all)
.and_then(|()| {
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&marker)
.map(drop)
});
match made {
Ok(()) => true,
Err(err) if err.kind() == ErrorKind::AlreadyExists => false,
Err(err) => {
tracing::debug!("couldn't keep that the usage line showed: {err}");
true
}
}
}
pub(crate) fn quiet(var: &dyn Fn(&str) -> Option<String>) -> bool {
let set = |name: &str| {
var(name).is_some_and(|value| {
!matches!(
value.trim().to_ascii_lowercase().as_str(),
"" | "0" | "false"
)
})
};
set("CI") || set("DO_NOT_TRACK") || var("ARGONAUTS_SESSION").is_some()
}
#[cfg(test)]
mod tests {
use super::*;
fn on() -> Settings {
let mut settings = Settings::default();
settings.telemetry.enabled = true;
settings
}
fn claim(paths: &Paths, settings: &Settings) -> bool {
claim_in(paths, settings, &|_| None)
}
#[test]
fn the_line_shows_once_per_field_list() {
let home = tempfile::tempdir().unwrap();
let paths = Paths::new(home.path());
assert!(!shown(&paths));
assert!(claim(&paths, &on()));
assert!(shown(&paths));
assert!(!claim(&paths, &on()), "once");
assert!(marker(&paths).ends_with("state/telemetry-notice-v3"));
std::fs::remove_file(marker(&paths)).unwrap();
std::fs::write(paths.home().join("state/telemetry-notice-v1"), "").unwrap();
assert!(claim(&paths, &on()));
let ci = tempfile::tempdir().unwrap();
let ci = Paths::new(ci.path());
assert!(!claim_in(&ci, &on(), &|name| (name == "CI")
.then(|| "true".to_owned())));
assert!(!shown(&ci));
}
#[test]
fn of_two_starting_together_one_shows_the_line() {
let home = tempfile::tempdir().unwrap();
let paths = Paths::new(home.path());
let settings = on();
let shown: Vec<bool> = std::thread::scope(|scope| {
let racers: Vec<_> = (0..8)
.map(|_| scope.spawn(|| claim(&paths, &settings)))
.collect();
racers
.into_iter()
.map(|racer| racer.join().unwrap())
.collect()
});
assert_eq!(shown.iter().filter(|shown| **shown).count(), 1, "{shown:?}");
}
#[test]
fn reports_off_show_no_line() {
let home = tempfile::tempdir().unwrap();
let paths = Paths::new(home.path());
assert!(!claim(
&paths,
&Settings {
telemetry: argonauts_core::settings::telemetry::TelemetrySettings {
enabled: false
},
..Settings::default()
}
));
assert!(!shown(&paths));
}
#[test]
fn ci_do_not_track_and_an_agents_session_are_quiet() {
let env = |pairs: &'static [(&'static str, &'static str)]| {
move |name: &str| {
pairs
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| (*value).to_owned())
}
};
assert!(quiet(&env(&[("CI", "true")])));
assert!(quiet(&env(&[("CI", "1")])));
assert!(!quiet(&env(&[("CI", "false")])));
assert!(!quiet(&env(&[("CI", "")])));
assert!(quiet(&env(&[("DO_NOT_TRACK", "1")])));
assert!(!quiet(&env(&[("DO_NOT_TRACK", "0")])));
assert!(quiet(&env(&[("ARGONAUTS_SESSION", "")])));
assert!(!quiet(&env(&[])));
}
}