archy/core/archipelago/src/api/rpc/analytics.rs

121 lines
4.4 KiB
Rust
Raw Normal View History

//! Opt-in anonymous node analytics.
//! When enabled, collects aggregate stats (app install counts, uptime, hardware tier).
//! No personally identifiable information. No IP addresses. No DIDs.
//! Data stays local until explicitly shared via future relay mechanism.
use super::RpcHandler;
use anyhow::Result;
use tracing::info;
const ANALYTICS_FILE: &str = "analytics-config.json";
impl RpcHandler {
/// Check if analytics are enabled.
pub(super) async fn handle_analytics_get_status(&self) -> Result<serde_json::Value> {
let config_path = self.config.data_dir.join(ANALYTICS_FILE);
let enabled = if config_path.exists() {
let data = tokio::fs::read_to_string(&config_path).await?;
let config: serde_json::Value = serde_json::from_str(&data).unwrap_or_default();
config["enabled"].as_bool().unwrap_or(false)
} else {
false
};
Ok(serde_json::json!({
"enabled": enabled,
"description": "Anonymous aggregate statistics. No personal data collected.",
}))
}
/// Enable opt-in analytics.
pub(super) async fn handle_analytics_enable(&self) -> Result<serde_json::Value> {
let config_path = self.config.data_dir.join(ANALYTICS_FILE);
let config = serde_json::json!({
"enabled": true,
"opted_in_at": chrono::Utc::now().to_rfc3339(),
});
tokio::fs::write(&config_path, serde_json::to_string_pretty(&config)?).await?;
info!("Analytics opted in");
Ok(serde_json::json!({ "enabled": true }))
}
/// Disable analytics.
pub(super) async fn handle_analytics_disable(&self) -> Result<serde_json::Value> {
let config_path = self.config.data_dir.join(ANALYTICS_FILE);
let config = serde_json::json!({
"enabled": false,
"opted_out_at": chrono::Utc::now().to_rfc3339(),
});
tokio::fs::write(&config_path, serde_json::to_string_pretty(&config)?).await?;
info!("Analytics opted out");
Ok(serde_json::json!({ "enabled": false }))
}
/// Get an anonymous analytics snapshot of this node.
/// Only returns aggregate data — no DIDs, no IPs, no secrets.
pub(super) async fn handle_analytics_get_snapshot(&self) -> Result<serde_json::Value> {
// Check if opted in
let config_path = self.config.data_dir.join(ANALYTICS_FILE);
let enabled = if config_path.exists() {
let data = tokio::fs::read_to_string(&config_path).await?;
let config: serde_json::Value = serde_json::from_str(&data).unwrap_or_default();
config["enabled"].as_bool().unwrap_or(false)
} else {
false
};
if !enabled {
return Ok(serde_json::json!({
"error": "Analytics not enabled. Opt in via analytics.enable first.",
"enabled": false,
}));
}
// Collect anonymous aggregate data
let (data, _) = self.state_manager.get_snapshot().await;
let app_count = data.package_data.len();
let running_count = data.package_data.values()
.filter(|p| matches!(p.state, crate::data_model::PackageState::Running))
.count();
// Hardware tier (anonymous)
let cpu_cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(0);
let mem_output = tokio::process::Command::new("grep")
.args(["MemTotal", "/proc/meminfo"])
.output()
.await;
let total_ram_mb = mem_output.ok()
.and_then(|o| {
let s = String::from_utf8_lossy(&o.stdout);
s.split_whitespace().nth(1)?.parse::<u64>().ok()
})
.map(|kb| kb / 1024)
.unwrap_or(0);
let hardware_tier = match total_ram_mb {
0..=3999 => "minimal",
4000..=7999 => "standard",
8000..=15999 => "power",
_ => "heavy",
};
let version = &data.server_info.version;
let federation_peers = data.peer_health.len();
Ok(serde_json::json!({
"version": version,
"app_count": app_count,
"running_count": running_count,
"hardware_tier": hardware_tier,
"cpu_cores": cpu_cores,
"ram_mb": total_ram_mb,
"federation_peers": federation_peers,
"collected_at": chrono::Utc::now().to_rfc3339(),
}))
}
}