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}