Skip to main content

rusty_stack/installers/common/
telemetry.rs

1use super::{log_warn, SealedToken};
2use crate::state::GPUInfo;
3use crate::telemetry::OptInGate;
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub enum BuildReportStatus {
9    Success,
10    Failure,
11}
12
13/// Bundled build-output artifacts for [`BuildReport::from_hardware`].
14///
15/// Groups the fields that describe the produced build (commit, paths, flags,
16/// binary metadata) so `from_hardware` stays under clippy's argument threshold.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct BuildReportArtifacts {
19    pub git_commit: String,
20    pub install_path: String,
21    pub cmake_flags: Vec<String>,
22    pub verification_path: String,
23    pub binary_version: String,
24    pub was_prebuilt: bool,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct BuildReport {
29    pub gpu_arch: String,
30    pub gpu_count: usize,
31    pub gpu_name: String,
32    pub rocm_version: String,
33    pub os: String,
34    pub os_distro: String,
35    pub build_duration_seconds: u64,
36    pub git_commit: String,
37    pub build_status: BuildReportStatus,
38    pub install_path: String,
39    pub cmake_flags: Vec<String>,
40    pub verification_path: String,
41    pub rdna3_validation_passed: bool,
42    pub wmma_available: bool,
43    pub shared_memory_ok: bool,
44    pub tokens_per_second_wmma: Option<f32>,
45    pub tokens_per_second_fallback: Option<f32>,
46    pub binary_version: String,
47    pub was_prebuilt: bool,
48}
49
50impl BuildReport {
51    pub fn from_hardware(
52        gpu: &GPUInfo,
53        os: impl Into<String>,
54        os_distro: impl Into<String>,
55        build_duration: Duration,
56        artifacts: BuildReportArtifacts,
57    ) -> Self {
58        Self {
59            gpu_arch: gpu.architecture.clone(),
60            gpu_count: gpu.gpu_count,
61            gpu_name: gpu.model.clone(),
62            rocm_version: gpu.rocm_version.clone(),
63            os: os.into(),
64            os_distro: os_distro.into(),
65            build_duration_seconds: build_duration.as_secs(),
66            git_commit: artifacts.git_commit,
67            build_status: BuildReportStatus::Success,
68            install_path: artifacts.install_path,
69            cmake_flags: artifacts.cmake_flags,
70            verification_path: artifacts.verification_path,
71            rdna3_validation_passed: false,
72            wmma_available: false,
73            shared_memory_ok: false,
74            tokens_per_second_wmma: None,
75            tokens_per_second_fallback: None,
76            binary_version: artifacts.binary_version,
77            was_prebuilt: artifacts.was_prebuilt,
78        }
79    }
80}
81
82pub fn submit_build_report(report: BuildReport) {
83    // Check opt-in gate before attempting any network call (VAL-TELEM-003/004)
84    if let Ok(gate) = OptInGate::new() {
85        if !gate.is_enabled() {
86            log_warn("telemetry opt-in disabled, skipping build report submission");
87            return;
88        }
89    }
90
91    let mut token = SealedToken::from_env();
92    let payload = build_report_payload(&report);
93
94    let response = ureq::post(submission_endpoint())
95        .header("Authorization", format!("Bearer {}", token.as_str()))
96        .header("Accept", "application/vnd.github+json")
97        .header("X-GitHub-Api-Version", "2022-11-28")
98        .header("User-Agent", "rusty-stack")
99        .send_json(payload);
100
101    if let Err(err) = response {
102        log_warn(&format!("telemetry dispatch failed: {}", err));
103    }
104
105    token.purge();
106}
107
108pub fn build_report_payload(report: &BuildReport) -> serde_json::Value {
109    serde_json::json!({
110        "event_type": "build-validation-report",
111        "client_payload": report,
112    })
113}
114
115pub fn submission_endpoint() -> &'static str {
116    "https://api.github.com/repos/scooter-lacroix/llama.cpp-turboquant-hip/dispatches"
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use std::time::Duration;
123
124    #[test]
125    fn build_report_serializes_all_fields() {
126        let gpu = GPUInfo {
127            architecture: "gfx1100".into(),
128            gpu_count: 2,
129            model: "AMD Radeon RX 7900 XTX".into(),
130            rocm_version: "7.2.1".into(),
131            ..Default::default()
132        };
133        let report = BuildReport::from_hardware(
134            &gpu,
135            "linux",
136            "ubuntu",
137            Duration::from_secs(42),
138            BuildReportArtifacts {
139                git_commit: "abc123".into(),
140                install_path: "/tmp/install".into(),
141                cmake_flags: vec!["-DGGML_HIP=ON".into()],
142                verification_path: "/tmp/verify".into(),
143                binary_version: "v0.4.0".into(),
144                was_prebuilt: true,
145            },
146        );
147
148        let json = serde_json::to_value(report).unwrap();
149        for key in [
150            "gpu_arch",
151            "gpu_count",
152            "gpu_name",
153            "rocm_version",
154            "os",
155            "os_distro",
156            "build_duration_seconds",
157            "git_commit",
158            "build_status",
159            "install_path",
160            "cmake_flags",
161            "verification_path",
162            "rdna3_validation_passed",
163            "wmma_available",
164            "shared_memory_ok",
165            "tokens_per_second_wmma",
166            "tokens_per_second_fallback",
167            "binary_version",
168            "was_prebuilt",
169        ] {
170            assert!(json.get(key).is_some(), "missing key {key}");
171        }
172    }
173
174    #[test]
175    fn submission_payload_matches_repository_dispatch_format() {
176        let gpu = GPUInfo {
177            architecture: "gfx1100".into(),
178            gpu_count: 1,
179            model: "AMD Radeon RX 7900 XTX".into(),
180            rocm_version: "7.2.1".into(),
181            ..Default::default()
182        };
183        let report = BuildReport::from_hardware(
184            &gpu,
185            "linux",
186            "ubuntu",
187            Duration::from_secs(12),
188            BuildReportArtifacts {
189                git_commit: "commit123".into(),
190                install_path: "/opt/mlstack".into(),
191                cmake_flags: vec!["-DGGML_HIP=ON".into(), "-DGPU_TARGETS=gfx1100".into()],
192                verification_path: "/tmp/verify".into(),
193                binary_version: "v0.4.0".into(),
194                was_prebuilt: false,
195            },
196        );
197
198        let payload = build_report_payload(&report);
199        assert_eq!(
200            payload["event_type"],
201            serde_json::Value::String("build-validation-report".into())
202        );
203        assert_eq!(payload["client_payload"]["gpu_arch"], "gfx1100");
204        assert_eq!(payload["client_payload"]["build_duration_seconds"], 12);
205        assert!(payload["client_payload"]["cmake_flags"].is_array());
206    }
207}