axonflow-sdk-rust 0.1.0

Rust SDK for the AxonFlow AI governance platform
Documentation
use std::fs;
use std::path::PathBuf;
use std::sync::Once;
use std::time::{Duration, SystemTime};
use tracing::debug;

static HEARTBEAT_ONCE: Once = Once::new();

const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(7 * 24 * 60 * 60); // 7 days

pub fn maybe_send_heartbeat(endpoint: &str) {
    HEARTBEAT_ONCE.call_once(|| {
        // AXONFLOW_TELEMETRY=off is the documented opt-out across all
        // AxonFlow SDKs. DO_NOT_TRACK is intentionally NOT honored — it is
        // commonly inherited from a parent shell and would silently disable
        // SDK telemetry without an explicit AxonFlow-scoped opt-in.
        if std::env::var("AXONFLOW_TELEMETRY")
            .unwrap_or_default()
            .eq_ignore_ascii_case("off")
        {
            debug!("Telemetry disabled via AXONFLOW_TELEMETRY=off");
            return;
        }

        if let Some(stamp_path) = resolve_stamp_path() {
            if let Ok(metadata) = fs::metadata(&stamp_path) {
                if let Ok(modified) = metadata.modified() {
                    if let Ok(elapsed) = SystemTime::now().duration_since(modified) {
                        if elapsed < HEARTBEAT_INTERVAL {
                            debug!(
                                "Telemetry heartbeat is still fresh ({}s elapsed)",
                                elapsed.as_secs()
                            );
                            return;
                        }
                    }
                }
            }

            let endpoint = endpoint.to_string();
            if let Ok(handle) = tokio::runtime::Handle::try_current() {
                handle.spawn(async move {
                    send_heartbeat(&endpoint, stamp_path).await;
                });
            }
        }
    });
}

fn resolve_stamp_path() -> Option<PathBuf> {
    home::home_dir().map(|mut p| {
        #[cfg(target_os = "macos")]
        {
            p.push("Library");
            p.push("Caches");
        }
        #[cfg(not(target_os = "macos"))]
        {
            p.push(".cache");
        }
        p.push("axonflow");
        p.push("rust-telemetry-last-sent");
        p
    })
}

async fn send_heartbeat(endpoint: &str, stamp_path: PathBuf) {
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(3))
        .build();

    if let Ok(client) = client {
        let url = format!("{}/api/telemetry/heartbeat", endpoint);
        let payload = serde_json::json!({
            "sdk": "rust",
            "version": env!("CARGO_PKG_VERSION"),
        });

        match client.post(&url).json(&payload).send().await {
            Ok(resp) if resp.status().is_success() => {
                debug!("Telemetry heartbeat sent successfully");
                if let Some(parent) = stamp_path.parent() {
                    let _ = fs::create_dir_all(parent);
                }
                let _ = fs::write(
                    &stamp_path,
                    format!("last_sent={}", chrono::Utc::now().to_rfc3339()),
                );
            }
            Ok(resp) => debug!("Telemetry heartbeat rejected by server: {}", resp.status()),
            Err(e) => debug!("Telemetry heartbeat failed: {}", e),
        }
    }
}