1use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
4use std::borrow::Cow;
5
6#[derive(Debug, thiserror::Error, PartialEq, Eq)]
8pub enum CliError {
9 #[error("unknown or missing command")]
11 UnknownCommand,
12 #[error("unknown command: {command}")]
14 UnknownCommandName {
15 command: String,
17 next: &'static str,
19 },
20 #[error("missing argument: {argument}")]
22 MissingArgument {
23 argument: &'static str,
25 next: &'static str,
27 },
28 #[error("missing value for {flag}")]
30 MissingFlagValue {
31 flag: &'static str,
33 next: &'static str,
35 },
36 #[error("duplicate flag: {flag}")]
38 DuplicateFlag {
39 flag: &'static str,
41 next: &'static str,
43 },
44 #[error("unexpected argument for {command}: {argument}")]
46 UnexpectedArgument {
47 argument: String,
49 command: &'static str,
51 next: &'static str,
53 },
54 #[error("unknown flag: {flag}")]
56 UnknownFlag {
57 flag: String,
59 next: &'static str,
61 },
62 #[error("unsupported flag for {command}: {flag}")]
64 UnsupportedFlag {
65 flag: String,
67 command: &'static str,
69 next: &'static str,
71 },
72 #[error("unknown resource: {resource}")]
74 UnknownResource {
75 resource: String,
77 next: &'static str,
79 },
80 #[error("unknown issue status: {0}")]
82 UnknownStatus(String),
83 #[error("unknown trace status: {0}")]
85 UnknownTraceStatus(String),
86 #[error("unknown log level: {0}")]
88 UnknownLogLevel(String),
89 #[error("invalid limit: {0}")]
91 InvalidLimit(String),
92 #[error("invalid minimum duration: {0}")]
94 InvalidMinDuration(String),
95 #[error("unknown pagination mode")]
97 UnknownPagination,
98 #[error("invalid action cursor: {0}")]
100 InvalidActionCursor(String),
101 #[error("invalid log cursor: {0}")]
103 InvalidLogCursor(String),
104 #[error("invalid issue cursor: {0}")]
106 InvalidIssueCursor(String),
107 #[error("invalid support cursor: {0}")]
109 InvalidSupportCursor(String),
110 #[error("unknown support category")]
112 UnknownSupportCategory,
113 #[error("invalid support ticket id")]
115 InvalidSupportTicketId,
116 #[error("invalid support retry key")]
118 InvalidSupportRetryKey,
119 #[error("invalid support context reply")]
121 InvalidSupportContextReply,
122 #[error("invalid support context command")]
124 InvalidSupportContextCommand,
125 #[error("invalid support context")]
127 InvalidSupportContext,
128 #[error("invalid issue investigation command")]
130 InvalidInvestigationCommand,
131 #[error("invalid project doctor command")]
133 InvalidDoctorCommand,
134 #[error("invalid project create command")]
136 InvalidProjectCreateCommand,
137 #[error("invalid usage command")]
139 InvalidUsageCommand,
140 #[error("invalid native debug-artifact command")]
142 InvalidNativeDebugCommand,
143 #[error("invalid native debug-artifact identity")]
145 InvalidNativeDebugIdentity,
146 #[error("invalid setup source: {0}")]
148 InvalidSetupSource(String),
149}
150
151#[derive(Debug, thiserror::Error)]
153pub enum RuntimeError {
154 #[error(transparent)]
156 Cli(#[from] CliError),
157 #[error(transparent)]
159 Io(#[from] std::io::Error),
160 #[error(transparent)]
162 Http(#[from] reqwest::Error),
163 #[error("not logged in: run logbrew login")]
165 MissingToken,
166 #[error("api returned status {status}: {body}")]
168 Api {
169 status: u16,
171 body: String,
173 auth_source: &'static str,
175 auth_label: &'static str,
177 },
178 #[error("LogBrew API unreachable: {message}")]
180 StatusUnavailable {
181 api_url: String,
183 status_code: Option<u16>,
185 body: Option<String>,
187 authenticated: bool,
189 auth_source: &'static str,
191 auth_label: &'static str,
193 message: String,
195 },
196 #[error("{message}")]
198 Unavailable {
199 message: &'static str,
201 next: &'static str,
203 },
204 #[error("issue investigation returned an invalid response")]
206 InvestigationResponseInvalid,
207 #[error("native debug artifact is invalid")]
209 NativeDebugArtifactInvalid,
210 #[error("native debug-artifact response is invalid")]
212 NativeDebugResponseInvalid,
213 #[error("native debug-artifact verification failed")]
215 NativeDebugVerificationFailed,
216}
217
218pub fn write_cli_error<W: std::io::Write>(
224 error: &CliError,
225 json: bool,
226 output: &mut W,
227) -> Result<(), std::io::Error> {
228 if json {
229 let body = serde_json::json!({
230 "ok": false,
231 "error": cli_error_code(error),
232 "message": error.to_string(),
233 "next": cli_error_next_step(error),
234 });
235 writeln!(output, "{body}")
236 } else {
237 writeln!(output, "{error}")?;
238 writeln!(output, "Next: {}", cli_error_next_step(error))
239 }
240}
241
242pub fn write_runtime_error<W: std::io::Write>(
248 error: &RuntimeError,
249 json: bool,
250 output: &mut W,
251) -> Result<(), std::io::Error> {
252 if json {
253 let body = runtime_error_json(error);
254 writeln!(output, "{body}")
255 } else {
256 write_human_runtime_error(error, output)
257 }
258}
259
260fn write_human_runtime_error<W: std::io::Write>(
262 error: &RuntimeError,
263 output: &mut W,
264) -> Result<(), std::io::Error> {
265 match error {
266 RuntimeError::StatusUnavailable {
267 api_url,
268 status_code,
269 body,
270 auth_label,
271 message,
272 ..
273 } => {
274 writeln!(output, "LogBrew API unreachable.")?;
275 writeln!(output, "API: {api_url}")?;
276 writeln!(output, "Auth: {auth_label}")?;
277 if let Some(status_code) = status_code {
278 writeln!(output, "Status: {status_code}")?;
279 }
280 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
281 writeln!(output, "Body: {body}")?;
282 } else {
283 writeln!(output, "Reason: {message}")?;
284 }
285 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
286 Ok(())
287 }
288 RuntimeError::Api {
289 status,
290 body,
291 auth_label,
292 ..
293 } => {
294 let api_details = ApiErrorDetails::parse(body);
295 writeln!(output, "{error}")?;
296 if let Some(code) = api_details.code.as_deref() {
297 writeln!(output, "Code: {code}")?;
298 }
299 writeln!(output, "Auth: {auth_label}")?;
300 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
301 }
302 RuntimeError::Cli(_)
303 | RuntimeError::Io(_)
304 | RuntimeError::Http(_)
305 | RuntimeError::MissingToken
306 | RuntimeError::InvestigationResponseInvalid
307 | RuntimeError::NativeDebugArtifactInvalid
308 | RuntimeError::NativeDebugResponseInvalid
309 | RuntimeError::NativeDebugVerificationFailed
310 | RuntimeError::Unavailable { .. } => {
311 writeln!(output, "{error}")?;
312 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
313 Ok(())
314 }
315 }
316}
317
318fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
320 match error {
321 RuntimeError::MissingToken
322 | RuntimeError::Unavailable { .. }
323 | RuntimeError::InvestigationResponseInvalid
324 | RuntimeError::NativeDebugArtifactInvalid
325 | RuntimeError::NativeDebugResponseInvalid
326 | RuntimeError::NativeDebugVerificationFailed
327 | RuntimeError::Io(_)
328 | RuntimeError::Http(_) => serde_json::json!({
329 "ok": false,
330 "error": runtime_error_code(error),
331 "message": error.to_string(),
332 "next": runtime_error_next_step(error),
333 }),
334 RuntimeError::Api {
335 status,
336 body,
337 auth_source,
338 ..
339 } => {
340 let api_details = ApiErrorDetails::parse(body);
341 let next = api_next_step(*status, &api_details);
342 serde_json::json!({
343 "ok": false,
344 "error": runtime_error_code(error),
345 "message": error.to_string(),
346 "status": status,
347 "body": body,
348 "api_error": api_details.error.as_deref(),
349 "api_code": api_details.code.as_deref(),
350 "api_next": api_details.next.as_deref(),
351 "auth_source": auth_source,
352 "next": next,
353 })
354 }
355 RuntimeError::StatusUnavailable {
356 api_url,
357 status_code,
358 body,
359 authenticated,
360 auth_source,
361 message,
362 ..
363 } => serde_json::json!({
364 "ok": false,
365 "error": runtime_error_code(error),
366 "status": "unreachable",
367 "status_code": status_code,
368 "body": body,
369 "api_url": api_url,
370 "authenticated": authenticated,
371 "auth_source": auth_source,
372 "message": message,
373 "next": runtime_error_next_step(error),
374 }),
375 RuntimeError::Cli(error) => serde_json::json!({
376 "ok": false,
377 "error": cli_error_code(error),
378 "message": error.to_string(),
379 "next": cli_error_next_step(error),
380 }),
381 }
382}
383
384const fn cli_error_code(error: &CliError) -> &'static str {
386 match error {
387 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
388 CliError::MissingArgument { .. } => "missing_argument",
389 CliError::MissingFlagValue { .. } => "missing_flag_value",
390 CliError::DuplicateFlag { .. } => "duplicate_flag",
391 CliError::UnexpectedArgument { .. } => "unexpected_argument",
392 CliError::UnknownFlag { .. } => "unknown_flag",
393 CliError::UnsupportedFlag { .. } => "unsupported_flag",
394 CliError::UnknownResource { .. } => "unknown_resource",
395 CliError::UnknownStatus(_) => "unknown_status",
396 CliError::UnknownTraceStatus(_) => "unknown_trace_status",
397 CliError::UnknownLogLevel(_) => "unknown_log_level",
398 CliError::InvalidLimit(_) => "invalid_limit",
399 CliError::InvalidMinDuration(_) => "invalid_min_duration",
400 CliError::UnknownPagination => "unknown_pagination",
401 CliError::InvalidActionCursor(_) => "invalid_action_cursor",
402 CliError::InvalidLogCursor(_) => "invalid_log_cursor",
403 CliError::InvalidIssueCursor(_) => "invalid_issue_cursor",
404 CliError::InvalidSupportCursor(_) => "invalid_support_cursor",
405 CliError::UnknownSupportCategory => "unknown_support_category",
406 CliError::InvalidSupportTicketId => "invalid_support_ticket_id",
407 CliError::InvalidSupportRetryKey => "invalid_support_retry_key",
408 CliError::InvalidSupportContextReply => "invalid_support_context_reply",
409 CliError::InvalidSupportContextCommand => "invalid_support_context_command",
410 CliError::InvalidSupportContext => "invalid_support_context",
411 CliError::InvalidInvestigationCommand => "invalid_investigation_command",
412 CliError::InvalidDoctorCommand => "invalid_doctor_command",
413 CliError::InvalidProjectCreateCommand => "invalid_project_create_command",
414 CliError::InvalidUsageCommand => "invalid_usage_command",
415 CliError::InvalidNativeDebugCommand | CliError::InvalidNativeDebugIdentity => {
416 "invalid_native_debug_command"
417 }
418 CliError::InvalidSetupSource(_) => "invalid_setup_source",
419 }
420}
421
422const fn cli_error_next_step(error: &CliError) -> &'static str {
424 match error {
425 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
426 CliError::InvalidMinDuration(_) => "use --min-duration-ms with a non-negative whole number",
427 CliError::UnknownPagination
428 | CliError::InvalidActionCursor(_)
429 | CliError::InvalidLogCursor(_)
430 | CliError::InvalidIssueCursor(_)
431 | CliError::InvalidSupportCursor(_) => {
432 "use --pagination cursor alone for the first page, then use --cursor-time and --cursor-id together from next_cursor"
433 }
434 CliError::UnknownSupportCategory => {
435 "use sdk_install_failure, ingest_failure, auth_failure, project_setup, dashboard_issue, docs_confusion, cli_issue, mobile_issue, billing_question, or other"
436 }
437 CliError::InvalidSupportTicketId => {
438 "use the ticket_id returned by logbrew support create or list"
439 }
440 CliError::InvalidSupportRetryKey => {
441 "use --retry-key with 1 to 128 visible ASCII characters and reuse it only for an exact retry"
442 }
443 CliError::InvalidSupportContextReply => {
444 "use support reply <ticket_id> --context <text> --retry-key <key>"
445 }
446 CliError::InvalidSupportContextCommand => {
447 "use support context <ticket_id> with optional --json"
448 }
449 CliError::InvalidSupportContext => {
450 "use --context with 1 to 4000 characters after trimming whitespace"
451 }
452 CliError::InvalidInvestigationCommand => {
453 "use logbrew investigate issue <issue_id> with optional --json"
454 }
455 CliError::InvalidDoctorCommand => {
456 "use logbrew doctor --project <project_id> with optional --json"
457 }
458 CliError::InvalidProjectCreateCommand => {
459 "use logbrew projects create <name> --ingest-key-file <path> with optional --runtime, --environment, --abandon-retry, and --json"
460 }
461 CliError::InvalidUsageCommand => "use logbrew usage with optional --json",
462 CliError::InvalidNativeDebugCommand => {
463 "use logbrew debug-artifacts upload <path> --project <project_id> --release <release> --environment <environment> --service <service>"
464 }
465 CliError::InvalidNativeDebugIdentity => {
466 "use a lowercase UUID and architecture arm64, arm64e, or x86_64"
467 }
468 CliError::InvalidSetupSource(_) => "use --source api, cli, or sdk",
469 CliError::MissingArgument { next, .. }
470 | CliError::MissingFlagValue { next, .. }
471 | CliError::DuplicateFlag { next, .. }
472 | CliError::UnexpectedArgument { next, .. }
473 | CliError::UnsupportedFlag { next, .. }
474 | CliError::UnknownFlag { next, .. }
475 | CliError::UnknownResource { next, .. }
476 | CliError::UnknownCommandName { next, .. } => next,
477 CliError::UnknownCommand => "run logbrew --help",
478 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
479 CliError::UnknownTraceStatus(_) => "use --status error or --status ok",
480 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
481 }
482}
483
484const fn runtime_error_code(error: &RuntimeError) -> &'static str {
486 match error {
487 RuntimeError::Cli(error) => cli_error_code(error),
488 RuntimeError::Io(_) => "io_error",
489 RuntimeError::Http(_) => "http_error",
490 RuntimeError::MissingToken => "not_logged_in",
491 RuntimeError::Api { .. } => "api_error",
492 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
493 RuntimeError::Unavailable { .. } => "unavailable",
494 RuntimeError::InvestigationResponseInvalid => "investigation_response_invalid",
495 RuntimeError::NativeDebugArtifactInvalid => "native_debug_artifact_invalid",
496 RuntimeError::NativeDebugResponseInvalid => "native_debug_response_invalid",
497 RuntimeError::NativeDebugVerificationFailed => "native_debug_verification_failed",
498 }
499}
500
501const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
503
504#[derive(Debug, Clone, Default, PartialEq, Eq)]
506struct ApiErrorDetails {
507 error: Option<String>,
509 code: Option<String>,
511 next: Option<String>,
513}
514
515impl ApiErrorDetails {
516 fn parse(body: &str) -> Self {
518 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
519 return Self::default();
520 };
521
522 Self {
523 error: json_string_field(&value, "error"),
524 code: json_string_field(&value, "code"),
525 next: json_string_field(&value, "next"),
526 }
527 }
528}
529
530fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
532 value
533 .get(key)
534 .and_then(serde_json::Value::as_str)
535 .map(str::trim)
536 .filter(|value| !value.is_empty())
537 .map(ToOwned::to_owned)
538}
539
540fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
542 match error {
543 RuntimeError::Api { status, body, .. } => {
544 let api_details = ApiErrorDetails::parse(body);
545 api_next_step(*status, &api_details)
546 }
547 RuntimeError::Cli(_)
548 | RuntimeError::Io(_)
549 | RuntimeError::Http(_)
550 | RuntimeError::MissingToken
551 | RuntimeError::InvestigationResponseInvalid
552 | RuntimeError::NativeDebugArtifactInvalid
553 | RuntimeError::NativeDebugResponseInvalid
554 | RuntimeError::NativeDebugVerificationFailed
555 | RuntimeError::StatusUnavailable { .. }
556 | RuntimeError::Unavailable { .. } => {
557 Cow::Borrowed(fallback_runtime_error_next_step(error))
558 }
559 }
560}
561
562fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
564 api_details.next.as_ref().map_or_else(
565 || Cow::Borrowed(fallback_api_next_step(status)),
566 |next| Cow::Owned(next.clone()),
567 )
568}
569
570const fn fallback_api_next_step(status: u16) -> &'static str {
572 match status {
573 401 | 403 => "run logbrew login",
574 400 | 422 => "check command arguments or filters",
575 404 => "check the resource id or filters",
576 429 => "retry later",
577 500..=599 => "check LOGBREW_API_URL or retry later",
578 _ => "check command arguments or retry later",
579 }
580}
581
582const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
584 match error {
585 RuntimeError::Cli(error) => cli_error_next_step(error),
586 RuntimeError::MissingToken => "run logbrew login",
587 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
588 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
589 STATUS_UNAVAILABLE_NEXT_STEP
590 }
591 RuntimeError::Unavailable { next, .. } => next,
592 RuntimeError::InvestigationResponseInvalid => {
593 "retry the issue investigation; if it repeats, report the public response contract"
594 }
595 RuntimeError::NativeDebugArtifactInvalid => {
596 "provide one validated Apple dSYM bundle or Mach-O debug object"
597 }
598 RuntimeError::NativeDebugResponseInvalid => {
599 "retry the native debug-artifact request; if it repeats, report the public response contract"
600 }
601 RuntimeError::NativeDebugVerificationFailed => {
602 "retry exact native debug-artifact lookup before using native symbolication"
603 }
604 RuntimeError::Io(_) => "check local files and permissions",
605 }
606}