1mod event_coalescer;
2
3use crate::{ChannelId, TelemetrySettings};
4use anyhow::Result;
5use clock::SystemClock;
6use collections::{HashMap, HashSet};
7use futures::Future;
8use gpui::{AppContext, BackgroundExecutor, Task};
9use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
10use once_cell::sync::Lazy;
11use parking_lot::Mutex;
12use release_channel::ReleaseChannel;
13use settings::{Settings, SettingsStore};
14use sha2::{Digest, Sha256};
15use std::fs::File;
16use std::io::Write;
17use std::time::Instant;
18use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
19use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
20use telemetry_events::{
21 ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
22 EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
23 SettingEvent,
24};
25use util::{ResultExt, TryFutureExt};
26use worktree::{UpdatedEntriesSet, WorktreeId};
27
28use self::event_coalescer::EventCoalescer;
29
30pub struct Telemetry {
31 clock: Arc<dyn SystemClock>,
32 http_client: Arc<HttpClientWithUrl>,
33 executor: BackgroundExecutor,
34 state: Arc<Mutex<TelemetryState>>,
35}
36
37struct TelemetryState {
38 settings: TelemetrySettings,
39 system_id: Option<Arc<str>>, // Per system
40 installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
41 session_id: Option<String>, // Per app launch
42 metrics_id: Option<Arc<str>>, // Per logged-in user
43 release_channel: Option<&'static str>,
44 architecture: &'static str,
45 events_queue: Vec<EventWrapper>,
46 flush_events_task: Option<Task<()>>,
47 log_file: Option<File>,
48 is_staff: Option<bool>,
49 first_event_date_time: Option<Instant>,
50 event_coalescer: EventCoalescer,
51 max_queue_size: usize,
52 worktree_id_map: WorktreeIdMap,
53
54 os_name: String,
55 app_version: String,
56 os_version: Option<String>,
57}
58
59#[derive(Debug)]
60struct WorktreeIdMap(HashMap<String, ProjectCache>);
61
62#[derive(Debug)]
63struct ProjectCache {
64 name: String,
65 worktree_ids_reported: HashSet<WorktreeId>,
66}
67
68impl ProjectCache {
69 fn new(name: String) -> Self {
70 Self {
71 name,
72 worktree_ids_reported: HashSet::default(),
73 }
74 }
75}
76
77#[cfg(debug_assertions)]
78const MAX_QUEUE_LEN: usize = 5;
79
80#[cfg(not(debug_assertions))]
81const MAX_QUEUE_LEN: usize = 50;
82
83#[cfg(debug_assertions)]
84const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
85
86#[cfg(not(debug_assertions))]
87const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
88static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
89 option_env!("ZED_CLIENT_CHECKSUM_SEED")
90 .map(|s| s.as_bytes().into())
91 .or_else(|| {
92 env::var("ZED_CLIENT_CHECKSUM_SEED")
93 .ok()
94 .map(|s| s.as_bytes().into())
95 })
96});
97
98pub fn os_name() -> String {
99 #[cfg(target_os = "macos")]
100 {
101 "macOS".to_string()
102 }
103 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
104 {
105 format!("Linux {}", gpui::guess_compositor())
106 }
107
108 #[cfg(target_os = "windows")]
109 {
110 "Windows".to_string()
111 }
112}
113
114/// Note: This might do blocking IO! Only call from background threads
115pub fn os_version() -> String {
116 #[cfg(target_os = "macos")]
117 {
118 use cocoa::base::nil;
119 use cocoa::foundation::NSProcessInfo;
120
121 unsafe {
122 let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
123 let version = process_info.operatingSystemVersion();
124 gpui::SemanticVersion::new(
125 version.majorVersion as usize,
126 version.minorVersion as usize,
127 version.patchVersion as usize,
128 )
129 .to_string()
130 }
131 }
132 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
133 {
134 use std::path::Path;
135
136 let content = if let Ok(file) = std::fs::read_to_string(&Path::new("/etc/os-release")) {
137 file
138 } else if let Ok(file) = std::fs::read_to_string(&Path::new("/usr/lib/os-release")) {
139 file
140 } else {
141 log::error!("Failed to load /etc/os-release, /usr/lib/os-release");
142 "".to_string()
143 };
144 let mut name = "unknown".to_string();
145 let mut version = "unknown".to_string();
146
147 for line in content.lines() {
148 if line.starts_with("ID=") {
149 name = line.trim_start_matches("ID=").trim_matches('"').to_string();
150 }
151 if line.starts_with("VERSION_ID=") {
152 version = line
153 .trim_start_matches("VERSION_ID=")
154 .trim_matches('"')
155 .to_string();
156 }
157 }
158
159 format!("{} {}", name, version)
160 }
161
162 #[cfg(target_os = "windows")]
163 {
164 let mut info = unsafe { std::mem::zeroed() };
165 let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
166 if status.is_ok() {
167 gpui::SemanticVersion::new(
168 info.dwMajorVersion as _,
169 info.dwMinorVersion as _,
170 info.dwBuildNumber as _,
171 )
172 .to_string()
173 } else {
174 "unknown".to_string()
175 }
176 }
177}
178
179impl Telemetry {
180 pub fn new(
181 clock: Arc<dyn SystemClock>,
182 client: Arc<HttpClientWithUrl>,
183 cx: &mut AppContext,
184 ) -> Arc<Self> {
185 let release_channel =
186 ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
187
188 TelemetrySettings::register(cx);
189
190 let state = Arc::new(Mutex::new(TelemetryState {
191 settings: *TelemetrySettings::get_global(cx),
192 architecture: env::consts::ARCH,
193 release_channel,
194 system_id: None,
195 installation_id: None,
196 session_id: None,
197 metrics_id: None,
198 events_queue: Vec::new(),
199 flush_events_task: None,
200 log_file: None,
201 is_staff: None,
202 first_event_date_time: None,
203 event_coalescer: EventCoalescer::new(clock.clone()),
204 max_queue_size: MAX_QUEUE_LEN,
205 worktree_id_map: WorktreeIdMap(HashMap::from_iter([
206 (
207 "pnpm-lock.yaml".to_string(),
208 ProjectCache::new("pnpm".to_string()),
209 ),
210 (
211 "yarn.lock".to_string(),
212 ProjectCache::new("yarn".to_string()),
213 ),
214 (
215 "package.json".to_string(),
216 ProjectCache::new("node".to_string()),
217 ),
218 ])),
219
220 os_version: None,
221 os_name: os_name(),
222 app_version: release_channel::AppVersion::global(cx).to_string(),
223 }));
224 Self::log_file_path();
225
226 cx.background_executor()
227 .spawn({
228 let state = state.clone();
229 async move {
230 if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
231 state.lock().log_file = Some(tempfile);
232 }
233 }
234 })
235 .detach();
236
237 cx.observe_global::<SettingsStore>({
238 let state = state.clone();
239
240 move |cx| {
241 let mut state = state.lock();
242 state.settings = *TelemetrySettings::get_global(cx);
243 }
244 })
245 .detach();
246
247 // TODO: Replace all hardware stuff with nested SystemSpecs json
248 let this = Arc::new(Self {
249 clock,
250 http_client: client,
251 executor: cx.background_executor().clone(),
252 state,
253 });
254
255 // We should only ever have one instance of Telemetry, leak the subscription to keep it alive
256 // rather than store in TelemetryState, complicating spawn as subscriptions are not Send
257 std::mem::forget(cx.on_app_quit({
258 let this = this.clone();
259 move |_| this.shutdown_telemetry()
260 }));
261
262 this
263 }
264
265 #[cfg(any(test, feature = "test-support"))]
266 fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
267 Task::ready(())
268 }
269
270 // Skip calling this function in tests.
271 // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
272 #[cfg(not(any(test, feature = "test-support")))]
273 fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
274 self.report_app_event("close".to_string());
275 // TODO: close final edit period and make sure it's sent
276 Task::ready(())
277 }
278
279 pub fn log_file_path() -> PathBuf {
280 paths::logs_dir().join("telemetry.log")
281 }
282
283 pub fn start(
284 self: &Arc<Self>,
285 system_id: Option<String>,
286 installation_id: Option<String>,
287 session_id: String,
288 cx: &AppContext,
289 ) {
290 let mut state = self.state.lock();
291 state.system_id = system_id.map(|id| id.into());
292 state.installation_id = installation_id.map(|id| id.into());
293 state.session_id = Some(session_id);
294 state.app_version = release_channel::AppVersion::global(cx).to_string();
295 state.os_name = os_name();
296
297 drop(state);
298
299 let this = self.clone();
300 cx.background_executor()
301 .spawn(async move {
302 let mut system = System::new_with_specifics(
303 RefreshKind::new().with_cpu(CpuRefreshKind::everything()),
304 );
305
306 let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory();
307 let current_process = Pid::from_u32(std::process::id());
308 system.refresh_processes_specifics(
309 sysinfo::ProcessesToUpdate::Some(&[current_process]),
310 refresh_kind,
311 );
312
313 // Waiting some amount of time before the first query is important to get a reasonable value
314 // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
315 const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
316
317 loop {
318 smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
319
320 let current_process = Pid::from_u32(std::process::id());
321 system.refresh_processes_specifics(
322 sysinfo::ProcessesToUpdate::Some(&[current_process]),
323 refresh_kind,
324 );
325 let Some(process) = system.process(current_process) else {
326 log::error!(
327 "Failed to find own process {current_process:?} in system process table"
328 );
329 // TODO: Fire an error telemetry event
330 return;
331 };
332
333 this.report_memory_event(process.memory(), process.virtual_memory());
334 this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
335 }
336 })
337 .detach();
338 }
339
340 pub fn metrics_enabled(self: &Arc<Self>) -> bool {
341 let state = self.state.lock();
342 let enabled = state.settings.metrics;
343 drop(state);
344 enabled
345 }
346
347 pub fn set_authenticated_user_info(
348 self: &Arc<Self>,
349 metrics_id: Option<String>,
350 is_staff: bool,
351 ) {
352 let mut state = self.state.lock();
353
354 if !state.settings.metrics {
355 return;
356 }
357
358 let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
359 state.metrics_id.clone_from(&metrics_id);
360 state.is_staff = Some(is_staff);
361 drop(state);
362 }
363
364 pub fn report_editor_event(
365 self: &Arc<Self>,
366 file_extension: Option<String>,
367 vim_mode: bool,
368 operation: &'static str,
369 copilot_enabled: bool,
370 copilot_enabled_for_language: bool,
371 is_via_ssh: bool,
372 ) {
373 let event = Event::Editor(EditorEvent {
374 file_extension,
375 vim_mode,
376 operation: operation.into(),
377 copilot_enabled,
378 copilot_enabled_for_language,
379 is_via_ssh,
380 });
381
382 self.report_event(event)
383 }
384
385 pub fn report_inline_completion_event(
386 self: &Arc<Self>,
387 provider: String,
388 suggestion_accepted: bool,
389 file_extension: Option<String>,
390 ) {
391 let event = Event::InlineCompletion(InlineCompletionEvent {
392 provider,
393 suggestion_accepted,
394 file_extension,
395 });
396
397 self.report_event(event)
398 }
399
400 pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
401 self.report_event(Event::Assistant(event));
402 }
403
404 pub fn report_call_event(
405 self: &Arc<Self>,
406 operation: &'static str,
407 room_id: Option<u64>,
408 channel_id: Option<ChannelId>,
409 ) {
410 let event = Event::Call(CallEvent {
411 operation: operation.to_string(),
412 room_id,
413 channel_id: channel_id.map(|cid| cid.0),
414 });
415
416 self.report_event(event)
417 }
418
419 pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
420 let event = Event::Cpu(CpuEvent {
421 usage_as_percentage,
422 core_count,
423 });
424
425 self.report_event(event)
426 }
427
428 pub fn report_memory_event(
429 self: &Arc<Self>,
430 memory_in_bytes: u64,
431 virtual_memory_in_bytes: u64,
432 ) {
433 let event = Event::Memory(MemoryEvent {
434 memory_in_bytes,
435 virtual_memory_in_bytes,
436 });
437
438 self.report_event(event)
439 }
440
441 pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
442 let event = Event::App(AppEvent { operation });
443
444 self.report_event(event.clone());
445
446 event
447 }
448
449 pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
450 let event = Event::Setting(SettingEvent {
451 setting: setting.to_string(),
452 value,
453 });
454
455 self.report_event(event)
456 }
457
458 pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
459 self.report_event(Event::Extension(ExtensionEvent {
460 extension_id,
461 version,
462 }))
463 }
464
465 pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
466 let mut state = self.state.lock();
467 let period_data = state.event_coalescer.log_event(environment);
468 drop(state);
469
470 if let Some((start, end, environment)) = period_data {
471 let event = Event::Edit(EditEvent {
472 duration: end
473 .saturating_duration_since(start)
474 .min(Duration::from_secs(60 * 60 * 24))
475 .as_millis() as i64,
476 environment: environment.to_string(),
477 is_via_ssh,
478 });
479
480 self.report_event(event);
481 }
482 }
483
484 pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
485 let event = Event::Action(ActionEvent {
486 source: source.to_string(),
487 action,
488 });
489
490 self.report_event(event)
491 }
492
493 pub fn report_discovered_project_events(
494 self: &Arc<Self>,
495 worktree_id: WorktreeId,
496 updated_entries_set: &UpdatedEntriesSet,
497 ) {
498 let project_type_names: Vec<String> = {
499 let mut state = self.state.lock();
500 state
501 .worktree_id_map
502 .0
503 .iter_mut()
504 .filter_map(|(project_file_name, project_type_telemetry)| {
505 if project_type_telemetry
506 .worktree_ids_reported
507 .contains(&worktree_id)
508 {
509 return None;
510 }
511
512 let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
513 path.as_ref()
514 .file_name()
515 .and_then(|name| name.to_str())
516 .map(|name_str| name_str == project_file_name)
517 .unwrap_or(false)
518 });
519
520 if !project_file_found {
521 return None;
522 }
523
524 project_type_telemetry
525 .worktree_ids_reported
526 .insert(worktree_id);
527
528 Some(project_type_telemetry.name.clone())
529 })
530 .collect()
531 };
532
533 // Done on purpose to avoid calling `self.state.lock()` multiple times
534 for project_type_name in project_type_names {
535 self.report_app_event(format!("open {} project", project_type_name));
536 }
537 }
538
539 pub fn report_repl_event(
540 self: &Arc<Self>,
541 kernel_language: String,
542 kernel_status: String,
543 repl_session_id: String,
544 ) {
545 let event = Event::Repl(ReplEvent {
546 kernel_language,
547 kernel_status,
548 repl_session_id,
549 });
550
551 self.report_event(event)
552 }
553
554 fn report_event(self: &Arc<Self>, event: Event) {
555 let mut state = self.state.lock();
556
557 if !state.settings.metrics {
558 return;
559 }
560
561 if state.flush_events_task.is_none() {
562 let this = self.clone();
563 let executor = self.executor.clone();
564 state.flush_events_task = Some(self.executor.spawn(async move {
565 executor.timer(FLUSH_INTERVAL).await;
566 this.flush_events();
567 }));
568 }
569
570 let date_time = self.clock.utc_now();
571
572 let milliseconds_since_first_event = match state.first_event_date_time {
573 Some(first_event_date_time) => date_time
574 .saturating_duration_since(first_event_date_time)
575 .min(Duration::from_secs(60 * 60 * 24))
576 .as_millis() as i64,
577 None => {
578 state.first_event_date_time = Some(date_time);
579 0
580 }
581 };
582
583 let signed_in = state.metrics_id.is_some();
584 state.events_queue.push(EventWrapper {
585 signed_in,
586 milliseconds_since_first_event,
587 event,
588 });
589
590 if state.installation_id.is_some() && state.events_queue.len() >= state.max_queue_size {
591 drop(state);
592 self.flush_events();
593 }
594 }
595
596 pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
597 self.state.lock().metrics_id.clone()
598 }
599
600 pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
601 self.state.lock().installation_id.clone()
602 }
603
604 pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
605 self.state.lock().is_staff
606 }
607
608 fn build_request(
609 self: &Arc<Self>,
610 // We take in the JSON bytes buffer so we can reuse the existing allocation.
611 mut json_bytes: Vec<u8>,
612 event_request: EventRequestBody,
613 ) -> Result<Request<AsyncBody>> {
614 json_bytes.clear();
615 serde_json::to_writer(&mut json_bytes, &event_request)?;
616
617 let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
618
619 Ok(Request::builder()
620 .method(Method::POST)
621 .uri(
622 self.http_client
623 .build_zed_api_url("/telemetry/events", &[])?
624 .as_ref(),
625 )
626 .header("Content-Type", "application/json")
627 .header("x-zed-checksum", checksum)
628 .body(json_bytes.into())?)
629 }
630
631 pub fn flush_events(self: &Arc<Self>) {
632 let mut state = self.state.lock();
633 state.first_event_date_time = None;
634 let mut events = mem::take(&mut state.events_queue);
635 state.flush_events_task.take();
636 drop(state);
637 if events.is_empty() {
638 return;
639 }
640
641 let this = self.clone();
642 self.executor
643 .spawn(
644 async move {
645 let mut json_bytes = Vec::new();
646
647 if let Some(file) = &mut this.state.lock().log_file {
648 for event in &mut events {
649 json_bytes.clear();
650 serde_json::to_writer(&mut json_bytes, event)?;
651 file.write_all(&json_bytes)?;
652 file.write_all(b"\n")?;
653 }
654 }
655
656 let request_body = {
657 let state = this.state.lock();
658
659 EventRequestBody {
660 system_id: state.system_id.as_deref().map(Into::into),
661 installation_id: state.installation_id.as_deref().map(Into::into),
662 session_id: state.session_id.clone(),
663 metrics_id: state.metrics_id.as_deref().map(Into::into),
664 is_staff: state.is_staff,
665 app_version: state.app_version.clone(),
666 os_name: state.os_name.clone(),
667 os_version: state.os_version.clone(),
668 architecture: state.architecture.to_string(),
669
670 release_channel: state.release_channel.map(Into::into),
671 events,
672 }
673 };
674
675 let request = this.build_request(json_bytes, request_body)?;
676 let response = this.http_client.send(request).await?;
677 if response.status() != 200 {
678 log::error!("Failed to send events: HTTP {:?}", response.status());
679 }
680 anyhow::Ok(())
681 }
682 .log_err(),
683 )
684 .detach();
685 }
686}
687
688pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
689 let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
690 return None;
691 };
692
693 let mut summer = Sha256::new();
694 summer.update(checksum_seed);
695 summer.update(json);
696 summer.update(checksum_seed);
697 let mut checksum = String::new();
698 for byte in summer.finalize().as_slice() {
699 use std::fmt::Write;
700 write!(&mut checksum, "{:02x}", byte).unwrap();
701 }
702
703 Some(checksum)
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use clock::FakeSystemClock;
710 use gpui::TestAppContext;
711 use http_client::FakeHttpClient;
712
713 #[gpui::test]
714 fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
715 init_test(cx);
716 let clock = Arc::new(FakeSystemClock::new());
717 let http = FakeHttpClient::with_200_response();
718 let system_id = Some("system_id".to_string());
719 let installation_id = Some("installation_id".to_string());
720 let session_id = "session_id".to_string();
721
722 cx.update(|cx| {
723 let telemetry = Telemetry::new(clock.clone(), http, cx);
724
725 telemetry.state.lock().max_queue_size = 4;
726 telemetry.start(system_id, installation_id, session_id, cx);
727
728 assert!(is_empty_state(&telemetry));
729
730 let first_date_time = clock.utc_now();
731 let operation = "test".to_string();
732
733 let event = telemetry.report_app_event(operation.clone());
734 assert_eq!(
735 event,
736 Event::App(AppEvent {
737 operation: operation.clone(),
738 })
739 );
740 assert_eq!(telemetry.state.lock().events_queue.len(), 1);
741 assert!(telemetry.state.lock().flush_events_task.is_some());
742 assert_eq!(
743 telemetry.state.lock().first_event_date_time,
744 Some(first_date_time)
745 );
746
747 clock.advance(Duration::from_millis(100));
748
749 let event = telemetry.report_app_event(operation.clone());
750 assert_eq!(
751 event,
752 Event::App(AppEvent {
753 operation: operation.clone(),
754 })
755 );
756 assert_eq!(telemetry.state.lock().events_queue.len(), 2);
757 assert!(telemetry.state.lock().flush_events_task.is_some());
758 assert_eq!(
759 telemetry.state.lock().first_event_date_time,
760 Some(first_date_time)
761 );
762
763 clock.advance(Duration::from_millis(100));
764
765 let event = telemetry.report_app_event(operation.clone());
766 assert_eq!(
767 event,
768 Event::App(AppEvent {
769 operation: operation.clone(),
770 })
771 );
772 assert_eq!(telemetry.state.lock().events_queue.len(), 3);
773 assert!(telemetry.state.lock().flush_events_task.is_some());
774 assert_eq!(
775 telemetry.state.lock().first_event_date_time,
776 Some(first_date_time)
777 );
778
779 clock.advance(Duration::from_millis(100));
780
781 // Adding a 4th event should cause a flush
782 let event = telemetry.report_app_event(operation.clone());
783 assert_eq!(
784 event,
785 Event::App(AppEvent {
786 operation: operation.clone(),
787 })
788 );
789
790 assert!(is_empty_state(&telemetry));
791 });
792 }
793
794 #[gpui::test]
795 async fn test_telemetry_flush_on_flush_interval(
796 executor: BackgroundExecutor,
797 cx: &mut TestAppContext,
798 ) {
799 init_test(cx);
800 let clock = Arc::new(FakeSystemClock::new());
801 let http = FakeHttpClient::with_200_response();
802 let system_id = Some("system_id".to_string());
803 let installation_id = Some("installation_id".to_string());
804 let session_id = "session_id".to_string();
805
806 cx.update(|cx| {
807 let telemetry = Telemetry::new(clock.clone(), http, cx);
808 telemetry.state.lock().max_queue_size = 4;
809 telemetry.start(system_id, installation_id, session_id, cx);
810
811 assert!(is_empty_state(&telemetry));
812
813 let first_date_time = clock.utc_now();
814 let operation = "test".to_string();
815
816 let event = telemetry.report_app_event(operation.clone());
817 assert_eq!(
818 event,
819 Event::App(AppEvent {
820 operation: operation.clone(),
821 })
822 );
823 assert_eq!(telemetry.state.lock().events_queue.len(), 1);
824 assert!(telemetry.state.lock().flush_events_task.is_some());
825 assert_eq!(
826 telemetry.state.lock().first_event_date_time,
827 Some(first_date_time)
828 );
829
830 let duration = Duration::from_millis(1);
831
832 // Test 1 millisecond before the flush interval limit is met
833 executor.advance_clock(FLUSH_INTERVAL - duration);
834
835 assert!(!is_empty_state(&telemetry));
836
837 // Test the exact moment the flush interval limit is met
838 executor.advance_clock(duration);
839
840 assert!(is_empty_state(&telemetry));
841 });
842 }
843
844 // TODO:
845 // Test settings
846 // Update FakeHTTPClient to keep track of the number of requests and assert on it
847
848 fn init_test(cx: &mut TestAppContext) {
849 cx.update(|cx| {
850 let settings_store = SettingsStore::test(cx);
851 cx.set_global(settings_store);
852 });
853 }
854
855 fn is_empty_state(telemetry: &Telemetry) -> bool {
856 telemetry.state.lock().events_queue.is_empty()
857 && telemetry.state.lock().flush_events_task.is_none()
858 && telemetry.state.lock().first_event_date_time.is_none()
859 }
860}