Skip to content

Commit

Permalink
feat: allow errors to provide their own stack traces
Browse files Browse the repository at this point in the history
  • Loading branch information
subzero10 authored Apr 25, 2024
2 parents b6d7a95 + 9ab217d commit b8d3e83
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 4 deletions.
22 changes: 18 additions & 4 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package honeybadger

import (
"errors"
"fmt"
"reflect"
"runtime"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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()
Expand Down
51 changes: 51 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package honeybadger

import (
"fmt"
"runtime"
"strings"
"testing"
)
Expand Down Expand Up @@ -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)
}
}
}

0 comments on commit b8d3e83

Please sign in to comment.