1use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use colored::Colorize;
7
8#[derive(Debug, Clone)]
10pub enum CrashCommand {
11 List,
13 Show {
15 file: PathBuf
17 },
18 Send {
20 file: PathBuf,
22 yes: bool
24 },
25 Clear {
27 yes: bool
29 }
30}
31
32#[derive(Debug, Clone)]
34pub enum TelemetryCommand {
35 Status,
37 Enable,
39 Disable,
41 Crash(CrashCommand),
43 Flush
45}
46
47fn resolve_crash_file(file: &PathBuf) -> Result<PathBuf> {
54 if file.exists() {
55 return Ok(file.clone());
56 }
57 let dir = zoi_telemetry::crash::list_crashes();
58 for known in dir {
59 if known.ends_with(file)
60 || known.file_name().is_some_and(|n| n == file.as_os_str())
61 {
62 return Ok(known);
63 }
64 }
65 Err(anyhow::anyhow!(
66 "Crash report not found: {}. Run 'zoi telemetry crash list' to see \
67 available reports.",
68 file.display()
69 ))
70}
71
72pub fn run(cmd: TelemetryCommand) -> Result<()> {
79 match cmd {
80 TelemetryCommand::Status => {
81 let cfg = crate::pkg::config::read_config()?;
82 let status = if cfg.telemetry_enabled {
83 "Enabled".green()
84 } else {
85 "Disabled".yellow()
86 };
87 println!(
88 "{} telemetry is currently {}.",
89 "::".bold().blue(),
90 status
91 );
92
93 if cfg.telemetry_enabled {
94 let id = crate::pkg::telemetry::get_anonymous_id();
95 println!("Anonymous Client ID: {}", id.cyan());
96 println!(
97 "\nThank you for helping us improve Zoi! We collect \
98 minimal, anonymous data about:"
99 );
100 println!(
101 "- {} (OS, Arch, Distro, Shell)",
102 "Environment".bold()
103 );
104 println!("- {} (Action, Scope, Reason)", "Operations".bold());
105 println!(
106 "- {} (Name, Version, Repo, License)",
107 "Package Metadata".bold()
108 );
109 let crashes = crate::pkg::telemetry::crash::list_crashes();
110 if crashes.is_empty() {
111 println!(
112 "- {}: none stored locally",
113 "Crash Reports".bold()
114 );
115 } else {
116 println!(
117 "- {}: {} stored locally (see 'zoi telemetry crash \
118 list')",
119 "Crash Reports".bold(),
120 crashes.len()
121 );
122 }
123 } else {
124 println!(
125 "\nTelemetry is anonymous and helps us prioritize \
126 features and platforms."
127 );
128 println!("Run 'zoi telemetry enable' to help the project.");
129 println!(
130 "Note: crash reports are still saved locally to \
131 $XDG_STATE_HOME/zoi/crash/ but never uploaded while \
132 telemetry is disabled."
133 );
134 }
135 }
136 TelemetryCommand::Enable => {
137 let mut cfg = crate::pkg::config::read_user_config()?;
138
139 println!(
140 "{}",
141 "Notice: Enabling telemetry shares anonymous usage data to \
142 help improve Zoi."
143 .dimmed()
144 );
145 println!(
146 "{}",
147 "Success analytics contain no personal data or IP addresses. \
148 Failure reports may include error text with local paths."
149 .dimmed()
150 );
151 println!(
152 "{} {}",
153 "Full Privacy Policy:".dimmed(),
154 "https://zillowe.qzz.io/legal/privacy".cyan()
155 );
156
157 cfg.telemetry_enabled = true;
158 crate::pkg::config::write_user_config(&cfg)?;
159 println!("{} telemetry enabled", "Success:".green());
160 }
161 TelemetryCommand::Disable => {
162 let mut cfg = crate::pkg::config::read_user_config()?;
163 cfg.telemetry_enabled = false;
164 crate::pkg::config::write_user_config(&cfg)?;
165 println!("{} telemetry disabled", "Success:".green());
166 }
167 TelemetryCommand::Crash(crash_cmd) => run_crash(crash_cmd)?,
168 TelemetryCommand::Flush => {
169 let summary = crate::pkg::telemetry::queue::flush_queue();
170 println!(
171 "Telemetry flush: {} sent, {} failed, {} skipped.",
172 summary.sent, summary.failed, summary.skipped
173 );
174 }
175 }
176 Ok(())
177}
178
179fn run_crash(cmd: CrashCommand) -> Result<()> {
185 match cmd {
186 CrashCommand::List => {
187 let crashes = crate::pkg::telemetry::crash::list_crashes();
188 if crashes.is_empty() {
189 println!("No crash reports stored locally.");
190 println!(
191 "Crash reports are saved to $XDG_STATE_HOME/zoi/crash/ \
192 (default ~/.local/state/zoi/crash/) with a .zoicrash \
193 extension."
194 );
195 } else {
196 println!("Stored crash reports:");
197 for path in crashes {
198 println!(" {}", path.display());
199 }
200 }
201 }
202 CrashCommand::Show { file } => {
203 let path = resolve_crash_file(&file)?;
204 let contents =
205 std::fs::read_to_string(&path).with_context(|| {
206 format!("Failed to read {}", path.display())
207 })?;
208 println!("{contents}");
209 }
210 CrashCommand::Send { file, yes } => {
211 let path = resolve_crash_file(&file)?;
212 if !yes
213 && !crate::pkg::utils::ask_for_confirmation(
214 &format!(
215 "Upload {} to Sentry? Crash reports may contain stack \
216 memory snapshots.",
217 path.display()
218 ),
219 false
220 )
221 {
222 println!("Upload cancelled.");
223 return Ok(());
224 }
225 let sent = crate::pkg::telemetry::crash::send_crash_report(&path)
226 .map_err(|e| anyhow::anyhow!("{e}"))?;
227 if sent {
228 println!("{} crash report uploaded", "Success:".green());
229 std::fs::remove_file(&path).with_context(|| {
230 format!("Uploaded but failed to remove {}", path.display())
231 })?;
232 } else {
233 println!(
234 "{} telemetry is disabled; crash report was not uploaded. \
235 Enable with 'zoi telemetry enable' first.",
236 "Skipped:".yellow()
237 );
238 }
239 }
240 CrashCommand::Clear { yes } => {
241 let crashes = crate::pkg::telemetry::crash::list_crashes();
242 if crashes.is_empty() {
243 println!("No crash reports to delete.");
244 return Ok(());
245 }
246 if !yes
247 && !crate::pkg::utils::ask_for_confirmation(
248 &format!(
249 "Delete {} stored crash report(s)?",
250 crashes.len()
251 ),
252 false
253 )
254 {
255 println!("Cancelled.");
256 return Ok(());
257 }
258 for path in crashes {
259 std::fs::remove_file(&path).with_context(|| {
260 format!("Failed to remove {}", path.display())
261 })?;
262 }
263 println!("{} crash reports cleared", "Success:".green());
264 }
265 }
266 Ok(())
267}