github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runbits/errors/errors.go (about) 1 package errors 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/thoas/go-funk" 10 11 "github.com/ActiveState/cli/internal/analytics" 12 anaConst "github.com/ActiveState/cli/internal/analytics/constants" 13 "github.com/ActiveState/cli/internal/analytics/dimensions" 14 "github.com/ActiveState/cli/internal/captain" 15 "github.com/ActiveState/cli/internal/condition" 16 "github.com/ActiveState/cli/internal/constants" 17 "github.com/ActiveState/cli/internal/errs" 18 "github.com/ActiveState/cli/internal/locale" 19 "github.com/ActiveState/cli/internal/logging" 20 "github.com/ActiveState/cli/internal/multilog" 21 "github.com/ActiveState/cli/internal/output" 22 "github.com/ActiveState/cli/internal/rtutils/ptr" 23 ) 24 25 var PanicOnMissingLocale = true 26 27 type ErrorTips interface { 28 ErrorTips() []string 29 } 30 31 type OutputError struct { 32 error 33 } 34 35 func (o *OutputError) MarshalOutput(f output.Format) interface{} { 36 var outLines []string 37 isInputError := locale.IsInputError(o.error) 38 39 // Print what happened 40 if !isInputError && f == output.PlainFormatName { 41 outLines = append(outLines, output.Title(locale.Tl("err_what_happened", "[ERROR]Something Went Wrong[/RESET]")).String()) 42 } 43 44 var userFacingError errs.UserFacingError 45 if errors.As(o.error, &userFacingError) { 46 message := userFacingError.UserError() 47 if f == output.PlainFormatName { 48 outLines = append(outLines, formatMessage(message)...) 49 } else { 50 outLines = append(outLines, message) 51 } 52 } else { 53 rerrs := locale.UnpackError(o.error) 54 if len(rerrs) == 0 { 55 // It's possible the error came from cobra or something else low level that doesn't use localization 56 logging.Warning("Error does not have localization: %s", errs.JoinMessage(o.error)) 57 rerrs = []error{o.error} 58 } 59 for _, errv := range rerrs { 60 message := normalizeError(locale.ErrorMessage(errv)) 61 if f == output.PlainFormatName { 62 outLines = append(outLines, formatMessage(message)...) 63 } else { 64 outLines = append(outLines, message) 65 } 66 } 67 } 68 69 // Concatenate error tips 70 errorTips := getErrorTips(o.error) 71 errorTips = append(errorTips, locale.Tl("err_help_forum", "Ask For Help → [ACTIONABLE]{{.V0}}[/RESET]", constants.ForumsURL)) 72 73 // Print tips 74 enableTips := os.Getenv(constants.DisableErrorTipsEnvVarName) != "true" && f == output.PlainFormatName 75 if enableTips { 76 outLines = append(outLines, "") // separate error from "Need More Help?" header 77 outLines = append(outLines, strings.TrimSpace(output.Title(locale.Tl("err_more_help", "Need More Help?")).String())) 78 for _, tip := range errorTips { 79 outLines = append(outLines, fmt.Sprintf(" [DISABLED]•[/RESET] %s", normalizeError(tip))) 80 } 81 } 82 return strings.Join(outLines, "\n") 83 } 84 85 // formatMessage formats the error message for plain output. It adds a 86 // x prefix to the first line and indents the rest of the lines to match 87 // the indentation of the first line. 88 func formatMessage(message string) []string { 89 var output []string 90 lines := strings.Split(message, "\n") 91 for i, line := range lines { 92 if i == 0 { 93 output = append(output, fmt.Sprintf(" [NOTICE][ERROR]x[/RESET] %s", line)) 94 } else { 95 output = append(output, fmt.Sprintf(" %s", line)) 96 } 97 } 98 99 return output 100 } 101 102 func getErrorTips(err error) []string { 103 errorTips := []string{} 104 for _, err := range errs.Unpack(err) { 105 v, ok := err.(ErrorTips) 106 if !ok { 107 continue 108 } 109 for _, tip := range v.ErrorTips() { 110 if funk.Contains(errorTips, tip) { 111 continue 112 } 113 errorTips = append(errorTips, tip) 114 } 115 } 116 return errorTips 117 } 118 119 func (o *OutputError) MarshalStructured(f output.Format) interface{} { 120 var userFacingError errs.UserFacingError 121 var message string 122 if errors.As(o.error, &userFacingError) { 123 message = userFacingError.UserError() 124 } else { 125 message = locale.JoinedErrorMessage(o.error) 126 } 127 return output.StructuredError{message, getErrorTips(o.error)} 128 } 129 130 // normalizeError ensures the given erorr message ends with a period. 131 func normalizeError(msg string) string { 132 msg = strings.TrimRight(msg, " \r\n") 133 if !strings.HasSuffix(msg, ".") { 134 msg = msg + "." 135 } 136 return msg 137 } 138 139 // ParseUserFacing returns the exit code and a user facing error message. 140 func ParseUserFacing(err error) (int, error) { 141 if err == nil { 142 return 0, nil 143 } 144 145 _, hasMarshaller := err.(output.Marshaller) 146 147 // unwrap exit code before we remove un-localized wrapped errors from err variable 148 code := errs.ParseExitCode(err) 149 150 if errs.IsSilent(err) { 151 logging.Debug("Suppressing silent failure: %v", err.Error()) 152 return code, nil 153 } 154 155 // If the error already has a marshalling function we do not want to wrap 156 // it again in the OutputError type. 157 if hasMarshaller { 158 return code, err 159 } 160 161 return code, &OutputError{err} 162 } 163 164 func ReportError(err error, cmd *captain.Command, an analytics.Dispatcher) { 165 stack := "not provided" 166 var ee errs.Errorable 167 isErrs := errors.As(err, &ee) 168 169 // Get the stack closest to the root as that will most accurately tell us where the error originated 170 for childErr := err; childErr != nil; childErr = errors.Unwrap(childErr) { 171 var ee2 errs.Errorable 172 if errors.As(childErr, &ee2) { 173 stack = ee2.Stack().String() 174 } 175 } 176 177 _, hasMarshaller := err.(output.Marshaller) 178 179 cmdName := cmd.Name() 180 childCmd, findErr := cmd.Find(os.Args[1:]) 181 if findErr != nil { 182 logging.Error("Could not find child command: %v", findErr) 183 } 184 185 var flagNames []string 186 for _, flag := range cmd.ActiveFlags() { 187 flagNames = append(flagNames, fmt.Sprintf("--%s", flag.Name)) 188 } 189 190 label := []string{cmdName} 191 if childCmd != nil { 192 label = append(label, childCmd.JoinedSubCommandNames()) 193 } 194 label = append(label, flagNames...) 195 196 // Log error if this isn't a user input error 197 var action string 198 errorMsg := err.Error() 199 if IsReportableError(err) { 200 multilog.Critical("Returning error:\n%s\nCreated at:\n%s", errs.JoinMessage(err), stack) 201 action = anaConst.ActCommandError 202 } else { 203 logging.Debug("Returning input error:\n%s\nCreated at:\n%s", errs.JoinMessage(err), stack) 204 action = anaConst.ActCommandInputError 205 for _, err := range errs.Unpack(err) { 206 if locale.IsInputErrorNonRecursive(err) { 207 errorMsg = locale.ErrorMessage(err) 208 break 209 } 210 } 211 } 212 213 an.EventWithLabel(anaConst.CatDebug, action, strings.Join(label, " "), &dimensions.Values{ 214 Error: ptr.To(errorMsg), 215 }) 216 217 if (!locale.HasError(err) && !errs.IsUserFacing(err)) && isErrs && !hasMarshaller { 218 multilog.Error("MUST ADDRESS: Error does not have localization: %s", errs.JoinMessage(err)) 219 220 // If this wasn't built via CI then this is a dev workstation, and we should be more aggressive 221 if !condition.BuiltViaCI() && PanicOnMissingLocale { 222 panic(fmt.Sprintf("Errors must be localized! Please localize: %s, called at: %s\n", errs.JoinMessage(err), stack)) 223 } 224 } 225 } 226 227 func IsReportableError(err error) bool { 228 return !locale.IsInputError(err) && !errs.IsExternalError(err) 229 }