devalang_core/utils/
telemetry.rs

1use crate::{
2    common::api::get_api_url,
3    config::{driver::ProjectConfig, settings::get_user_config, stats::ProjectStats},
4};
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Serialize, Deserialize, Default, Clone)]
9pub enum TelemetryErrorLevel {
10    #[default]
11    None,
12    Warning,
13    Critical,
14}
15
16#[derive(Serialize, Deserialize, Default, Clone)]
17pub struct TelemetryProjectInfo {
18    config: Option<TelemetryProjectInfoConfig>,
19    stats: Option<TelemetryProjectInfoStats>,
20}
21
22#[derive(Serialize, Deserialize, Default, Clone, Debug)]
23pub struct TelemetryProjectInfoStats {
24    counts: TelemetryProjectInfoStatsCounts,
25    features: TelemetryProjectInfoStatsFeatures,
26    audio: TelemetryProjectInfoStatsAudio,
27}
28
29#[derive(Serialize, Deserialize, Default, Clone, Debug)]
30pub struct TelemetryProjectInfoStatsCounts {
31    pub nb_files: usize,
32    pub nb_modules: usize,
33    pub nb_lines: usize,
34    pub nb_banks: usize,
35    pub nb_plugins: usize,
36}
37
38#[derive(Serialize, Deserialize, Default, Clone, Debug)]
39pub struct TelemetryProjectInfoStatsFeatures {
40    pub uses_imports: bool,
41    pub uses_functions: bool,
42    pub uses_groups: bool,
43    pub uses_automations: bool,
44    pub uses_loops: bool,
45}
46
47#[derive(Serialize, Deserialize, Default, Clone, Debug)]
48pub struct TelemetryProjectInfoStatsAudio {
49    pub avg_bpm: Option<u32>,
50    pub has_synths: bool,
51    pub has_samples: bool,
52}
53
54#[derive(Serialize, Deserialize, Default, Clone, Debug)]
55pub struct TelemetryProjectInfoConfig {
56    pub entry_defined: bool,
57    pub output_defined: bool,
58    pub watch_defined: bool,
59    pub repeat_defined: bool,
60    pub debug_defined: bool,
61    pub compress_defined: bool,
62}
63
64#[derive(Serialize, Deserialize, Default, Clone)]
65pub struct TelemetryEvent {
66    pub uuid: String,
67    pub cli_version: String,
68    pub os: String,
69    pub command: Vec<String>,
70    pub project_info: Option<TelemetryProjectInfo>,
71    pub error_level: TelemetryErrorLevel,
72    pub error_message: Option<String>,
73    pub exit_code: Option<i32>,
74    pub timestamp: String,
75    pub duration: u64,
76    pub success: bool,
77}
78
79impl TelemetryEvent {
80    pub fn set_timestamp(&mut self, timestamp: String) {
81        self.timestamp = timestamp;
82    }
83
84    pub fn set_duration(&mut self, duration: u64) {
85        self.duration = duration;
86    }
87
88    pub fn set_success(&mut self, success: bool) {
89        self.success = success;
90    }
91
92    pub fn set_error(
93        &mut self,
94        level: TelemetryErrorLevel,
95        message: Option<String>,
96        exit_code: Option<i32>,
97    ) {
98        self.error_level = level;
99        self.error_message = message;
100        self.exit_code = exit_code;
101    }
102}
103
104pub struct TelemetryEventCreator {
105    pub events: Vec<TelemetryEvent>,
106}
107
108impl TelemetryEventCreator {
109    pub fn new() -> Self {
110        TelemetryEventCreator { events: Vec::new() }
111    }
112
113    pub fn create_event(&mut self, event: TelemetryEvent) {
114        self.events.push(event.clone());
115    }
116
117    pub fn get_base_event(&self) -> TelemetryEvent {
118        let mut stats_enabled = false;
119
120        let user_config = get_user_config().unwrap_or_default();
121
122        if user_config.telemetry.stats == true {
123            stats_enabled = true;
124        }
125
126        let mut event: TelemetryEvent = TelemetryEvent {
127            uuid: user_config.telemetry.uuid.clone(),
128            cli_version: env!("CARGO_PKG_VERSION").to_string(),
129            os: std::env::consts::OS.to_string(),
130            command: std::env::args().collect::<Vec<_>>(),
131            project_info: None,
132            error_level: TelemetryErrorLevel::None,
133            error_message: None,
134            exit_code: None,
135            timestamp: chrono::Utc::now().to_string(),
136            duration: 0,
137            success: true,
138        };
139
140        let project_settings = ProjectConfig::get();
141        let project_stats = ProjectStats::get();
142
143        if project_settings.is_ok() && project_stats.is_ok() {
144            let project_settings = project_settings.unwrap();
145            let project_stats = project_stats.unwrap();
146
147            let mut stats = None;
148
149            if stats_enabled {
150                stats = Some(TelemetryProjectInfoStats {
151                    counts: TelemetryProjectInfoStatsCounts {
152                        nb_files: project_stats.counts.nb_files,
153                        nb_modules: project_stats.counts.nb_modules,
154                        nb_lines: project_stats.counts.nb_lines,
155                        nb_banks: project_stats.counts.nb_banks,
156                        nb_plugins: project_stats.counts.nb_plugins,
157                    },
158                    features: TelemetryProjectInfoStatsFeatures {
159                        uses_imports: project_stats.features.uses_imports,
160                        uses_functions: project_stats.features.uses_functions,
161                        uses_groups: project_stats.features.uses_groups,
162                        uses_automations: project_stats.features.uses_automations,
163                        uses_loops: project_stats.features.uses_loops,
164                    },
165                    audio: TelemetryProjectInfoStatsAudio {
166                        avg_bpm: project_stats.audio.avg_bpm,
167                        has_synths: project_stats.audio.has_synths,
168                        has_samples: project_stats.audio.has_samples,
169                    },
170                });
171            }
172
173            event.project_info = Some(TelemetryProjectInfo {
174                config: Some(TelemetryProjectInfoConfig {
175                    entry_defined: project_settings.defaults.entry.is_some(),
176                    output_defined: project_settings.defaults.output.is_some(),
177                    watch_defined: project_settings.defaults.watch.is_some(),
178                    repeat_defined: project_settings.defaults.repeat.is_some(),
179                    debug_defined: project_settings.defaults.debug.is_some(),
180                    compress_defined: project_settings.defaults.compress.is_some(),
181                }),
182                stats: stats,
183            });
184        } else {
185            event.project_info = None;
186        }
187
188        event
189    }
190}
191
192pub fn refresh_event_project_info(event: &mut TelemetryEvent) {
193    let user_config = get_user_config().unwrap_or_default();
194    let stats_enabled = user_config.telemetry.stats;
195
196    let project_settings = ProjectConfig::get();
197    let project_stats = ProjectStats::get();
198
199    if project_settings.is_ok() && project_stats.is_ok() {
200        let project_settings = project_settings.unwrap();
201        let project_stats = project_stats.unwrap();
202
203        let mut stats = None;
204        if stats_enabled {
205            stats = Some(TelemetryProjectInfoStats {
206                counts: TelemetryProjectInfoStatsCounts {
207                    nb_files: project_stats.counts.nb_files,
208                    nb_modules: project_stats.counts.nb_modules,
209                    nb_lines: project_stats.counts.nb_lines,
210                    nb_banks: project_stats.counts.nb_banks,
211                    nb_plugins: project_stats.counts.nb_plugins,
212                },
213                features: TelemetryProjectInfoStatsFeatures {
214                    uses_imports: project_stats.features.uses_imports,
215                    uses_functions: project_stats.features.uses_functions,
216                    uses_groups: project_stats.features.uses_groups,
217                    uses_automations: project_stats.features.uses_automations,
218                    uses_loops: project_stats.features.uses_loops,
219                },
220                audio: TelemetryProjectInfoStatsAudio {
221                    avg_bpm: project_stats.audio.avg_bpm,
222                    has_synths: project_stats.audio.has_synths,
223                    has_samples: project_stats.audio.has_samples,
224                },
225            });
226        }
227
228        event.project_info = Some(TelemetryProjectInfo {
229            config: Some(TelemetryProjectInfoConfig {
230                entry_defined: project_settings.defaults.entry.is_some(),
231                output_defined: project_settings.defaults.output.is_some(),
232                watch_defined: project_settings.defaults.watch.is_some(),
233                repeat_defined: project_settings.defaults.repeat.is_some(),
234                debug_defined: project_settings.defaults.debug.is_some(),
235                compress_defined: project_settings.defaults.compress.is_some(),
236            }),
237            stats,
238        });
239    } else {
240        event.project_info = None;
241    }
242}
243
244#[derive(Debug)]
245pub enum TelemetrySendError {
246    Http(String),
247}
248
249pub async fn send_telemetry_event(event: &TelemetryEvent) -> Result<(), TelemetrySendError> {
250    if let Some(cfg) = get_user_config() {
251        if cfg.telemetry.enabled == false {
252            return Ok(());
253        }
254    }
255
256    let telemetry_url = format!("{}/v1/telemetry/send", get_api_url());
257    let client = reqwest::Client::builder()
258        .timeout(Duration::from_secs(5))
259        .build()
260        .map_err(|e| TelemetrySendError::Http(format!("client build error: {}", e)))?;
261
262    let mut last_err: Option<String> = None;
263    for (i, delay_ms) in [0u64, 250, 500, 1000].iter().enumerate() {
264        if *delay_ms > 0 {
265            tokio::time::sleep(Duration::from_millis(*delay_ms)).await;
266        }
267
268        let res = client
269            .post(telemetry_url.clone())
270            .json(event)
271            .send()
272            .await
273            .and_then(|r| r.error_for_status());
274
275        match res {
276            Ok(_) => {
277                return Ok(());
278            }
279            Err(err) => {
280                last_err = Some(err.to_string());
281
282                if i == 3 {
283                    break;
284                }
285            }
286        }
287    }
288
289    Err(TelemetrySendError::Http(
290        last_err.unwrap_or_else(|| "unknown error".to_string()),
291    ))
292}