Skip to main content

xet_runtime/config/groups/
telemetry.rs

1use std::time::Duration;
2
3crate::config_group!({
4    /// Whether the client reports transfer performance telemetry to the CAS server.
5    ///
6    /// When enabled, each upload or download transfer sends a single summary document to
7    /// `POST /v1/telemetry` when it finishes, plus periodic heartbeat documents for transfers
8    /// that run longer than `heartbeat_after`. Sends are best-effort: they are never retried,
9    /// never block data movement, and failures are only logged at DEBUG.
10    ///
11    /// The payload carries no file names, paths, hashes, repository ids, or user ids.
12    ///
13    /// Telemetry is force-disabled regardless of this value when any of huggingface_hub's opt-out
14    /// variables (`HF_HUB_DISABLE_TELEMETRY`, `DISABLE_TELEMETRY`, `DO_NOT_TRACK`, `HF_HUB_OFFLINE`,
15    /// `TRANSFORMERS_OFFLINE`) is set to a truthy value.
16    ///
17    /// The default value is true.
18    ///
19    /// Use the environment variable `HF_XET_TELEMETRY_ENABLED` to set this value.
20    ref enabled: bool = true;
21
22    /// How long a transfer must run before it starts emitting heartbeat documents.
23    ///
24    /// Short transfers - the common case - only ever emit their terminal summary. Heartbeats
25    /// exist so a long transfer that hangs or is killed still reports something.
26    ///
27    /// Set to zero to disable heartbeats entirely.
28    ///
29    /// The default value is 5 minutes.
30    ///
31    /// Use the environment variable `HF_XET_TELEMETRY_HEARTBEAT_AFTER` to set this value.
32    ref heartbeat_after: Duration = Duration::from_secs(300);
33
34    /// The interval between heartbeat documents once `heartbeat_after` has elapsed.
35    ///
36    /// The default value is 5 minutes.
37    ///
38    /// Use the environment variable `HF_XET_TELEMETRY_HEARTBEAT_INTERVAL` to set this value.
39    ref heartbeat_interval: Duration = Duration::from_secs(300);
40
41    /// Whole-request budget for a single telemetry POST, including connection setup.
42    ///
43    /// The default value is 5 seconds.
44    ///
45    /// Use the environment variable `HF_XET_TELEMETRY_REQUEST_TIMEOUT` to set this value.
46    ref request_timeout: Duration = Duration::from_secs(5);
47
48    /// How long a transfer waits for its terminal telemetry document to be delivered.
49    ///
50    /// This runs after all transfer work has completed, so it delays no data movement, but it
51    /// does delay the return of `finalize()`. A short bounded wait is used because a fully
52    /// detached final send is usually lost: host processes frequently exit within milliseconds
53    /// of the transfer returning.
54    ///
55    /// Set to zero to make the terminal send fully detached.
56    ///
57    /// The default value is 2 seconds.
58    ///
59    /// Use the environment variable `HF_XET_TELEMETRY_FINAL_FLUSH_TIMEOUT` to set this value.
60    ref final_flush_timeout: Duration = Duration::from_secs(2);
61
62    /// Maximum number of telemetry requests allowed in flight at once, across the whole process.
63    ///
64    /// Documents submitted beyond this cap are dropped rather than queued; this is the
65    /// backpressure mechanism that keeps a degraded telemetry endpoint from accumulating tasks.
66    ///
67    /// The cap is process-wide rather than per-transfer, so it bounds a wide fan-out - a snapshot
68    /// download becomes many concurrent per-file transfers, and a per-transfer cap would multiply
69    /// by that count instead of limiting it.
70    ///
71    /// The default value is 32. It is sized as a process-wide number: a transfer emits one terminal
72    /// document, and heartbeats only start after `heartbeat_after`, so the realistic burst is a set
73    /// of concurrent transfers finalizing together. A much smaller ceiling would shed most of a wide
74    /// snapshot's terminal documents, which are the ones worth keeping.
75    ///
76    /// Use the environment variable `HF_XET_TELEMETRY_MAX_IN_FLIGHT` to set this value.
77    ref max_in_flight: usize = 32;
78});
79
80#[cfg(all(test, not(target_family = "wasm")))]
81mod tests {
82    use serial_test::serial;
83
84    use crate::config::XetConfig;
85    use crate::utils::EnvVarGuard;
86    use crate::utils::configuration_utils::TELEMETRY_OPT_OUT_VARS;
87
88    const XET_ENABLED: &str = "HF_XET_TELEMETRY_ENABLED";
89
90    /// Clears every variable that participates in the gating decision, so a value exported in the
91    /// developer's shell cannot make these tests pass or fail spuriously.
92    fn clear_all() -> Vec<EnvVarGuard> {
93        std::iter::once(XET_ENABLED)
94            .chain(TELEMETRY_OPT_OUT_VARS.iter().copied())
95            .map(EnvVarGuard::unset)
96            .collect()
97    }
98
99    fn telemetry_enabled() -> bool {
100        XetConfig::default().with_env_overrides().telemetry.enabled
101    }
102
103    /// Also covers opt-out variables whose value does not parse as truthy: presence alone must not
104    /// disable, so `HF_HUB_OFFLINE=0` leaves telemetry on.
105    #[test]
106    #[serial(env)]
107    fn test_enabled_by_default() {
108        let _guards = clear_all();
109        assert!(telemetry_enabled());
110
111        for &var in TELEMETRY_OPT_OUT_VARS {
112            for value in ["0", "false", "", "unparseable"] {
113                let _g = EnvVarGuard::set(var, value);
114                assert!(telemetry_enabled(), "{var}={value:?} must not disable telemetry");
115            }
116        }
117    }
118
119    /// Disabled by our own variable, and by any huggingface_hub opt-out set to a truthy value. A user
120    /// asking for privacy wins over an explicit `HF_XET_TELEMETRY_ENABLED=1`.
121    #[test]
122    #[serial(env)]
123    fn test_disabled_by_env_vars() {
124        let _guards = clear_all();
125        {
126            let _g = EnvVarGuard::set(XET_ENABLED, "0");
127            assert!(!telemetry_enabled());
128        }
129
130        let _enabled = EnvVarGuard::set(XET_ENABLED, "1");
131        assert!(telemetry_enabled());
132        for &var in TELEMETRY_OPT_OUT_VARS {
133            // huggingface_hub's truthy set is 1/ON/YES/TRUE, case-insensitive.
134            for value in ["1", "ON", "yes", "True"] {
135                let _g = EnvVarGuard::set(var, value);
136                assert!(!telemetry_enabled(), "{var}={value:?} must disable telemetry");
137            }
138        }
139    }
140
141    #[test]
142    #[serial(env)]
143    fn test_durations_and_cap_have_expected_defaults() {
144        let _guards = clear_all();
145        let t = XetConfig::default().with_env_overrides().telemetry;
146        assert_eq!(t.heartbeat_after.as_secs(), 300);
147        assert_eq!(t.heartbeat_interval.as_secs(), 300);
148        assert_eq!(t.request_timeout.as_secs(), 5);
149        assert_eq!(t.final_flush_timeout.as_secs(), 2);
150        // Process-wide, not per-transfer - see `max_in_flight`'s docs for why it is sized this way.
151        assert_eq!(t.max_in_flight, 32);
152    }
153
154    #[test]
155    #[serial(env)]
156    fn test_durations_parse_from_env() {
157        let _guards = clear_all();
158        let _after = EnvVarGuard::set("HF_XET_TELEMETRY_HEARTBEAT_AFTER", "90s");
159        let _flush = EnvVarGuard::set("HF_XET_TELEMETRY_FINAL_FLUSH_TIMEOUT", "0s");
160        let t = XetConfig::default().with_env_overrides().telemetry;
161        assert_eq!(t.heartbeat_after.as_secs(), 90);
162        assert_eq!(t.final_flush_timeout.as_secs(), 0);
163    }
164}