use std::path::{Path, PathBuf};
use codewhale_config::{ResolvedRuntimeOptions, SetupState, TELEMETRY_NOTICE_VERSION};
use crate::buffer;
use crate::event::Surface;
pub const TELEMETRY_DIR: &str = "telemetry";
#[derive(Debug)]
pub enum TelemetryDecision {
Enabled(TelemetryConsent),
OptedOut,
ForcedOff,
}
impl TelemetryDecision {
#[must_use]
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled(_))
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::Enabled(_) => "enabled",
Self::OptedOut => "opted_out",
Self::ForcedOff => "forced_off",
}
}
}
#[derive(Debug)]
pub struct TelemetryConsent {
root: PathBuf,
endpoint: Option<String>,
surface: Surface,
config_path: Option<PathBuf>,
}
impl TelemetryConsent {
#[must_use]
pub fn with_config_path(mut self, config_path: Option<PathBuf>) -> Self {
self.config_path = config_path;
self
}
#[must_use]
pub fn config_path(&self) -> Option<&Path> {
self.config_path.as_deref()
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn endpoint(&self) -> Option<&str> {
self.endpoint.as_deref()
}
#[must_use]
pub fn surface(&self) -> Surface {
self.surface
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndpointError {
Unparseable,
InsecureScheme,
UnsupportedScheme,
}
impl EndpointError {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Unparseable => "unparseable",
Self::InsecureScheme => "plaintext http to a non-loopback host",
Self::UnsupportedScheme => "scheme is neither https nor http",
}
}
}
pub fn validate_endpoint(raw: &str) -> Result<String, EndpointError> {
let trimmed = raw.trim();
let url = reqwest::Url::parse(trimmed).map_err(|_| EndpointError::Unparseable)?;
match url.scheme() {
"https" => Ok(trimmed.to_string()),
"http" => {
if is_loopback_host(url.host_str()) {
Ok(trimmed.to_string())
} else {
Err(EndpointError::InsecureScheme)
}
}
_ => Err(EndpointError::UnsupportedScheme),
}
}
fn is_loopback_host(host: Option<&str>) -> bool {
let Some(host) = host else {
return false;
};
let bare = host.trim_start_matches('[').trim_end_matches(']');
match bare.parse::<std::net::IpAddr>() {
Ok(address) => address.is_loopback(),
Err(_) => bare.eq_ignore_ascii_case("localhost"),
}
}
pub fn decide(
resolved: &ResolvedRuntimeOptions,
setup: &SetupState,
surface: Surface,
) -> TelemetryDecision {
let home = codewhale_paths::codewhale_home().ok().flatten();
decide_in_home(home.as_deref(), resolved, setup, surface)
}
pub fn decide_in_home(
home: Option<&Path>,
resolved: &ResolvedRuntimeOptions,
setup: &SetupState,
surface: Surface,
) -> TelemetryDecision {
let root = home.map(|home| home.join(TELEMETRY_DIR));
if !resolved.telemetry {
if resolved.telemetry_explicit_off {
return opted_out(root.as_deref());
}
return TelemetryDecision::ForcedOff;
}
if setup.needs_telemetry_notice(TELEMETRY_NOTICE_VERSION) {
return TelemetryDecision::ForcedOff;
}
if !setup.telemetry_opt_in {
return opted_out(root.as_deref());
}
let Some(root) = root else {
return TelemetryDecision::ForcedOff;
};
let endpoint = match resolved.telemetry_endpoint.as_deref() {
Some(raw) if !raw.trim().is_empty() => match validate_endpoint(raw) {
Ok(endpoint) => Some(endpoint),
Err(error) => {
tracing::warn!(
"telemetry endpoint refused ({}); telemetry is off for this run",
error.label()
);
return TelemetryDecision::ForcedOff;
}
},
_ => None,
};
TelemetryDecision::Enabled(TelemetryConsent {
root,
endpoint,
surface,
config_path: None,
})
}
#[must_use]
pub fn re_decide(config_path: Option<&Path>, surface: Surface) -> TelemetryDecision {
let Ok(store) = codewhale_config::ConfigStore::load(config_path.map(Path::to_path_buf)) else {
return TelemetryDecision::ForcedOff;
};
let resolved = store
.config
.resolve_runtime_options(&codewhale_config::CliRuntimeOverrides::default());
let setup = SetupState::load().ok().flatten().unwrap_or_default();
decide(&resolved, &setup, surface)
}
fn opted_out(root: Option<&Path>) -> TelemetryDecision {
if let Some(root) = root
&& root.is_dir()
&& let Err(error) = buffer::wipe(root)
{
tracing::warn!("telemetry opt-out wipe was incomplete: {error}");
}
TelemetryDecision::OptedOut
}