use std::path::Path;
use super::config::read_section;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsentState {
Enabled,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecidedBy {
DisabledEnv,
UniversalOptOut,
NoBakedDsn,
ConfigFile,
DefaultEnabled,
}
#[derive(Debug, Clone, Copy)]
pub struct Resolved {
pub state: ConsentState,
#[allow(dead_code)]
pub decided_by: DecidedBy,
}
impl Resolved {
pub fn enabled(&self) -> bool {
self.state == ConsentState::Enabled
}
fn disabled(decided_by: DecidedBy) -> Self {
Self {
state: ConsentState::Disabled,
decided_by,
}
}
}
pub fn resolve(config_path: &Path, dsn_present: bool) -> Resolved {
if is_truthy_env("SENTRY_DISABLED") || is_truthy_env("SAFERSKILLS_NO_CRASH_REPORT") {
return Resolved::disabled(DecidedBy::DisabledEnv);
}
if env_present("SAFERSKILLS_NO_TELEMETRY") || env_present("DO_NOT_TRACK") || env_present("CI") {
return Resolved::disabled(DecidedBy::UniversalOptOut);
}
if !dsn_present {
return Resolved::disabled(DecidedBy::NoBakedDsn);
}
match read_section(config_path) {
Ok(Some(section)) if !section.enabled => Resolved::disabled(DecidedBy::ConfigFile),
Ok(Some(_)) => Resolved {
state: ConsentState::Enabled,
decided_by: DecidedBy::ConfigFile,
},
Ok(None) | Err(_) => Resolved {
state: ConsentState::Enabled,
decided_by: DecidedBy::DefaultEnabled,
},
}
}
fn env_present(key: &str) -> bool {
std::env::var_os(key).is_some_and(|v| !v.is_empty())
}
fn is_truthy_env(name: &str) -> bool {
match std::env::var(name) {
Ok(v) => {
let v = v.trim().to_ascii_lowercase();
!matches!(v.as_str(), "" | "0" | "false" | "no" | "off")
}
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tempfile::TempDir;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const ENVS: &[&str] = &[
"SENTRY_DISABLED",
"SAFERSKILLS_NO_CRASH_REPORT",
"SAFERSKILLS_NO_TELEMETRY",
"DO_NOT_TRACK",
"CI",
];
fn with_clean_env<F: FnOnce()>(f: F) {
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
for k in ENVS {
std::env::remove_var(k);
}
f();
for k in ENVS {
std::env::remove_var(k);
}
}
#[test]
fn disabled_when_sentry_disabled_env_set() {
with_clean_env(|| {
std::env::set_var("SENTRY_DISABLED", "1");
let tmp = TempDir::new().unwrap();
let r = resolve(&tmp.path().join("config.toml"), true);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::DisabledEnv);
});
}
#[test]
fn disabled_when_universal_opt_out_set() {
with_clean_env(|| {
std::env::set_var("DO_NOT_TRACK", "1");
let tmp = TempDir::new().unwrap();
let r = resolve(&tmp.path().join("config.toml"), true);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::UniversalOptOut);
});
}
#[test]
fn disabled_when_no_dsn() {
with_clean_env(|| {
let tmp = TempDir::new().unwrap();
let r = resolve(&tmp.path().join("config.toml"), false);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::NoBakedDsn);
});
}
#[test]
fn disabled_when_config_says_false() {
with_clean_env(|| {
let tmp = TempDir::new().unwrap();
let p = tmp.path().join("config.toml");
std::fs::write(&p, "[crashreport]\nenabled = false\n").unwrap();
let r = resolve(&p, true);
assert_eq!(r.state, ConsentState::Disabled);
assert_eq!(r.decided_by, DecidedBy::ConfigFile);
});
}
#[test]
fn enabled_by_default_when_config_missing() {
with_clean_env(|| {
let tmp = TempDir::new().unwrap();
let r = resolve(&tmp.path().join("config.toml"), true);
assert!(r.enabled());
assert_eq!(r.decided_by, DecidedBy::DefaultEnabled);
});
}
#[test]
fn enabled_when_config_says_true() {
with_clean_env(|| {
let tmp = TempDir::new().unwrap();
let p = tmp.path().join("config.toml");
std::fs::write(&p, "[crashreport]\nenabled = true\n").unwrap();
let r = resolve(&p, true);
assert!(r.enabled());
assert_eq!(r.decided_by, DecidedBy::ConfigFile);
});
}
}