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 projects command")]
139 InvalidProjectsCommand,
140 #[error("invalid usage command")]
142 InvalidUsageCommand,
143 #[error("invalid native debug-artifact command")]
145 InvalidNativeDebugCommand,
146 #[error("invalid native debug-artifact identity")]
148 InvalidNativeDebugIdentity,
149 #[error("invalid setup source: {0}")]
151 InvalidSetupSource(String),
152}
153
154#[derive(Debug, thiserror::Error)]
156pub enum RuntimeError {
157 #[error(transparent)]
159 Cli(#[from] CliError),
160 #[error(transparent)]
162 Io(#[from] std::io::Error),
163 #[error(transparent)]
165 Http(#[from] reqwest::Error),
166 #[error("not logged in: run logbrew login")]
168 MissingToken,
169 #[error("api returned status {status}: {body}")]
171 Api {
172 status: u16,
174 body: String,
176 auth_source: &'static str,
178 auth_label: &'static str,
180 },
181 #[error("LogBrew API unreachable: {message}")]
183 StatusUnavailable {
184 api_url: String,
186 status_code: Option<u16>,
188 body: Option<String>,
190 authenticated: bool,
192 auth_source: &'static str,
194 auth_label: &'static str,
196 message: String,
198 },
199 #[error("{message}")]
201 Unavailable {
202 message: &'static str,
204 next: &'static str,
206 },
207 #[error("issue investigation returned an invalid response")]
209 InvestigationResponseInvalid,
210 #[error("native debug artifact is invalid")]
212 NativeDebugArtifactInvalid,
213 #[error("native debug-artifact response is invalid")]
215 NativeDebugResponseInvalid,
216 #[error("native debug-artifact verification failed")]
218 NativeDebugVerificationFailed,
219}
220
221pub fn write_cli_error<W: std::io::Write>(
227 error: &CliError,
228 json: bool,
229 output: &mut W,
230) -> Result<(), std::io::Error> {
231 if json {
232 let body = serde_json::json!({
233 "ok": false,
234 "error": cli_error_code(error),
235 "message": error.to_string(),
236 "next": cli_error_next_step(error),
237 });
238 writeln!(output, "{body}")
239 } else {
240 writeln!(output, "{error}")?;
241 writeln!(output, "Next: {}", cli_error_next_step(error))
242 }
243}
244
245pub fn write_runtime_error<W: std::io::Write>(
251 error: &RuntimeError,
252 json: bool,
253 output: &mut W,
254) -> Result<(), std::io::Error> {
255 if json {
256 let body = runtime_error_json(error);
257 writeln!(output, "{body}")
258 } else {
259 write_human_runtime_error(error, output)
260 }
261}
262
263pub fn write_native_debug_runtime_error<W: std::io::Write>(
269 error: &RuntimeError,
270 output: &mut W,
271) -> Result<(), std::io::Error> {
272 let body = match error {
273 RuntimeError::Api { status, .. } => {
274 let (code, next) = native_debug_api_recovery(*status);
275 serde_json::json!({
276 "ok": false,
277 "error": code,
278 "status": status,
279 "next": next,
280 })
281 }
282 RuntimeError::Cli(_)
283 | RuntimeError::Io(_)
284 | RuntimeError::Http(_)
285 | RuntimeError::MissingToken
286 | RuntimeError::StatusUnavailable { .. }
287 | RuntimeError::Unavailable { .. }
288 | RuntimeError::InvestigationResponseInvalid
289 | RuntimeError::NativeDebugArtifactInvalid
290 | RuntimeError::NativeDebugResponseInvalid
291 | RuntimeError::NativeDebugVerificationFailed => serde_json::json!({
292 "ok": false,
293 "error": runtime_error_code(error),
294 "next": fallback_runtime_error_next_step(error),
295 }),
296 };
297 writeln!(output, "{body}")
298}
299
300const fn native_debug_api_recovery(status: u16) -> (&'static str, &'static str) {
302 match status {
303 400 => (
304 "validation_failed",
305 "check the artifact identity and request scope, then retry",
306 ),
307 401 | 403 => (
308 "unauthorized",
309 "sign in and retry the native debug-artifact command",
310 ),
311 404 => (
312 "not_found",
313 "check the exact project, release, environment, service, UUID, and architecture",
314 ),
315 413 => (
316 "payload_too_large",
317 "reduce the native debug-artifact upload below the documented size limits and retry",
318 ),
319 422 => (
320 "validation_failed",
321 "send manifest and debug_file_N multipart parts from LogBrew Apple release tooling",
322 ),
323 429 => (
324 "rate_limited",
325 "retry the same native debug-artifact command later",
326 ),
327 500..=599 => (
328 "server_error",
329 "retry the same native debug-artifact command later",
330 ),
331 _ => (
332 "unexpected_response",
333 "retry the native debug-artifact command",
334 ),
335 }
336}
337
338fn write_human_runtime_error<W: std::io::Write>(
340 error: &RuntimeError,
341 output: &mut W,
342) -> Result<(), std::io::Error> {
343 match error {
344 RuntimeError::StatusUnavailable {
345 api_url,
346 status_code,
347 body,
348 auth_label,
349 message,
350 ..
351 } => {
352 writeln!(output, "LogBrew API unreachable.")?;
353 writeln!(output, "API: {api_url}")?;
354 writeln!(output, "Auth: {auth_label}")?;
355 if let Some(status_code) = status_code {
356 writeln!(output, "Status: {status_code}")?;
357 }
358 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
359 writeln!(output, "Body: {body}")?;
360 } else {
361 writeln!(output, "Reason: {message}")?;
362 }
363 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
364 Ok(())
365 }
366 RuntimeError::Api {
367 status,
368 body,
369 auth_label,
370 ..
371 } => {
372 let api_details = ApiErrorDetails::parse(body);
373 writeln!(output, "{error}")?;
374 if let Some(code) = api_details.code.as_deref() {
375 writeln!(output, "Code: {code}")?;
376 }
377 writeln!(output, "Auth: {auth_label}")?;
378 writeln!(output, "Next: {}", api_next_step(*status, &api_details))
379 }
380 RuntimeError::Cli(_)
381 | RuntimeError::Io(_)
382 | RuntimeError::Http(_)
383 | RuntimeError::MissingToken
384 | RuntimeError::InvestigationResponseInvalid
385 | RuntimeError::NativeDebugArtifactInvalid
386 | RuntimeError::NativeDebugResponseInvalid
387 | RuntimeError::NativeDebugVerificationFailed
388 | RuntimeError::Unavailable { .. } => {
389 writeln!(output, "{error}")?;
390 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
391 Ok(())
392 }
393 }
394}
395
396fn runtime_error_json(error: &RuntimeError) -> serde_json::Value {
398 match error {
399 RuntimeError::MissingToken
400 | RuntimeError::Unavailable { .. }
401 | RuntimeError::InvestigationResponseInvalid
402 | RuntimeError::NativeDebugArtifactInvalid
403 | RuntimeError::NativeDebugResponseInvalid
404 | RuntimeError::NativeDebugVerificationFailed
405 | RuntimeError::Io(_)
406 | RuntimeError::Http(_) => serde_json::json!({
407 "ok": false,
408 "error": runtime_error_code(error),
409 "message": error.to_string(),
410 "next": runtime_error_next_step(error),
411 }),
412 RuntimeError::Api {
413 status,
414 body,
415 auth_source,
416 ..
417 } => {
418 let api_details = ApiErrorDetails::parse(body);
419 let next = api_next_step(*status, &api_details);
420 serde_json::json!({
421 "ok": false,
422 "error": runtime_error_code(error),
423 "message": error.to_string(),
424 "status": status,
425 "body": body,
426 "api_error": api_details.error.as_deref(),
427 "api_code": api_details.code.as_deref(),
428 "api_next": api_details.next.as_deref(),
429 "auth_source": auth_source,
430 "next": next,
431 })
432 }
433 RuntimeError::StatusUnavailable {
434 api_url,
435 status_code,
436 body,
437 authenticated,
438 auth_source,
439 message,
440 ..
441 } => serde_json::json!({
442 "ok": false,
443 "error": runtime_error_code(error),
444 "status": "unreachable",
445 "status_code": status_code,
446 "body": body,
447 "api_url": api_url,
448 "authenticated": authenticated,
449 "auth_source": auth_source,
450 "message": message,
451 "next": runtime_error_next_step(error),
452 }),
453 RuntimeError::Cli(error) => serde_json::json!({
454 "ok": false,
455 "error": cli_error_code(error),
456 "message": error.to_string(),
457 "next": cli_error_next_step(error),
458 }),
459 }
460}
461
462const fn cli_error_code(error: &CliError) -> &'static str {
464 match error {
465 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
466 CliError::MissingArgument { .. } => "missing_argument",
467 CliError::MissingFlagValue { .. } => "missing_flag_value",
468 CliError::DuplicateFlag { .. } => "duplicate_flag",
469 CliError::UnexpectedArgument { .. } => "unexpected_argument",
470 CliError::UnknownFlag { .. } => "unknown_flag",
471 CliError::UnsupportedFlag { .. } => "unsupported_flag",
472 CliError::UnknownResource { .. } => "unknown_resource",
473 CliError::UnknownStatus(_) => "unknown_status",
474 CliError::UnknownTraceStatus(_) => "unknown_trace_status",
475 CliError::UnknownLogLevel(_) => "unknown_log_level",
476 CliError::InvalidLimit(_) => "invalid_limit",
477 CliError::InvalidMinDuration(_) => "invalid_min_duration",
478 CliError::UnknownPagination => "unknown_pagination",
479 CliError::InvalidActionCursor(_) => "invalid_action_cursor",
480 CliError::InvalidLogCursor(_) => "invalid_log_cursor",
481 CliError::InvalidIssueCursor(_) => "invalid_issue_cursor",
482 CliError::InvalidSupportCursor(_) => "invalid_support_cursor",
483 CliError::UnknownSupportCategory => "unknown_support_category",
484 CliError::InvalidSupportTicketId => "invalid_support_ticket_id",
485 CliError::InvalidSupportRetryKey => "invalid_support_retry_key",
486 CliError::InvalidSupportContextReply => "invalid_support_context_reply",
487 CliError::InvalidSupportContextCommand => "invalid_support_context_command",
488 CliError::InvalidSupportContext => "invalid_support_context",
489 CliError::InvalidInvestigationCommand => "invalid_investigation_command",
490 CliError::InvalidDoctorCommand => "invalid_doctor_command",
491 CliError::InvalidProjectCreateCommand => "invalid_project_create_command",
492 CliError::InvalidProjectsCommand => "invalid_projects_command",
493 CliError::InvalidUsageCommand => "invalid_usage_command",
494 CliError::InvalidNativeDebugCommand | CliError::InvalidNativeDebugIdentity => {
495 "invalid_native_debug_command"
496 }
497 CliError::InvalidSetupSource(_) => "invalid_setup_source",
498 }
499}
500
501const fn cli_error_next_step(error: &CliError) -> &'static str {
503 match error {
504 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
505 CliError::InvalidMinDuration(_) => "use --min-duration-ms with a non-negative whole number",
506 CliError::UnknownPagination
507 | CliError::InvalidActionCursor(_)
508 | CliError::InvalidLogCursor(_)
509 | CliError::InvalidIssueCursor(_)
510 | CliError::InvalidSupportCursor(_) => {
511 "use --pagination cursor alone for the first page, then use --cursor-time and --cursor-id together from next_cursor"
512 }
513 CliError::UnknownSupportCategory => {
514 "use sdk_install_failure, ingest_failure, auth_failure, project_setup, dashboard_issue, docs_confusion, cli_issue, mobile_issue, billing_question, or other"
515 }
516 CliError::InvalidSupportTicketId => {
517 "use the ticket_id returned by logbrew support create or list"
518 }
519 CliError::InvalidSupportRetryKey => {
520 "use --retry-key with 1 to 128 visible ASCII characters and reuse it only for an exact retry"
521 }
522 CliError::InvalidSupportContextReply => {
523 "use support reply <ticket_id> --context <text> --retry-key <key>"
524 }
525 CliError::InvalidSupportContextCommand => {
526 "use support context <ticket_id> with optional --json"
527 }
528 CliError::InvalidSupportContext => {
529 "use --context with 1 to 4000 characters after trimming whitespace"
530 }
531 CliError::InvalidInvestigationCommand => {
532 "use logbrew investigate issue <issue_id> with optional --json"
533 }
534 CliError::InvalidDoctorCommand => {
535 "use logbrew doctor --project <project_id> with optional --json"
536 }
537 CliError::InvalidProjectCreateCommand => {
538 "use logbrew projects create <name> --ingest-key-file <path> with optional --runtime, --environment, --abandon-retry, and --json"
539 }
540 CliError::InvalidProjectsCommand => {
541 "use logbrew projects with optional --json, or logbrew projects --help"
542 }
543 CliError::InvalidUsageCommand => "use logbrew usage with optional --json",
544 CliError::InvalidNativeDebugCommand => {
545 "use logbrew debug-artifacts upload <path> --project <project_id> --release <release> --environment <environment> --service <service> with optional --expect-image-uuid, --dry-run, and --json"
546 }
547 CliError::InvalidNativeDebugIdentity => {
548 "use a UUID in 8-4-4-4-12 form and architecture arm64, arm64e, or x86_64"
549 }
550 CliError::InvalidSetupSource(_) => "use --source api, cli, or sdk",
551 CliError::MissingArgument { next, .. }
552 | CliError::MissingFlagValue { next, .. }
553 | CliError::DuplicateFlag { next, .. }
554 | CliError::UnexpectedArgument { next, .. }
555 | CliError::UnsupportedFlag { next, .. }
556 | CliError::UnknownFlag { next, .. }
557 | CliError::UnknownResource { next, .. }
558 | CliError::UnknownCommandName { next, .. } => next,
559 CliError::UnknownCommand => "run logbrew --help",
560 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
561 CliError::UnknownTraceStatus(_) => "use --status error or --status ok",
562 CliError::UnknownLogLevel(_) => "use one of info, warning, error, critical",
563 }
564}
565
566const fn runtime_error_code(error: &RuntimeError) -> &'static str {
568 match error {
569 RuntimeError::Cli(error) => cli_error_code(error),
570 RuntimeError::Io(_) => "io_error",
571 RuntimeError::Http(_) => "http_error",
572 RuntimeError::MissingToken => "not_logged_in",
573 RuntimeError::Api { .. } => "api_error",
574 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
575 RuntimeError::Unavailable { .. } => "unavailable",
576 RuntimeError::InvestigationResponseInvalid => "investigation_response_invalid",
577 RuntimeError::NativeDebugArtifactInvalid => "native_debug_artifact_invalid",
578 RuntimeError::NativeDebugResponseInvalid => "native_debug_response_invalid",
579 RuntimeError::NativeDebugVerificationFailed => "native_debug_verification_failed",
580 }
581}
582
583const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
585
586#[derive(Debug, Clone, Default, PartialEq, Eq)]
588struct ApiErrorDetails {
589 error: Option<String>,
591 code: Option<String>,
593 next: Option<String>,
595}
596
597impl ApiErrorDetails {
598 fn parse(body: &str) -> Self {
600 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else {
601 return Self::default();
602 };
603
604 Self {
605 error: json_string_field(&value, "error"),
606 code: json_string_field(&value, "code"),
607 next: json_string_field(&value, "next"),
608 }
609 }
610}
611
612fn json_string_field(value: &serde_json::Value, key: &str) -> Option<String> {
614 value
615 .get(key)
616 .and_then(serde_json::Value::as_str)
617 .map(str::trim)
618 .filter(|value| !value.is_empty())
619 .map(ToOwned::to_owned)
620}
621
622fn runtime_error_next_step(error: &RuntimeError) -> Cow<'static, str> {
624 match error {
625 RuntimeError::Api { status, body, .. } => {
626 let api_details = ApiErrorDetails::parse(body);
627 api_next_step(*status, &api_details)
628 }
629 RuntimeError::Cli(_)
630 | RuntimeError::Io(_)
631 | RuntimeError::Http(_)
632 | RuntimeError::MissingToken
633 | RuntimeError::InvestigationResponseInvalid
634 | RuntimeError::NativeDebugArtifactInvalid
635 | RuntimeError::NativeDebugResponseInvalid
636 | RuntimeError::NativeDebugVerificationFailed
637 | RuntimeError::StatusUnavailable { .. }
638 | RuntimeError::Unavailable { .. } => {
639 Cow::Borrowed(fallback_runtime_error_next_step(error))
640 }
641 }
642}
643
644fn api_next_step(status: u16, api_details: &ApiErrorDetails) -> Cow<'static, str> {
646 api_details.next.as_ref().map_or_else(
647 || Cow::Borrowed(fallback_api_next_step(status)),
648 |next| Cow::Owned(next.clone()),
649 )
650}
651
652const fn fallback_api_next_step(status: u16) -> &'static str {
654 match status {
655 401 | 403 => "run logbrew login",
656 400 | 422 => "check command arguments or filters",
657 404 => "check the resource id or filters",
658 429 => "retry later",
659 500..=599 => "check LOGBREW_API_URL or retry later",
660 _ => "check command arguments or retry later",
661 }
662}
663
664const fn fallback_runtime_error_next_step(error: &RuntimeError) -> &'static str {
666 match error {
667 RuntimeError::Cli(error) => cli_error_next_step(error),
668 RuntimeError::MissingToken => "run logbrew login",
669 RuntimeError::Api { status, .. } => fallback_api_next_step(*status),
670 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
671 STATUS_UNAVAILABLE_NEXT_STEP
672 }
673 RuntimeError::Unavailable { next, .. } => next,
674 RuntimeError::InvestigationResponseInvalid => {
675 "retry the issue investigation; if it repeats, report the public response contract"
676 }
677 RuntimeError::NativeDebugArtifactInvalid => {
678 "provide one validated Apple dSYM, ZIP, or Mach-O object matching every --expect-image-uuid value"
679 }
680 RuntimeError::NativeDebugResponseInvalid => {
681 "retry the native debug-artifact request; if it repeats, report the public response contract"
682 }
683 RuntimeError::NativeDebugVerificationFailed => {
684 "retry exact native debug-artifact lookup before using native symbolication"
685 }
686 RuntimeError::Io(_) => "check local files and permissions",
687 }
688}