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