async_inspect/
telemetry.rs

1//! Usage telemetry for async-inspect
2//!
3//! This module provides anonymous usage telemetry to help improve async-inspect.
4//! Telemetry is privacy-focused and can be disabled via:
5//! - Environment variable: `ASYNC_INSPECT_NO_TELEMETRY=1`
6//! - Environment variable: `DO_NOT_TRACK=1`
7//! - Compile-time: `--no-default-features` or exclude `telemetry` feature
8//!
9//! We collect anonymous usage data to understand:
10//! - Which features are most used
11//! - Common usage patterns
12//! - Performance characteristics in real-world scenarios
13//!
14//! No personal information, code, or sensitive data is ever collected.
15
16use once_cell::sync::OnceCell;
17use std::sync::atomic::{AtomicBool, Ordering};
18
19static TELEMETRY: OnceCell<Telemetry> = OnceCell::new();
20static TELEMETRY_DISABLED: AtomicBool = AtomicBool::new(false);
21#[cfg(feature = "telemetry")]
22static OPT_OUT_TRACKED: AtomicBool = AtomicBool::new(false);
23
24/// Telemetry wrapper
25pub struct Telemetry {
26    #[cfg(feature = "telemetry")]
27    inner: Option<telemetry_kit::TelemetryKit>,
28    #[cfg(not(feature = "telemetry"))]
29    _phantom: std::marker::PhantomData<()>,
30}
31
32impl Telemetry {
33    /// Check if telemetry is enabled
34    #[must_use]
35    pub fn is_enabled(&self) -> bool {
36        #[cfg(feature = "telemetry")]
37        {
38            self.inner.is_some() && !TELEMETRY_DISABLED.load(Ordering::Relaxed)
39        }
40        #[cfg(not(feature = "telemetry"))]
41        {
42            false
43        }
44    }
45}
46
47#[cfg(feature = "telemetry")]
48fn create_telemetry_kit() -> Option<telemetry_kit::TelemetryKit> {
49    telemetry_kit::TelemetryKit::builder()
50        .service_name("async-inspect")
51        .ok()
52        .and_then(|b| b.service_version(env!("CARGO_PKG_VERSION")).build().ok())
53}
54
55/// Initialize telemetry (call once at startup)
56///
57/// Respects `ASYNC_INSPECT_NO_TELEMETRY=1` and `DO_NOT_TRACK=1` environment variables.
58pub fn init() {
59    let _ = TELEMETRY.get_or_init(|| {
60        // Check for opt-out environment variables
61        let no_telemetry = std::env::var("ASYNC_INSPECT_NO_TELEMETRY")
62            .map(|v| v == "1" || v.to_lowercase() == "true")
63            .unwrap_or(false);
64
65        let do_not_track = std::env::var("DO_NOT_TRACK")
66            .map(|v| v == "1" || v.to_lowercase() == "true")
67            .unwrap_or(false);
68
69        if no_telemetry || do_not_track {
70            TELEMETRY_DISABLED.store(true, Ordering::Relaxed);
71
72            // Track that someone opted out (this helps us understand opt-out rates)
73            // This is a one-time anonymous ping with no identifying information
74            #[cfg(feature = "telemetry")]
75            if !OPT_OUT_TRACKED.swap(true, Ordering::Relaxed) {
76                // Spawn a background task to send opt-out signal
77                std::thread::spawn(|| {
78                    let rt = tokio::runtime::Builder::new_current_thread()
79                        .enable_all()
80                        .build();
81                    if let Ok(rt) = rt {
82                        rt.block_on(async {
83                            if let Some(tk) = create_telemetry_kit() {
84                                let _ = tk
85                                    .track_feature("telemetry_opt_out", |event| event.success(true))
86                                    .await;
87                            }
88                        });
89                    }
90                });
91            }
92
93            return Telemetry {
94                #[cfg(feature = "telemetry")]
95                inner: None,
96                #[cfg(not(feature = "telemetry"))]
97                _phantom: std::marker::PhantomData,
98            };
99        }
100
101        #[cfg(feature = "telemetry")]
102        {
103            Telemetry {
104                inner: create_telemetry_kit(),
105            }
106        }
107
108        #[cfg(not(feature = "telemetry"))]
109        {
110            Telemetry {
111                _phantom: std::marker::PhantomData,
112            }
113        }
114    });
115}
116
117/// Get the global telemetry instance
118#[cfg(feature = "telemetry")]
119fn get() -> Option<&'static Telemetry> {
120    TELEMETRY.get()
121}
122
123/// Track a command/CLI invocation
124#[allow(unused_variables)]
125pub async fn track_command(command: &str, success: bool, duration_ms: Option<u64>) {
126    #[cfg(feature = "telemetry")]
127    {
128        if let Some(telemetry) = get() {
129            if let Some(ref tk) = telemetry.inner {
130                let _ = tk
131                    .track_command(command, |event| {
132                        let event = event.success(success);
133                        if let Some(ms) = duration_ms {
134                            event.duration_ms(ms)
135                        } else {
136                            event
137                        }
138                    })
139                    .await;
140            }
141        }
142    }
143}
144
145/// Track a feature usage
146#[allow(unused_variables)]
147pub async fn track_feature(feature: &str, metadata: Option<&str>) {
148    #[cfg(feature = "telemetry")]
149    {
150        if let Some(telemetry) = get() {
151            if let Some(ref tk) = telemetry.inner {
152                let _ = tk
153                    .track_feature(feature, |event| {
154                        let event = event.success(true);
155                        if let Some(meta) = metadata {
156                            event.method(meta)
157                        } else {
158                            event
159                        }
160                    })
161                    .await;
162            }
163        }
164    }
165}
166
167/// Track feature usage synchronously (spawns background task)
168#[allow(unused_variables)]
169pub fn track_feature_sync(feature: &'static str, metadata: Option<&'static str>) {
170    #[cfg(feature = "telemetry")]
171    {
172        if TELEMETRY_DISABLED.load(Ordering::Relaxed) {
173            return;
174        }
175        tokio::spawn(async move {
176            track_feature(feature, metadata).await;
177        });
178    }
179}
180
181/// Track command usage synchronously (spawns background task)
182#[allow(unused_variables)]
183pub fn track_command_sync(command: &'static str, success: bool, duration_ms: Option<u64>) {
184    #[cfg(feature = "telemetry")]
185    {
186        if TELEMETRY_DISABLED.load(Ordering::Relaxed) {
187            return;
188        }
189        tokio::spawn(async move {
190            track_command(command, success, duration_ms).await;
191        });
192    }
193}
194
195/// Programmatically disable telemetry at runtime
196pub fn disable() {
197    TELEMETRY_DISABLED.store(true, Ordering::Relaxed);
198}
199
200/// Check if telemetry is currently enabled
201#[must_use]
202pub fn is_enabled() -> bool {
203    #[cfg(feature = "telemetry")]
204    {
205        !TELEMETRY_DISABLED.load(Ordering::Relaxed) && get().is_some_and(Telemetry::is_enabled)
206    }
207    #[cfg(not(feature = "telemetry"))]
208    {
209        false
210    }
211}