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  }