use std::path::Path;
use super::config::{read_consent, ConsentFile};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsentState {
Enabled,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecidedBy {
DoNotTrackEnv,
OpenlatchDisabledEnv,
CiEnvironment,
ConfigFile,
DefaultUnconsented,
NoBakedKey,
}
#[derive(Debug, Clone, Copy)]
pub struct Resolved {
pub state: ConsentState,
pub decided_by: DecidedBy,
}
impl Resolved {
pub fn enabled(&self) -> bool {
self.state == ConsentState::Enabled
}
}
pub fn resolve(config_path: &Path) -> Resolved {
if is_truthy_env("DO_NOT_TRACK") {
return Resolved {
state: ConsentState::Disabled,
decided_by: DecidedBy::DoNotTrackEnv,
};
}
if is_truthy_env("OPENLATCH_TELEMETRY_DISABLED") {
return Resolved {
state: ConsentState::Disabled,
decided_by: DecidedBy::OpenlatchDisabledEnv,
};
}
if in_ci() {
return Resolved {
state: ConsentState::Disabled,
decided_by: DecidedBy::CiEnvironment,
};
}
match read_consent(config_path) {
Ok(Some(ConsentFile { enabled, .. })) => Resolved {
state: if enabled {
ConsentState::Enabled
} else {
ConsentState::Disabled
},
decided_by: DecidedBy::ConfigFile,
},
Ok(None) => Resolved {
state: ConsentState::Disabled,
decided_by: DecidedBy::DefaultUnconsented,
},
Err(_) => Resolved {
state: ConsentState::Disabled,
decided_by: DecidedBy::ConfigFile,
},
}
}
fn is_truthy_env(name: &str) -> bool {
std::env::var(name).is_ok_and(|v| {
let v = v.trim().to_ascii_lowercase();
!matches!(v.as_str(), "" | "0" | "false" | "no" | "off")
})
}
pub fn in_ci() -> bool {
const CI_VARS: &[&str] = &[
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"CIRCLECI",
"JENKINS_URL",
"BUILDKITE",
"TF_BUILD",
"TEAMCITY_VERSION",
"BITBUCKET_BUILD_NUMBER",
];
CI_VARS
.iter()
.any(|v| std::env::var(v).is_ok_and(|x| !x.is_empty()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrashDecidedBy {
CrashReportingEnv,
NoProjectKey,
ConfigFile,
DefaultEnabled,
}
#[derive(Debug, Clone, Copy)]
pub struct CrashResolved {
pub state: ConsentState,
pub decided_by: CrashDecidedBy,
}
impl CrashResolved {
pub fn enabled(&self) -> bool {
self.state == ConsentState::Enabled
}
}
pub fn resolve_crash(config_path: &Path, key_present: bool) -> CrashResolved {
if is_falsy_env("OPENLATCH_CRASH_REPORTING") {
return CrashResolved {
state: ConsentState::Disabled,
decided_by: CrashDecidedBy::CrashReportingEnv,
};
}
if !key_present {
return CrashResolved {
state: ConsentState::Disabled,
decided_by: CrashDecidedBy::NoProjectKey,
};
}
match super::config::read_crashreport_section(config_path) {
Ok(Some(section)) => CrashResolved {
state: if section.enabled {
ConsentState::Enabled
} else {
ConsentState::Disabled
},
decided_by: CrashDecidedBy::ConfigFile,
},
Ok(None) | Err(_) => CrashResolved {
state: ConsentState::Enabled,
decided_by: CrashDecidedBy::DefaultEnabled,
},
}
}
fn is_falsy_env(name: &str) -> bool {
std::env::var(name).is_ok_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "no" | "off"
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tempfile::TempDir;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn clear_env() {
for v in [
"DO_NOT_TRACK",
"OPENLATCH_TELEMETRY_DISABLED",
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"CIRCLECI",
"JENKINS_URL",
"BUILDKITE",
"TF_BUILD",
"TEAMCITY_VERSION",
"BITBUCKET_BUILD_NUMBER",
] {
std::env::remove_var(v);
}
}
#[test]
fn test_do_not_track_hard_overrides_enabled_config() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
std::env::set_var("DO_NOT_TRACK", "1");
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
super::super::config::write_consent(&path, true).unwrap();
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::DoNotTrackEnv);
clear_env();
}
#[test]
fn test_openlatch_disabled_env_wins_over_config() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
std::env::set_var("OPENLATCH_TELEMETRY_DISABLED", "1");
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
super::super::config::write_consent(&path, true).unwrap();
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::OpenlatchDisabledEnv);
clear_env();
}
#[test]
fn test_ci_environment_disables_even_when_enabled() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
std::env::set_var("GITHUB_ACTIONS", "true");
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
super::super::config::write_consent(&path, true).unwrap();
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::CiEnvironment);
clear_env();
}
#[test]
fn test_missing_file_defaults_to_disabled_unconsented() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::DefaultUnconsented);
}
#[test]
fn test_enabled_config_with_no_env_overrides_is_enabled() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
super::super::config::write_consent(&path, true).unwrap();
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Enabled);
assert_eq!(r.decided_by, DecidedBy::ConfigFile);
}
#[test]
fn test_falsy_env_values_do_not_disable() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
std::env::set_var("DO_NOT_TRACK", "0");
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
super::super::config::write_consent(&path, true).unwrap();
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Enabled);
assert_eq!(r.decided_by, DecidedBy::ConfigFile);
clear_env();
}
fn clear_crash_env() {
std::env::remove_var("OPENLATCH_CRASH_REPORTING");
std::env::remove_var("SENTRY_DISABLED");
}
#[test]
fn crash_env_falsy_disables() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
for value in ["0", "false", "no", "off", "FALSE", "Off", " 0 "] {
std::env::set_var("OPENLATCH_CRASH_REPORTING", value);
let r = resolve_crash(&tmp.path().join("config.toml"), true);
assert_eq!(r.state, ConsentState::Disabled, "value: {value:?}");
assert_eq!(r.decided_by, CrashDecidedBy::CrashReportingEnv);
}
clear_crash_env();
}
#[test]
fn crash_env_truthy_leaves_reporting_on() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
for value in ["1", "true", "yes", ""] {
std::env::set_var("OPENLATCH_CRASH_REPORTING", value);
let r = resolve_crash(&tmp.path().join("config.toml"), true);
assert_eq!(r.state, ConsentState::Enabled, "value: {value:?}");
}
clear_crash_env();
}
#[test]
fn sentry_disabled_has_no_effect() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
std::env::set_var("SENTRY_DISABLED", "1");
let tmp = TempDir::new().unwrap();
let r = resolve_crash(&tmp.path().join("config.toml"), true);
assert_eq!(r.state, ConsentState::Enabled);
assert_eq!(r.decided_by, CrashDecidedBy::DefaultEnabled);
clear_crash_env();
}
#[test]
fn crash_disabled_without_a_project_key() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
let r = resolve_crash(&tmp.path().join("config.toml"), false);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, CrashDecidedBy::NoProjectKey);
}
#[test]
fn crash_config_false_disables() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("config.toml");
std::fs::write(
&path,
"[crashreport]
enabled = false
",
)
.unwrap();
let r = resolve_crash(&path, true);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, CrashDecidedBy::ConfigFile);
}
#[test]
fn crash_config_true_reports_config_file() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("config.toml");
std::fs::write(
&path,
"[crashreport]
enabled = true
",
)
.unwrap();
let r = resolve_crash(&path, true);
assert_eq!(r.state, ConsentState::Enabled);
assert_eq!(r.decided_by, CrashDecidedBy::ConfigFile);
}
#[test]
fn crash_enabled_by_default_when_the_section_is_absent() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
let r = resolve_crash(&tmp.path().join("config.toml"), true);
assert_eq!(r.state, ConsentState::Enabled);
assert_eq!(r.decided_by, CrashDecidedBy::DefaultEnabled);
}
#[test]
fn crash_enabled_when_the_config_is_corrupt() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_crash_env();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("config.toml");
std::fs::write(
&path,
b"[crashreport
enabled = ",
)
.unwrap();
let r = resolve_crash(&path, true);
assert_eq!(r.state, ConsentState::Enabled);
assert_eq!(r.decided_by, CrashDecidedBy::DefaultEnabled);
}
#[test]
fn the_two_gates_are_independent() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
clear_crash_env();
let tmp = TempDir::new().unwrap();
let telemetry_path = tmp.path().join("telemetry.json");
let config_path = tmp.path().join("config.toml");
for (telemetry_on, crash_on) in [(true, true), (true, false), (false, true), (false, false)]
{
super::super::config::write_consent(&telemetry_path, telemetry_on).unwrap();
std::fs::write(
&config_path,
format!(
"[crashreport]
enabled = {crash_on}
"
),
)
.unwrap();
let telemetry = resolve(&telemetry_path);
let crash = resolve_crash(&config_path, true);
assert_eq!(
telemetry.enabled(),
telemetry_on,
"product telemetry moved with crash={crash_on}"
);
assert_eq!(
crash.enabled(),
crash_on,
"crash reporting moved with telemetry={telemetry_on}"
);
}
clear_env();
clear_crash_env();
}
#[test]
fn neither_gate_env_rung_reaches_the_other() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
clear_crash_env();
let tmp = TempDir::new().unwrap();
let telemetry_path = tmp.path().join("telemetry.json");
let config_path = tmp.path().join("config.toml");
super::super::config::write_consent(&telemetry_path, true).unwrap();
std::env::set_var("DO_NOT_TRACK", "1");
assert!(!resolve(&telemetry_path).enabled());
assert!(
resolve_crash(&config_path, true).enabled(),
"DO_NOT_TRACK must not silence crash reports"
);
clear_env();
std::env::set_var("CI", "true");
assert!(!resolve(&telemetry_path).enabled());
assert!(
resolve_crash(&config_path, true).enabled(),
"a CI run must still report panics"
);
clear_env();
std::env::set_var("OPENLATCH_CRASH_REPORTING", "0");
assert!(!resolve_crash(&config_path, true).enabled());
assert!(
resolve(&telemetry_path).enabled(),
"the crash opt-out must not disable product telemetry"
);
clear_crash_env();
clear_env();
}
#[test]
fn test_corrupt_config_file_resolves_disabled() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
clear_env();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("telemetry.json");
std::fs::write(&path, b"{ not json").unwrap();
let r = resolve(&path);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::ConfigFile);
}
}