1use std::collections::HashMap;
2
3use once_cell::sync::Lazy;
4use uuid::Uuid;
5
6use crate::constants::{DEFAULT_PATCH_API_PROXY_URL, DEFAULT_SOCKET_API_URL, USER_AGENT};
7use crate::utils::env_compat::read_env_with_legacy;
8
9static SESSION_ID: Lazy<String> = Lazy::new(|| Uuid::new_v4().to_string());
16
17const PACKAGE_VERSION: &str = "1.0.0";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PatchTelemetryEventType {
27 PatchApplied,
29 PatchApplyFailed,
30 PatchRemoved,
31 PatchRemoveFailed,
32 PatchRolledBack,
33 PatchRollbackFailed,
34 PatchScanned,
36 PatchScanFailed,
37 PatchFetched,
38 PatchFetchFailed,
39 PatchListed,
41 PatchRepaired,
42 PatchRepairFailed,
43 PatchSetup,
44 PatchUnlocked,
45 PatchUnlockFailed,
46 VexGenerated,
48 VexFailed,
49}
50
51impl PatchTelemetryEventType {
52 pub fn as_str(&self) -> &'static str {
54 match self {
55 Self::PatchApplied => "patch_applied",
56 Self::PatchApplyFailed => "patch_apply_failed",
57 Self::PatchRemoved => "patch_removed",
58 Self::PatchRemoveFailed => "patch_remove_failed",
59 Self::PatchRolledBack => "patch_rolled_back",
60 Self::PatchRollbackFailed => "patch_rollback_failed",
61 Self::PatchScanned => "patch_scanned",
62 Self::PatchScanFailed => "patch_scan_failed",
63 Self::PatchFetched => "patch_fetched",
64 Self::PatchFetchFailed => "patch_fetch_failed",
65 Self::PatchListed => "patch_listed",
66 Self::PatchRepaired => "patch_repaired",
67 Self::PatchRepairFailed => "patch_repair_failed",
68 Self::PatchSetup => "patch_setup",
69 Self::PatchUnlocked => "patch_unlocked",
70 Self::PatchUnlockFailed => "patch_unlock_failed",
71 Self::VexGenerated => "vex_generated",
72 Self::VexFailed => "vex_failed",
73 }
74 }
75}
76
77#[derive(Debug, Clone, serde::Serialize)]
79pub struct PatchTelemetryContext {
80 pub version: String,
81 pub platform: String,
82 pub arch: String,
83 pub command: String,
84}
85
86#[derive(Debug, Clone, serde::Serialize)]
88pub struct PatchTelemetryError {
89 #[serde(rename = "type")]
90 pub error_type: String,
91 pub message: Option<String>,
92}
93
94#[derive(Debug, Clone, serde::Serialize)]
96pub struct PatchTelemetryEvent {
97 pub event_sender_created_at: String,
98 pub event_type: String,
99 pub context: PatchTelemetryContext,
100 pub session_id: String,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub metadata: Option<HashMap<String, serde_json::Value>>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub error: Option<PatchTelemetryError>,
105}
106
107pub struct TrackPatchEventOptions {
109 pub event_type: PatchTelemetryEventType,
111 pub command: String,
113 pub metadata: Option<HashMap<String, serde_json::Value>>,
115 pub error: Option<(String, String)>,
118 pub api_token: Option<String>,
120 pub org_slug: Option<String>,
122}
123
124pub fn is_telemetry_disabled() -> bool {
142 let env_value =
143 read_env_with_legacy("SOCKET_TELEMETRY_DISABLED", "SOCKET_PATCH_TELEMETRY_DISABLED")
144 .unwrap_or_default();
145 let disabled_via_env = matches!(env_value.as_str(), "1" | "true");
146 let vitest = std::env::var("VITEST").unwrap_or_default() == "true";
147 let offline = matches!(
148 std::env::var("SOCKET_OFFLINE").unwrap_or_default().as_str(),
149 "1" | "true"
150 );
151 disabled_via_env || vitest || offline
152}
153
154fn is_debug_enabled() -> bool {
157 matches!(
158 read_env_with_legacy("SOCKET_DEBUG", "SOCKET_PATCH_DEBUG")
159 .unwrap_or_default()
160 .as_str(),
161 "1" | "true"
162 )
163}
164
165fn debug_log(message: &str) {
167 if is_debug_enabled() {
168 eprintln!("[socket-patch telemetry] {message}");
169 }
170}
171
172fn build_telemetry_context(command: &str) -> PatchTelemetryContext {
178 PatchTelemetryContext {
179 version: PACKAGE_VERSION.to_string(),
180 platform: std::env::consts::OS.to_string(),
181 arch: std::env::consts::ARCH.to_string(),
182 command: command.to_string(),
183 }
184}
185
186pub fn sanitize_error_message(message: &str) -> String {
191 if let Some(home) = home_dir_string() {
192 if !home.is_empty() {
193 return message.replace(&home, "~");
194 }
195 }
196 message.to_string()
197}
198
199fn home_dir_string() -> Option<String> {
201 std::env::var("HOME")
202 .ok()
203 .or_else(|| std::env::var("USERPROFILE").ok())
204}
205
206fn build_telemetry_event(options: &TrackPatchEventOptions) -> PatchTelemetryEvent {
208 let error = options.error.as_ref().map(|(error_type, message)| {
209 PatchTelemetryError {
210 error_type: error_type.clone(),
211 message: Some(sanitize_error_message(message)),
212 }
213 });
214
215 PatchTelemetryEvent {
216 event_sender_created_at: chrono_now_iso(),
217 event_type: options.event_type.as_str().to_string(),
218 context: build_telemetry_context(&options.command),
219 session_id: SESSION_ID.clone(),
220 metadata: options.metadata.clone(),
221 error,
222 }
223}
224
225fn chrono_now_iso() -> String {
227 let now = std::time::SystemTime::now();
228 let duration = now
229 .duration_since(std::time::UNIX_EPOCH)
230 .unwrap_or_default();
231 let secs = duration.as_secs();
232
233 let days = secs / 86400;
234 let remaining = secs % 86400;
235 let hours = remaining / 3600;
236 let minutes = (remaining % 3600) / 60;
237 let seconds = remaining % 60;
238 let millis = duration.subsec_millis();
239
240 let (year, month, day) = days_to_ymd(days);
241
242 format!(
243 "{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z"
244 )
245}
246
247fn days_to_ymd(days: u64) -> (u64, u64, u64) {
249 let z = days as i64 + 719468;
251 let era = if z >= 0 { z } else { z - 146096 } / 146097;
252 let doe = (z - era * 146097) as u64;
253 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
254 let y = yoe as i64 + era * 400;
255 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
256 let mp = (5 * doy + 2) / 153;
257 let d = doy - (153 * mp + 2) / 5 + 1;
258 let m = if mp < 10 { mp + 3 } else { mp - 9 };
259 let y = if m <= 2 { y + 1 } else { y };
260 (y as u64, m, d)
261}
262
263async fn send_telemetry_event(
272 event: &PatchTelemetryEvent,
273 api_token: Option<&str>,
274 org_slug: Option<&str>,
275) {
276 let (url, use_auth) = match (api_token, org_slug) {
277 (Some(_token), Some(slug)) => {
278 let api_url = std::env::var("SOCKET_API_URL")
279 .unwrap_or_else(|_| DEFAULT_SOCKET_API_URL.to_string());
280 (format!("{api_url}/v0/orgs/{slug}/telemetry"), true)
281 }
282 _ => {
283 let proxy_url =
284 read_env_with_legacy("SOCKET_PROXY_URL", "SOCKET_PATCH_PROXY_URL")
285 .unwrap_or_else(|| DEFAULT_PATCH_API_PROXY_URL.to_string());
286 (format!("{proxy_url}/patch/telemetry"), false)
287 }
288 };
289
290 debug_log(&format!("Sending telemetry to {url}"));
291
292 let client = match reqwest::Client::builder()
293 .timeout(std::time::Duration::from_secs(5))
294 .build()
295 {
296 Ok(c) => c,
297 Err(e) => {
298 debug_log(&format!("Failed to build HTTP client: {e}"));
299 return;
300 }
301 };
302
303 let mut request = client
304 .post(&url)
305 .header("Content-Type", "application/json")
306 .header("User-Agent", USER_AGENT);
307
308 if use_auth {
309 if let Some(token) = api_token {
310 request = request.header("Authorization", format!("Bearer {token}"));
311 }
312 }
313
314 match request.json(event).send().await {
315 Ok(response) => {
316 let status = response.status();
317 if status.is_success() {
318 debug_log("Telemetry sent successfully");
319 } else {
320 debug_log(&format!("Telemetry request returned status {status}"));
321 }
322 }
323 Err(e) => {
324 debug_log(&format!("Telemetry request failed: {e}"));
325 }
326 }
327}
328
329pub async fn track_patch_event(options: TrackPatchEventOptions) {
341 if is_telemetry_disabled() {
342 debug_log("Telemetry is disabled, skipping event");
343 return;
344 }
345
346 let event = build_telemetry_event(&options);
347 send_telemetry_event(
348 &event,
349 options.api_token.as_deref(),
350 options.org_slug.as_deref(),
351 )
352 .await;
353}
354
355pub async fn track_patch_applied(
364 patches_count: usize,
365 dry_run: bool,
366 api_token: Option<&str>,
367 org_slug: Option<&str>,
368) {
369 let mut metadata = HashMap::new();
370 metadata.insert(
371 "patches_count".to_string(),
372 serde_json::Value::Number(serde_json::Number::from(patches_count)),
373 );
374 metadata.insert("dry_run".to_string(), serde_json::Value::Bool(dry_run));
375
376 track_patch_event(TrackPatchEventOptions {
377 event_type: PatchTelemetryEventType::PatchApplied,
378 command: "apply".to_string(),
379 metadata: Some(metadata),
380 error: None,
381 api_token: api_token.map(|s| s.to_string()),
382 org_slug: org_slug.map(|s| s.to_string()),
383 })
384 .await;
385}
386
387pub async fn track_patch_apply_failed(
392 error: impl std::fmt::Display,
393 dry_run: bool,
394 api_token: Option<&str>,
395 org_slug: Option<&str>,
396) {
397 let mut metadata = HashMap::new();
398 metadata.insert("dry_run".to_string(), serde_json::Value::Bool(dry_run));
399
400 track_patch_event(TrackPatchEventOptions {
401 event_type: PatchTelemetryEventType::PatchApplyFailed,
402 command: "apply".to_string(),
403 metadata: Some(metadata),
404 error: Some(("Error".to_string(), error.to_string())),
405 api_token: api_token.map(|s| s.to_string()),
406 org_slug: org_slug.map(|s| s.to_string()),
407 })
408 .await;
409}
410
411pub async fn track_patch_removed(
413 removed_count: usize,
414 api_token: Option<&str>,
415 org_slug: Option<&str>,
416) {
417 let mut metadata = HashMap::new();
418 metadata.insert(
419 "removed_count".to_string(),
420 serde_json::Value::Number(serde_json::Number::from(removed_count)),
421 );
422
423 track_patch_event(TrackPatchEventOptions {
424 event_type: PatchTelemetryEventType::PatchRemoved,
425 command: "remove".to_string(),
426 metadata: Some(metadata),
427 error: None,
428 api_token: api_token.map(|s| s.to_string()),
429 org_slug: org_slug.map(|s| s.to_string()),
430 })
431 .await;
432}
433
434pub async fn track_patch_remove_failed(
438 error: impl std::fmt::Display,
439 api_token: Option<&str>,
440 org_slug: Option<&str>,
441) {
442 track_patch_event(TrackPatchEventOptions {
443 event_type: PatchTelemetryEventType::PatchRemoveFailed,
444 command: "remove".to_string(),
445 metadata: None,
446 error: Some(("Error".to_string(), error.to_string())),
447 api_token: api_token.map(|s| s.to_string()),
448 org_slug: org_slug.map(|s| s.to_string()),
449 })
450 .await;
451}
452
453pub async fn track_patch_rolled_back(
455 rolled_back_count: usize,
456 api_token: Option<&str>,
457 org_slug: Option<&str>,
458) {
459 let mut metadata = HashMap::new();
460 metadata.insert(
461 "rolled_back_count".to_string(),
462 serde_json::Value::Number(serde_json::Number::from(rolled_back_count)),
463 );
464
465 track_patch_event(TrackPatchEventOptions {
466 event_type: PatchTelemetryEventType::PatchRolledBack,
467 command: "rollback".to_string(),
468 metadata: Some(metadata),
469 error: None,
470 api_token: api_token.map(|s| s.to_string()),
471 org_slug: org_slug.map(|s| s.to_string()),
472 })
473 .await;
474}
475
476pub async fn track_patch_rollback_failed(
480 error: impl std::fmt::Display,
481 api_token: Option<&str>,
482 org_slug: Option<&str>,
483) {
484 track_patch_event(TrackPatchEventOptions {
485 event_type: PatchTelemetryEventType::PatchRollbackFailed,
486 command: "rollback".to_string(),
487 metadata: None,
488 error: Some(("Error".to_string(), error.to_string())),
489 api_token: api_token.map(|s| s.to_string()),
490 org_slug: org_slug.map(|s| s.to_string()),
491 })
492 .await;
493}
494
495#[allow(clippy::too_many_arguments)]
509pub async fn track_patch_scanned(
510 packages_scanned: usize,
511 free_patches: usize,
512 paid_patches: usize,
513 can_access_paid: bool,
514 ecosystems: &[String],
515 fallback_to_proxy: bool,
516 api_token: Option<&str>,
517 org_slug: Option<&str>,
518) {
519 let mut metadata = HashMap::new();
520 metadata.insert(
521 "packages_scanned".to_string(),
522 serde_json::Value::Number(serde_json::Number::from(packages_scanned)),
523 );
524 metadata.insert(
525 "free_patches".to_string(),
526 serde_json::Value::Number(serde_json::Number::from(free_patches)),
527 );
528 metadata.insert(
529 "paid_patches".to_string(),
530 serde_json::Value::Number(serde_json::Number::from(paid_patches)),
531 );
532 metadata.insert(
533 "can_access_paid".to_string(),
534 serde_json::Value::Bool(can_access_paid),
535 );
536 metadata.insert(
537 "ecosystems".to_string(),
538 serde_json::Value::Array(
539 ecosystems
540 .iter()
541 .map(|e| serde_json::Value::String(e.clone()))
542 .collect(),
543 ),
544 );
545 metadata.insert(
546 "fallback_to_proxy".to_string(),
547 serde_json::Value::Bool(fallback_to_proxy),
548 );
549
550 track_patch_event(TrackPatchEventOptions {
551 event_type: PatchTelemetryEventType::PatchScanned,
552 command: "scan".to_string(),
553 metadata: Some(metadata),
554 error: None,
555 api_token: api_token.map(|s| s.to_string()),
556 org_slug: org_slug.map(|s| s.to_string()),
557 })
558 .await;
559}
560
561pub async fn track_patch_scan_failed(
563 error: impl std::fmt::Display,
564 fallback_to_proxy: bool,
565 api_token: Option<&str>,
566 org_slug: Option<&str>,
567) {
568 let mut metadata = HashMap::new();
569 metadata.insert(
570 "fallback_to_proxy".to_string(),
571 serde_json::Value::Bool(fallback_to_proxy),
572 );
573
574 track_patch_event(TrackPatchEventOptions {
575 event_type: PatchTelemetryEventType::PatchScanFailed,
576 command: "scan".to_string(),
577 metadata: Some(metadata),
578 error: Some(("Error".to_string(), error.to_string())),
579 api_token: api_token.map(|s| s.to_string()),
580 org_slug: org_slug.map(|s| s.to_string()),
581 })
582 .await;
583}
584
585pub async fn track_patch_fetched(
589 uuid: &str,
590 tier: &str,
591 ecosystem: &str,
592 download_mode: &str,
593 fallback_to_proxy: bool,
594 api_token: Option<&str>,
595 org_slug: Option<&str>,
596) {
597 let mut metadata = HashMap::new();
598 metadata.insert(
599 "uuid".to_string(),
600 serde_json::Value::String(uuid.to_string()),
601 );
602 metadata.insert(
603 "tier".to_string(),
604 serde_json::Value::String(tier.to_string()),
605 );
606 metadata.insert(
607 "ecosystem".to_string(),
608 serde_json::Value::String(ecosystem.to_string()),
609 );
610 metadata.insert(
611 "download_mode".to_string(),
612 serde_json::Value::String(download_mode.to_string()),
613 );
614 metadata.insert(
615 "fallback_to_proxy".to_string(),
616 serde_json::Value::Bool(fallback_to_proxy),
617 );
618
619 track_patch_event(TrackPatchEventOptions {
620 event_type: PatchTelemetryEventType::PatchFetched,
621 command: "get".to_string(),
622 metadata: Some(metadata),
623 error: None,
624 api_token: api_token.map(|s| s.to_string()),
625 org_slug: org_slug.map(|s| s.to_string()),
626 })
627 .await;
628}
629
630pub async fn track_patch_fetch_failed(
633 uuid: &str,
634 error: impl std::fmt::Display,
635 fallback_to_proxy: bool,
636 api_token: Option<&str>,
637 org_slug: Option<&str>,
638) {
639 let mut metadata = HashMap::new();
640 metadata.insert(
641 "uuid".to_string(),
642 serde_json::Value::String(uuid.to_string()),
643 );
644 metadata.insert(
645 "fallback_to_proxy".to_string(),
646 serde_json::Value::Bool(fallback_to_proxy),
647 );
648
649 track_patch_event(TrackPatchEventOptions {
650 event_type: PatchTelemetryEventType::PatchFetchFailed,
651 command: "get".to_string(),
652 metadata: Some(metadata),
653 error: Some(("Error".to_string(), error.to_string())),
654 api_token: api_token.map(|s| s.to_string()),
655 org_slug: org_slug.map(|s| s.to_string()),
656 })
657 .await;
658}
659
660pub async fn track_patch_listed(
666 patches_count: usize,
667 api_token: Option<&str>,
668 org_slug: Option<&str>,
669) {
670 let mut metadata = HashMap::new();
671 metadata.insert(
672 "patches_count".to_string(),
673 serde_json::Value::Number(serde_json::Number::from(patches_count)),
674 );
675
676 track_patch_event(TrackPatchEventOptions {
677 event_type: PatchTelemetryEventType::PatchListed,
678 command: "list".to_string(),
679 metadata: Some(metadata),
680 error: None,
681 api_token: api_token.map(|s| s.to_string()),
682 org_slug: org_slug.map(|s| s.to_string()),
683 })
684 .await;
685}
686
687pub async fn track_patch_repaired(
689 blobs_added: usize,
690 blobs_removed: usize,
691 bytes_freed: u64,
692 api_token: Option<&str>,
693 org_slug: Option<&str>,
694) {
695 let mut metadata = HashMap::new();
696 metadata.insert(
697 "blobs_added".to_string(),
698 serde_json::Value::Number(serde_json::Number::from(blobs_added)),
699 );
700 metadata.insert(
701 "blobs_removed".to_string(),
702 serde_json::Value::Number(serde_json::Number::from(blobs_removed)),
703 );
704 metadata.insert(
705 "bytes_freed".to_string(),
706 serde_json::Value::Number(serde_json::Number::from(bytes_freed)),
707 );
708
709 track_patch_event(TrackPatchEventOptions {
710 event_type: PatchTelemetryEventType::PatchRepaired,
711 command: "repair".to_string(),
712 metadata: Some(metadata),
713 error: None,
714 api_token: api_token.map(|s| s.to_string()),
715 org_slug: org_slug.map(|s| s.to_string()),
716 })
717 .await;
718}
719
720pub async fn track_patch_repair_failed(
722 error: impl std::fmt::Display,
723 api_token: Option<&str>,
724 org_slug: Option<&str>,
725) {
726 track_patch_event(TrackPatchEventOptions {
727 event_type: PatchTelemetryEventType::PatchRepairFailed,
728 command: "repair".to_string(),
729 metadata: None,
730 error: Some(("Error".to_string(), error.to_string())),
731 api_token: api_token.map(|s| s.to_string()),
732 org_slug: org_slug.map(|s| s.to_string()),
733 })
734 .await;
735}
736
737pub async fn track_patch_setup(
740 manager: &str,
741 api_token: Option<&str>,
742 org_slug: Option<&str>,
743) {
744 let mut metadata = HashMap::new();
745 metadata.insert(
746 "manager".to_string(),
747 serde_json::Value::String(manager.to_string()),
748 );
749
750 track_patch_event(TrackPatchEventOptions {
751 event_type: PatchTelemetryEventType::PatchSetup,
752 command: "setup".to_string(),
753 metadata: Some(metadata),
754 error: None,
755 api_token: api_token.map(|s| s.to_string()),
756 org_slug: org_slug.map(|s| s.to_string()),
757 })
758 .await;
759}
760
761pub async fn track_patch_unlocked(
765 was_held: bool,
766 released: bool,
767 api_token: Option<&str>,
768 org_slug: Option<&str>,
769) {
770 let mut metadata = HashMap::new();
771 metadata.insert("was_held".to_string(), serde_json::Value::Bool(was_held));
772 metadata.insert("released".to_string(), serde_json::Value::Bool(released));
773
774 track_patch_event(TrackPatchEventOptions {
775 event_type: PatchTelemetryEventType::PatchUnlocked,
776 command: "unlock".to_string(),
777 metadata: Some(metadata),
778 error: None,
779 api_token: api_token.map(|s| s.to_string()),
780 org_slug: org_slug.map(|s| s.to_string()),
781 })
782 .await;
783}
784
785pub async fn track_patch_unlock_failed(
787 error: impl std::fmt::Display,
788 api_token: Option<&str>,
789 org_slug: Option<&str>,
790) {
791 track_patch_event(TrackPatchEventOptions {
792 event_type: PatchTelemetryEventType::PatchUnlockFailed,
793 command: "unlock".to_string(),
794 metadata: None,
795 error: Some(("Error".to_string(), error.to_string())),
796 api_token: api_token.map(|s| s.to_string()),
797 org_slug: org_slug.map(|s| s.to_string()),
798 })
799 .await;
800}
801
802pub async fn track_vex_generated(
809 advisories_count: usize,
810 format: &str,
811 output_kind: &str,
812 api_token: Option<&str>,
813 org_slug: Option<&str>,
814) {
815 let mut metadata = HashMap::new();
816 metadata.insert(
817 "advisories_count".to_string(),
818 serde_json::Value::Number(serde_json::Number::from(advisories_count)),
819 );
820 metadata.insert(
821 "format".to_string(),
822 serde_json::Value::String(format.to_string()),
823 );
824 metadata.insert(
825 "output_kind".to_string(),
826 serde_json::Value::String(output_kind.to_string()),
827 );
828
829 track_patch_event(TrackPatchEventOptions {
830 event_type: PatchTelemetryEventType::VexGenerated,
831 command: "vex".to_string(),
832 metadata: Some(metadata),
833 error: None,
834 api_token: api_token.map(|s| s.to_string()),
835 org_slug: org_slug.map(|s| s.to_string()),
836 })
837 .await;
838}
839
840pub async fn track_vex_failed(
842 error: impl std::fmt::Display,
843 api_token: Option<&str>,
844 org_slug: Option<&str>,
845) {
846 track_patch_event(TrackPatchEventOptions {
847 event_type: PatchTelemetryEventType::VexFailed,
848 command: "vex".to_string(),
849 metadata: None,
850 error: Some(("Error".to_string(), error.to_string())),
851 api_token: api_token.map(|s| s.to_string()),
852 org_slug: org_slug.map(|s| s.to_string()),
853 })
854 .await;
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860
861 #[test]
866 fn test_is_telemetry_disabled() {
867 let orig_new = std::env::var("SOCKET_TELEMETRY_DISABLED").ok();
869 let orig_legacy = std::env::var("SOCKET_PATCH_TELEMETRY_DISABLED").ok();
870 let orig_vitest = std::env::var("VITEST").ok();
871 let orig_offline = std::env::var("SOCKET_OFFLINE").ok();
872
873 std::env::remove_var("SOCKET_TELEMETRY_DISABLED");
875 std::env::remove_var("SOCKET_PATCH_TELEMETRY_DISABLED");
876 std::env::remove_var("VITEST");
877 std::env::remove_var("SOCKET_OFFLINE");
878 assert!(!is_telemetry_disabled());
879
880 std::env::set_var("SOCKET_TELEMETRY_DISABLED", "1");
882 assert!(is_telemetry_disabled());
883 std::env::remove_var("SOCKET_TELEMETRY_DISABLED");
884
885 std::env::set_var("SOCKET_PATCH_TELEMETRY_DISABLED", "1");
887 assert!(is_telemetry_disabled());
888 std::env::set_var("SOCKET_PATCH_TELEMETRY_DISABLED", "true");
889 assert!(is_telemetry_disabled());
890 std::env::remove_var("SOCKET_PATCH_TELEMETRY_DISABLED");
891
892 std::env::set_var("SOCKET_OFFLINE", "1");
895 assert!(
896 is_telemetry_disabled(),
897 "SOCKET_OFFLINE=1 must disable telemetry (airgap)"
898 );
899 std::env::set_var("SOCKET_OFFLINE", "true");
900 assert!(
901 is_telemetry_disabled(),
902 "SOCKET_OFFLINE=true must disable telemetry (airgap)"
903 );
904 std::env::set_var("SOCKET_OFFLINE", "0");
906 assert!(!is_telemetry_disabled());
907 std::env::set_var("SOCKET_OFFLINE", "");
908 assert!(!is_telemetry_disabled());
909 std::env::remove_var("SOCKET_OFFLINE");
910
911 match orig_new {
913 Some(v) => std::env::set_var("SOCKET_TELEMETRY_DISABLED", v),
914 None => std::env::remove_var("SOCKET_TELEMETRY_DISABLED"),
915 }
916 match orig_legacy {
917 Some(v) => std::env::set_var("SOCKET_PATCH_TELEMETRY_DISABLED", v),
918 None => std::env::remove_var("SOCKET_PATCH_TELEMETRY_DISABLED"),
919 }
920 match orig_vitest {
921 Some(v) => std::env::set_var("VITEST", v),
922 None => std::env::remove_var("VITEST"),
923 }
924 match orig_offline {
925 Some(v) => std::env::set_var("SOCKET_OFFLINE", v),
926 None => std::env::remove_var("SOCKET_OFFLINE"),
927 }
928 }
929
930 #[test]
931 fn test_sanitize_error_message() {
932 let home = home_dir_string().unwrap_or_else(|| "/home/testuser".to_string());
933 let msg = format!("Failed to read {home}/projects/secret/file.txt");
934 let sanitized = sanitize_error_message(&msg);
935 assert!(sanitized.contains("~/projects/secret/file.txt"));
936 assert!(!sanitized.contains(&home));
937 }
938
939 #[test]
940 fn test_sanitize_error_message_no_home() {
941 let msg = "Some error without paths";
942 assert_eq!(sanitize_error_message(msg), msg);
943 }
944
945 #[test]
946 fn test_event_type_as_str() {
947 assert_eq!(PatchTelemetryEventType::PatchApplied.as_str(), "patch_applied");
949 assert_eq!(
950 PatchTelemetryEventType::PatchApplyFailed.as_str(),
951 "patch_apply_failed"
952 );
953 assert_eq!(PatchTelemetryEventType::PatchRemoved.as_str(), "patch_removed");
954 assert_eq!(
955 PatchTelemetryEventType::PatchRemoveFailed.as_str(),
956 "patch_remove_failed"
957 );
958 assert_eq!(
959 PatchTelemetryEventType::PatchRolledBack.as_str(),
960 "patch_rolled_back"
961 );
962 assert_eq!(
963 PatchTelemetryEventType::PatchRollbackFailed.as_str(),
964 "patch_rollback_failed"
965 );
966 assert_eq!(PatchTelemetryEventType::PatchScanned.as_str(), "patch_scanned");
968 assert_eq!(
969 PatchTelemetryEventType::PatchScanFailed.as_str(),
970 "patch_scan_failed"
971 );
972 assert_eq!(PatchTelemetryEventType::PatchFetched.as_str(), "patch_fetched");
973 assert_eq!(
974 PatchTelemetryEventType::PatchFetchFailed.as_str(),
975 "patch_fetch_failed"
976 );
977 assert_eq!(PatchTelemetryEventType::PatchListed.as_str(), "patch_listed");
979 assert_eq!(
980 PatchTelemetryEventType::PatchRepaired.as_str(),
981 "patch_repaired"
982 );
983 assert_eq!(
984 PatchTelemetryEventType::PatchRepairFailed.as_str(),
985 "patch_repair_failed"
986 );
987 assert_eq!(PatchTelemetryEventType::PatchSetup.as_str(), "patch_setup");
988 assert_eq!(
989 PatchTelemetryEventType::PatchUnlocked.as_str(),
990 "patch_unlocked"
991 );
992 assert_eq!(
993 PatchTelemetryEventType::PatchUnlockFailed.as_str(),
994 "patch_unlock_failed"
995 );
996 assert_eq!(PatchTelemetryEventType::VexGenerated.as_str(), "vex_generated");
998 assert_eq!(PatchTelemetryEventType::VexFailed.as_str(), "vex_failed");
999 }
1000
1001 #[test]
1002 fn test_build_telemetry_context() {
1003 let ctx = build_telemetry_context("apply");
1004 assert_eq!(ctx.command, "apply");
1005 assert_eq!(ctx.version, PACKAGE_VERSION);
1006 assert!(!ctx.platform.is_empty());
1007 assert!(!ctx.arch.is_empty());
1008 }
1009
1010 #[test]
1011 fn test_build_telemetry_event_basic() {
1012 let options = TrackPatchEventOptions {
1013 event_type: PatchTelemetryEventType::PatchApplied,
1014 command: "apply".to_string(),
1015 metadata: None,
1016 error: None,
1017 api_token: None,
1018 org_slug: None,
1019 };
1020
1021 let event = build_telemetry_event(&options);
1022 assert_eq!(event.event_type, "patch_applied");
1023 assert_eq!(event.context.command, "apply");
1024 assert!(!event.session_id.is_empty());
1025 assert!(!event.event_sender_created_at.is_empty());
1026 assert!(event.metadata.is_none());
1027 assert!(event.error.is_none());
1028 }
1029
1030 #[test]
1031 fn test_build_telemetry_event_with_metadata() {
1032 let mut metadata = HashMap::new();
1033 metadata.insert(
1034 "patches_count".to_string(),
1035 serde_json::Value::Number(5.into()),
1036 );
1037
1038 let options = TrackPatchEventOptions {
1039 event_type: PatchTelemetryEventType::PatchApplied,
1040 command: "apply".to_string(),
1041 metadata: Some(metadata),
1042 error: None,
1043 api_token: None,
1044 org_slug: None,
1045 };
1046
1047 let event = build_telemetry_event(&options);
1048 assert!(event.metadata.is_some());
1049 let meta = event.metadata.unwrap();
1050 assert_eq!(
1051 meta.get("patches_count").unwrap(),
1052 &serde_json::Value::Number(5.into())
1053 );
1054 }
1055
1056 #[test]
1057 fn test_build_telemetry_event_with_error() {
1058 let options = TrackPatchEventOptions {
1059 event_type: PatchTelemetryEventType::PatchApplyFailed,
1060 command: "apply".to_string(),
1061 metadata: None,
1062 error: Some(("IoError".to_string(), "file not found".to_string())),
1063 api_token: None,
1064 org_slug: None,
1065 };
1066
1067 let event = build_telemetry_event(&options);
1068 assert!(event.error.is_some());
1069 let err = event.error.unwrap();
1070 assert_eq!(err.error_type, "IoError");
1071 assert_eq!(err.message.unwrap(), "file not found");
1072 }
1073
1074 #[test]
1075 fn test_session_id_is_consistent() {
1076 let id1 = SESSION_ID.clone();
1077 let id2 = SESSION_ID.clone();
1078 assert_eq!(id1, id2);
1079 assert_eq!(id1.len(), 36);
1081 assert!(id1.contains('-'));
1082 }
1083
1084 #[test]
1085 fn test_chrono_now_iso_format() {
1086 let ts = chrono_now_iso();
1087 assert!(ts.ends_with('Z'));
1089 assert!(ts.contains('T'));
1090 assert!(ts.contains('-'));
1091 assert!(ts.contains(':'));
1092 assert_eq!(ts.len(), 24); }
1094
1095 #[test]
1096 fn test_days_to_ymd_epoch() {
1097 let (y, m, d) = days_to_ymd(0);
1098 assert_eq!((y, m, d), (1970, 1, 1));
1099 }
1100
1101 #[test]
1102 fn test_days_to_ymd_known_date() {
1103 let (y, m, d) = days_to_ymd(19723);
1105 assert_eq!((y, m, d), (2024, 1, 1));
1106 }
1107}