diff --git a/error.go b/error.go index 502417d..cda346f 100644 --- a/error.go +++ b/error.go @@ -1,6 +1,7 @@ package honeybadger import ( + "errors" "fmt" "reflect" "runtime" @@ -32,6 +33,10 @@ func (e Error) Error() string { return e.Message } +type stacked interface { + Callers() []uintptr +} + func NewError(msg interface{}) Error { return newError(msg, 2) } @@ -52,16 +57,25 @@ func newError(thing interface{}, stackOffset int) Error { err: err, Message: err.Error(), Class: reflect.TypeOf(err).String(), - Stack: generateStack(stackOffset), + Stack: generateStack(autostack(err, stackOffset)), } } -func generateStack(offset int) []*Frame { +func autostack(err error, offset int) []uintptr { + var s stacked + + if errors.As(err, &s) { + return s.Callers() + } + stack := make([]uintptr, maxFrames) length := runtime.Callers(2+offset, stack[:]) + return stack[:length] +} - frames := runtime.CallersFrames(stack[:length]) - result := make([]*Frame, 0, length) +func generateStack(stack []uintptr) []*Frame { + frames := runtime.CallersFrames(stack) + result := make([]*Frame, 0, len(stack)) for { frame, more := frames.Next() diff --git a/error_test.go b/error_test.go index 19d9f46..b65395b 100644 --- a/error_test.go +++ b/error_test.go @@ -1,6 +1,8 @@ package honeybadger import ( + "fmt" + "runtime" "strings" "testing" ) @@ -40,3 +42,52 @@ func TestNewErrorTrace(t *testing.T) { } } } + +type customerror struct { + error + callers []uintptr +} + +func (t customerror) Callers() []uintptr { + return t.callers +} + +func newcustomerror() customerror { + stack := make([]uintptr, maxFrames) + length := runtime.Callers(1, stack[:]) + return customerror{ + error: fmt.Errorf("hello world"), + callers: stack[:length], + } +} + +func TestNewErrorCustomTrace(t *testing.T) { + err := NewError(newcustomerror()) + + // The stack should look like this: + // github.com/honeybadger-io/honeybadger-go.newcustomerror + // github.com/honeybadger-io/honeybadger-go.TestNewErrorCustomTrace + // testing.tRunner + // runtime.goexit + if len(err.Stack) < 3 { + t.Errorf("Expected to generate full trace") + } + + // Checks that the top top methods are the (inlined) fn and the test Method + expected := []string{ + ".newcustomerror", + ".TestNewErrorCustomTrace", + } + + for i, suffix := range expected { + method := err.Stack[i].Method + if !strings.HasSuffix(method, suffix) { + // Logs the stack to give some context about the error + for j, stack := range err.Stack { + t.Logf("%d: %s", j, stack.Method) + } + + t.Fatalf("stack[%d].Method expected_suffix=%q actual=%q", i, suffix, method) + } + } +}