use serde::{Deserialize, Serialize};
use std::path::PathBuf;
pub const DEFAULT_ENDPOINT: &str = "https://acs.atomgit.com/api/v1/events";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TelemetryConfig {
pub enabled: Option<bool>,
pub endpoint: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct CliOverride {
pub disabled: bool,
}
#[derive(Debug, Clone)]
pub struct ResolvedConfig {
pub state: TelemetryState,
pub endpoint: String,
pub atomcode_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub enum TelemetryState {
Enabled,
Disabled(&'static str),
}
impl TelemetryState {
pub fn is_enabled(&self) -> bool {
matches!(self, TelemetryState::Enabled)
}
pub fn reason(&self) -> Option<&'static str> {
match self {
TelemetryState::Disabled(r) => Some(r),
_ => None,
}
}
}
pub fn resolve(
cfg: &TelemetryConfig,
cli: &CliOverride,
atomcode_dir: PathBuf,
env: &impl EnvLookup,
) -> ResolvedConfig {
let state = if env.var("ATOMCODE_TELEMETRY").as_deref() == Some("0") {
TelemetryState::Disabled("env:ATOMCODE_TELEMETRY=0")
} else if env.var("DO_NOT_TRACK").as_deref() == Some("1") {
TelemetryState::Disabled("env:DO_NOT_TRACK=1")
} else if cli.disabled {
TelemetryState::Disabled("cli:--no-telemetry")
} else if matches!(cfg.enabled, Some(false)) {
TelemetryState::Disabled("config")
} else {
TelemetryState::Enabled
};
let endpoint = env
.var("ATOMCODE_TELEMETRY_ENDPOINT")
.or_else(|| cfg.endpoint.clone())
.unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
ResolvedConfig {
state,
endpoint,
atomcode_dir,
}
}
pub trait EnvLookup {
fn var(&self, key: &str) -> Option<String>;
}
pub struct ProcessEnv;
impl EnvLookup for ProcessEnv {
fn var(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct MapEnv(HashMap<&'static str, &'static str>);
impl EnvLookup for MapEnv {
fn var(&self, key: &str) -> Option<String> {
self.0.get(key).map(|s| s.to_string())
}
}
fn env(kv: &[(&'static str, &'static str)]) -> MapEnv {
MapEnv(kv.iter().copied().collect())
}
fn dir() -> PathBuf {
PathBuf::from("/tmp/.atomcode-test")
}
#[test]
fn default_is_enabled() {
let r = resolve(
&TelemetryConfig::default(),
&CliOverride::default(),
dir(),
&env(&[]),
);
assert!(r.state.is_enabled());
assert_eq!(r.endpoint, DEFAULT_ENDPOINT);
}
#[test]
fn env_wins_over_config() {
let cfg = TelemetryConfig {
enabled: Some(true),
endpoint: None,
};
let r = resolve(
&cfg,
&CliOverride::default(),
dir(),
&env(&[("ATOMCODE_TELEMETRY", "0")]),
);
assert_eq!(r.state.reason(), Some("env:ATOMCODE_TELEMETRY=0"));
}
#[test]
fn do_not_track_wins_over_cli() {
let r = resolve(
&TelemetryConfig::default(),
&CliOverride { disabled: true },
dir(),
&env(&[("DO_NOT_TRACK", "1")]),
);
assert_eq!(r.state.reason(), Some("env:DO_NOT_TRACK=1"));
}
#[test]
fn cli_wins_over_config() {
let cfg = TelemetryConfig {
enabled: Some(true),
endpoint: None,
};
let r = resolve(&cfg, &CliOverride { disabled: true }, dir(), &env(&[]));
assert_eq!(r.state.reason(), Some("cli:--no-telemetry"));
}
#[test]
fn config_false_disables() {
let cfg = TelemetryConfig {
enabled: Some(false),
endpoint: None,
};
let r = resolve(&cfg, &CliOverride::default(), dir(), &env(&[]));
assert_eq!(r.state.reason(), Some("config"));
}
#[test]
fn endpoint_env_override() {
let r = resolve(
&TelemetryConfig::default(),
&CliOverride::default(),
dir(),
&env(&[("ATOMCODE_TELEMETRY_ENDPOINT", "https://test.example/v1")]),
);
assert_eq!(r.endpoint, "https://test.example/v1");
}
}