Skip to main content

atomcode_telemetry/
config.rs

1//! Telemetry configuration and 4-level opt-out resolution.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6pub const DEFAULT_ENDPOINT: &str = "https://acs.atomgit.com/api/v1/events";
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct TelemetryConfig {
10    pub enabled: Option<bool>,
11    pub endpoint: Option<String>,
12}
13
14#[derive(Debug, Clone, Default)]
15pub struct CliOverride {
16    pub disabled: bool,
17}
18
19#[derive(Debug, Clone)]
20pub struct ResolvedConfig {
21    pub state: TelemetryState,
22    pub endpoint: String,
23    pub atomcode_dir: PathBuf,
24}
25
26#[derive(Debug, Clone)]
27pub enum TelemetryState {
28    Enabled,
29    Disabled(&'static str),
30}
31
32impl TelemetryState {
33    pub fn is_enabled(&self) -> bool {
34        matches!(self, TelemetryState::Enabled)
35    }
36    pub fn reason(&self) -> Option<&'static str> {
37        match self {
38            TelemetryState::Disabled(r) => Some(r),
39            _ => None,
40        }
41    }
42}
43
44pub fn resolve(
45    cfg: &TelemetryConfig,
46    cli: &CliOverride,
47    atomcode_dir: PathBuf,
48    env: &impl EnvLookup,
49) -> ResolvedConfig {
50    let state = if env.var("ATOMCODE_TELEMETRY").as_deref() == Some("0") {
51        TelemetryState::Disabled("env:ATOMCODE_TELEMETRY=0")
52    } else if env.var("DO_NOT_TRACK").as_deref() == Some("1") {
53        TelemetryState::Disabled("env:DO_NOT_TRACK=1")
54    } else if cli.disabled {
55        TelemetryState::Disabled("cli:--no-telemetry")
56    } else if matches!(cfg.enabled, Some(false)) {
57        TelemetryState::Disabled("config")
58    } else {
59        TelemetryState::Enabled
60    };
61
62    let endpoint = env
63        .var("ATOMCODE_TELEMETRY_ENDPOINT")
64        .or_else(|| cfg.endpoint.clone())
65        .unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
66
67    ResolvedConfig {
68        state,
69        endpoint,
70        atomcode_dir,
71    }
72}
73
74pub trait EnvLookup {
75    fn var(&self, key: &str) -> Option<String>;
76}
77
78pub struct ProcessEnv;
79impl EnvLookup for ProcessEnv {
80    fn var(&self, key: &str) -> Option<String> {
81        std::env::var(key).ok()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::collections::HashMap;
89
90    struct MapEnv(HashMap<&'static str, &'static str>);
91    impl EnvLookup for MapEnv {
92        fn var(&self, key: &str) -> Option<String> {
93            self.0.get(key).map(|s| s.to_string())
94        }
95    }
96    fn env(kv: &[(&'static str, &'static str)]) -> MapEnv {
97        MapEnv(kv.iter().copied().collect())
98    }
99    fn dir() -> PathBuf {
100        PathBuf::from("/tmp/.atomcode-test")
101    }
102
103    #[test]
104    fn default_is_enabled() {
105        let r = resolve(
106            &TelemetryConfig::default(),
107            &CliOverride::default(),
108            dir(),
109            &env(&[]),
110        );
111        assert!(r.state.is_enabled());
112        assert_eq!(r.endpoint, DEFAULT_ENDPOINT);
113    }
114
115    #[test]
116    fn env_wins_over_config() {
117        let cfg = TelemetryConfig {
118            enabled: Some(true),
119            endpoint: None,
120        };
121        let r = resolve(
122            &cfg,
123            &CliOverride::default(),
124            dir(),
125            &env(&[("ATOMCODE_TELEMETRY", "0")]),
126        );
127        assert_eq!(r.state.reason(), Some("env:ATOMCODE_TELEMETRY=0"));
128    }
129
130    #[test]
131    fn do_not_track_wins_over_cli() {
132        let r = resolve(
133            &TelemetryConfig::default(),
134            &CliOverride { disabled: true },
135            dir(),
136            &env(&[("DO_NOT_TRACK", "1")]),
137        );
138        assert_eq!(r.state.reason(), Some("env:DO_NOT_TRACK=1"));
139    }
140
141    #[test]
142    fn cli_wins_over_config() {
143        let cfg = TelemetryConfig {
144            enabled: Some(true),
145            endpoint: None,
146        };
147        let r = resolve(&cfg, &CliOverride { disabled: true }, dir(), &env(&[]));
148        assert_eq!(r.state.reason(), Some("cli:--no-telemetry"));
149    }
150
151    #[test]
152    fn config_false_disables() {
153        let cfg = TelemetryConfig {
154            enabled: Some(false),
155            endpoint: None,
156        };
157        let r = resolve(&cfg, &CliOverride::default(), dir(), &env(&[]));
158        assert_eq!(r.state.reason(), Some("config"));
159    }
160
161    #[test]
162    fn endpoint_env_override() {
163        let r = resolve(
164            &TelemetryConfig::default(),
165            &CliOverride::default(),
166            dir(),
167            &env(&[("ATOMCODE_TELEMETRY_ENDPOINT", "https://test.example/v1")]),
168        );
169        assert_eq!(r.endpoint, "https://test.example/v1");
170    }
171}