atomcode_telemetry/
config.rs1use 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}