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::fs::File;
17use std::io::Write;
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<DateTime<Utc>>,
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 return 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.timestamp_millis() - start.timestamp_millis(),
473 environment: environment.to_string(),
474 is_via_ssh,
475 });
476
477 self.report_event(event);
478 }
479 }
480
481 pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
482 let event = Event::Action(ActionEvent {
483 source: source.to_string(),
484 action,
485 });
486
487 self.report_event(event)
488 }
489
490 pub fn report_discovered_project_events(
491 self: &Arc<Self>,
492 worktree_id: WorktreeId,
493 updated_entries_set: &UpdatedEntriesSet,
494 ) {
495 let project_type_names: Vec<String> = {
496 let mut state = self.state.lock();
497 state
498 .worktree_id_map
499 .0
500 .iter_mut()
501 .filter_map(|(project_file_name, project_type_telemetry)| {
502 if project_type_telemetry
503 .worktree_ids_reported
504 .contains(&worktree_id)
505 {
506 return None;
507 }
508
509 let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
510 path.as_ref()
511 .file_name()
512 .and_then(|name| name.to_str())
513 .map(|name_str| name_str == project_file_name)
514 .unwrap_or(false)
515 });
516
517 if !project_file_found {
518 return None;
519 }
520
521 project_type_telemetry
522 .worktree_ids_reported
523 .insert(worktree_id);
524
525 Some(project_type_telemetry.name.clone())
526 })
527 .collect()
528 };
529
530 // Done on purpose to avoid calling `self.state.lock()` multiple times
531 for project_type_name in project_type_names {
532 self.report_app_event(format!("open {} project", project_type_name));
533 }
534 }
535
536 pub fn report_repl_event(
537 self: &Arc<Self>,
538 kernel_language: String,
539 kernel_status: String,
540 repl_session_id: String,
541 ) {
542 let event = Event::Repl(ReplEvent {
543 kernel_language,
544 kernel_status,
545 repl_session_id,
546 });
547
548 self.report_event(event)
549 }
550
551 fn report_event(self: &Arc<Self>, event: Event) {
552 let mut state = self.state.lock();
553
554 if !state.settings.metrics {
555 return;
556 }
557
558 if state.flush_events_task.is_none() {
559 let this = self.clone();
560 let executor = self.executor.clone();
561 state.flush_events_task = Some(self.executor.spawn(async move {
562 executor.timer(FLUSH_INTERVAL).await;
563 this.flush_events();
564 }));
565 }
566
567 let date_time = self.clock.utc_now();
568
569 let milliseconds_since_first_event = match state.first_event_date_time {
570 Some(first_event_date_time) => {
571 date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
572 }
573 None => {
574 state.first_event_date_time = Some(date_time);
575 0
576 }
577 };
578
579 let signed_in = state.metrics_id.is_some();
580 state.events_queue.push(EventWrapper {
581 signed_in,
582 milliseconds_since_first_event,
583 event,
584 });
585
586 if state.installation_id.is_some() && state.events_queue.len() >= state.max_queue_size {
587 drop(state);
588 self.flush_events();
589 }
590 }
591
592 pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
593 self.state.lock().metrics_id.clone()
594 }
595
596 pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
597 self.state.lock().installation_id.clone()
598 }
599
600 pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
601 self.state.lock().is_staff
602 }
603
604 fn build_request(
605 self: &Arc<Self>,
606 // We take in the JSON bytes buffer so we can reuse the existing allocation.
607 mut json_bytes: Vec<u8>,
608 event_request: EventRequestBody,
609 ) -> Result<Request<AsyncBody>> {
610 json_bytes.clear();
611 serde_json::to_writer(&mut json_bytes, &event_request)?;
612
613 let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
614
615 Ok(Request::builder()
616 .method(Method::POST)
617 .uri(
618 self.http_client
619 .build_zed_api_url("/telemetry/events", &[])?
620 .as_ref(),
621 )
622 .header("Content-Type", "application/json")
623 .header("x-zed-checksum", checksum)
624 .body(json_bytes.into())?)
625 }
626
627 pub fn flush_events(self: &Arc<Self>) {
628 let mut state = self.state.lock();
629 state.first_event_date_time = None;
630 let mut events = mem::take(&mut state.events_queue);
631 state.flush_events_task.take();
632 drop(state);
633 if events.is_empty() {
634 return;
635 }
636
637 let this = self.clone();
638 self.executor
639 .spawn(
640 async move {
641 let mut json_bytes = Vec::new();
642
643 if let Some(file) = &mut this.state.lock().log_file {
644 for event in &mut events {
645 json_bytes.clear();
646 serde_json::to_writer(&mut json_bytes, event)?;
647 file.write_all(&json_bytes)?;
648 file.write_all(b"\n")?;
649 }
650 }
651
652 let request_body = {
653 let state = this.state.lock();
654
655 EventRequestBody {
656 system_id: state.system_id.as_deref().map(Into::into),
657 installation_id: state.installation_id.as_deref().map(Into::into),
658 session_id: state.session_id.clone(),
659 metrics_id: state.metrics_id.as_deref().map(Into::into),
660 is_staff: state.is_staff,
661 app_version: state.app_version.clone(),
662 os_name: state.os_name.clone(),
663 os_version: state.os_version.clone(),
664 architecture: state.architecture.to_string(),
665
666 release_channel: state.release_channel.map(Into::into),
667 events,
668 }
669 };
670
671 let request = this.build_request(json_bytes, request_body)?;
672 let response = this.http_client.send(request).await?;
673 if response.status() != 200 {
674 log::error!("Failed to send events: HTTP {:?}", response.status());
675 }
676 anyhow::Ok(())
677 }
678 .log_err(),
679 )
680 .detach();
681 }
682}
683
684pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
685 let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
686 return None;
687 };
688
689 let mut summer = Sha256::new();
690 summer.update(checksum_seed);
691 summer.update(json);
692 summer.update(checksum_seed);
693 let mut checksum = String::new();
694 for byte in summer.finalize().as_slice() {
695 use std::fmt::Write;
696 write!(&mut checksum, "{:02x}", byte).unwrap();
697 }
698
699 Some(checksum)
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705 use chrono::TimeZone;
706 use clock::FakeSystemClock;
707 use gpui::TestAppContext;
708 use http_client::FakeHttpClient;
709
710 #[gpui::test]
711 fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
712 init_test(cx);
713 let clock = Arc::new(FakeSystemClock::new(
714 Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
715 ));
716 let http = FakeHttpClient::with_200_response();
717 let system_id = Some("system_id".to_string());
718 let installation_id = Some("installation_id".to_string());
719 let session_id = "session_id".to_string();
720
721 cx.update(|cx| {
722 let telemetry = Telemetry::new(clock.clone(), http, cx);
723
724 telemetry.state.lock().max_queue_size = 4;
725 telemetry.start(system_id, installation_id, session_id, cx);
726
727 assert!(is_empty_state(&telemetry));
728
729 let first_date_time = clock.utc_now();
730 let operation = "test".to_string();
731
732 let event = telemetry.report_app_event(operation.clone());
733 assert_eq!(
734 event,
735 Event::App(AppEvent {
736 operation: operation.clone(),
737 })
738 );
739 assert_eq!(telemetry.state.lock().events_queue.len(), 1);
740 assert!(telemetry.state.lock().flush_events_task.is_some());
741 assert_eq!(
742 telemetry.state.lock().first_event_date_time,
743 Some(first_date_time)
744 );
745
746 clock.advance(chrono::Duration::milliseconds(100));
747
748 let event = telemetry.report_app_event(operation.clone());
749 assert_eq!(
750 event,
751 Event::App(AppEvent {
752 operation: operation.clone(),
753 })
754 );
755 assert_eq!(telemetry.state.lock().events_queue.len(), 2);
756 assert!(telemetry.state.lock().flush_events_task.is_some());
757 assert_eq!(
758 telemetry.state.lock().first_event_date_time,
759 Some(first_date_time)
760 );
761
762 clock.advance(chrono::Duration::milliseconds(100));
763
764 let event = telemetry.report_app_event(operation.clone());
765 assert_eq!(
766 event,
767 Event::App(AppEvent {
768 operation: operation.clone(),
769 })
770 );
771 assert_eq!(telemetry.state.lock().events_queue.len(), 3);
772 assert!(telemetry.state.lock().flush_events_task.is_some());
773 assert_eq!(
774 telemetry.state.lock().first_event_date_time,
775 Some(first_date_time)
776 );
777
778 clock.advance(chrono::Duration::milliseconds(100));
779
780 // Adding a 4th event should cause a flush
781 let event = telemetry.report_app_event(operation.clone());
782 assert_eq!(
783 event,
784 Event::App(AppEvent {
785 operation: operation.clone(),
786 })
787 );
788
789 assert!(is_empty_state(&telemetry));
790 });
791 }
792
793 #[gpui::test]
794 async fn test_telemetry_flush_on_flush_interval(
795 executor: BackgroundExecutor,
796 cx: &mut TestAppContext,
797 ) {
798 init_test(cx);
799 let clock = Arc::new(FakeSystemClock::new(
800 Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
801 ));
802 let http = FakeHttpClient::with_200_response();
803 let system_id = Some("system_id".to_string());
804 let installation_id = Some("installation_id".to_string());
805 let session_id = "session_id".to_string();
806
807 cx.update(|cx| {
808 let telemetry = Telemetry::new(clock.clone(), http, cx);
809 telemetry.state.lock().max_queue_size = 4;
810 telemetry.start(system_id, installation_id, session_id, cx);
811
812 assert!(is_empty_state(&telemetry));
813
814 let first_date_time = clock.utc_now();
815 let operation = "test".to_string();
816
817 let event = telemetry.report_app_event(operation.clone());
818 assert_eq!(
819 event,
820 Event::App(AppEvent {
821 operation: operation.clone(),
822 })
823 );
824 assert_eq!(telemetry.state.lock().events_queue.len(), 1);
825 assert!(telemetry.state.lock().flush_events_task.is_some());
826 assert_eq!(
827 telemetry.state.lock().first_event_date_time,
828 Some(first_date_time)
829 );
830
831 let duration = Duration::from_millis(1);
832
833 // Test 1 millisecond before the flush interval limit is met
834 executor.advance_clock(FLUSH_INTERVAL - duration);
835
836 assert!(!is_empty_state(&telemetry));
837
838 // Test the exact moment the flush interval limit is met
839 executor.advance_clock(duration);
840
841 assert!(is_empty_state(&telemetry));
842 });
843 }
844
845 // TODO:
846 // Test settings
847 // Update FakeHTTPClient to keep track of the number of requests and assert on it
848
849 fn init_test(cx: &mut TestAppContext) {
850 cx.update(|cx| {
851 let settings_store = SettingsStore::test(cx);
852 cx.set_global(settings_store);
853 });
854 }
855
856 fn is_empty_state(telemetry: &Telemetry) -> bool {
857 telemetry.state.lock().events_queue.is_empty()
858 && telemetry.state.lock().flush_events_task.is_none()
859 && telemetry.state.lock().first_event_date_time.is_none()
860 }
861}