Skip to content

Commit

Permalink
feat: Extract stack frames from golang.org/x/xerrors (#262)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhcarvalho authored Jul 20, 2020
1 parent 61e144f commit 713c600
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 4 deletions.
36 changes: 32 additions & 4 deletions stacktrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ func NewStacktrace() *Stacktrace {
func ExtractStacktrace(err error) *Stacktrace {
method := extractReflectedStacktraceMethod(err)

if !method.IsValid() {
return nil
}
var pcs []uintptr

pcs := extractPcs(method)
if method.IsValid() {
pcs = extractPcs(method)
} else {
pcs = extractXErrorsPC(err)
}

if len(pcs) == 0 {
return nil
Expand Down Expand Up @@ -129,6 +131,32 @@ func extractPcs(method reflect.Value) []uintptr {
return pcs
}

// extractXErrorsPC extracts program counters from error values compatible with
// the error types from golang.org/x/xerrors.
//
// It returns nil if err is not compatible with errors from that package or if
// no program counters are stored in err.
func extractXErrorsPC(err error) []uintptr {
// This implementation uses the reflect package to avoid a hard dependency
// on third-party packages.

// We don't know if err matches the expected type. For simplicity, instead
// of trying to account for all possible ways things can go wrong, some
// assumptions are made and if they are violated the code will panic. We
// recover from any panic and ignore it, returning nil.
//nolint: errcheck
defer func() { recover() }()

field := reflect.ValueOf(err).Elem().FieldByName("frame") // type Frame struct{ frames [3]uintptr }
field = field.FieldByName("frames")
field = field.Slice(1, field.Len()) // drop first pc pointing to xerrors.New
pc := make([]uintptr, field.Len())
for i := 0; i < field.Len(); i++ {
pc[i] = uintptr(field.Index(i).Uint())
}
return pc
}

// Frame represents a function call and it's metadata. Frames are associated
// with a Stacktrace.
type Frame struct {
Expand Down
70 changes: 70 additions & 0 deletions stacktrace_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,72 @@ func TestExtractStacktrace(t *testing.T) {
},
},
}},
// FIXME: The tests below are commented out to avoid introducing a
// dependency on golang.org/x/xerrors. We should enable them when we
// move tests with external dependencies to a separate module.
// See https://github.com/getsentry/sentry-go/issues/238.
//
// https://golang.org/x/xerrors
// "xerrors.errorString": {
// func() error {
// err := xerrors.New("xerror")
// errType := reflect.TypeOf(err).String()
// if errType != "*xerrors.errorString" {
// panic("unexpected error type: " + errType)
// }
// return err
// },
// &sentry.Stacktrace{
// Frames: []sentry.Frame{
// {
// Function: "TestExtractStacktrace.func1",
// Module: "github.com/getsentry/sentry-go_test",
// Lineno: 178,
// InApp: true,
// },
// },
// },
// },
// "xerrors.wrapError": {
// func() error {
// err := xerrors.Errorf("new error: %w", xerrors.New("xerror"))
// errType := reflect.TypeOf(err).String()
// if errType != "*xerrors.wrapError" {
// panic("unexpected error type: " + errType)
// }
// return err
// },
// &sentry.Stacktrace{
// Frames: []sentry.Frame{
// {
// Function: "TestExtractStacktrace.func2",
// Module: "github.com/getsentry/sentry-go_test",
// Lineno: 198,
// InApp: true,
// },
// },
// },
// },
// "xerrors.noWrapError": {
// func() error {
// err := xerrors.Errorf("new error: %w", "not an xerror")
// errType := reflect.TypeOf(err).String()
// if errType != "*xerrors.noWrapError" {
// panic("unexpected error type: " + errType)
// }
// return err
// },
// &sentry.Stacktrace{
// Frames: []sentry.Frame{
// {
// Function: "TestExtractStacktrace.func3",
// Module: "github.com/getsentry/sentry-go_test",
// Lineno: 218,
// InApp: true,
// },
// },
// },
// },
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -192,6 +258,10 @@ func TestExtractStacktrace(t *testing.T) {
func compareStacktrace(t *testing.T, got, want *sentry.Stacktrace) {
t.Helper()

if got == nil {
t.Fatal("got nil stack trace")
}

if len(got.Frames) == 0 {
t.Fatal("got no frames")
}
Expand Down
10 changes: 10 additions & 0 deletions stacktrace_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sentry

import (
"errors"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -159,3 +160,12 @@ func TestFilterFrames(t *testing.T) {
})
}
}

func TestExtractXErrorsPC(t *testing.T) {
// This ensures that extractXErrorsPC does not break code that doesn't use
// golang.org/x/xerrors. For tests that check that it works on the
// appropriate type of errors, see stacktrace_external_test.go.
if got := extractXErrorsPC(errors.New("test")); got != nil {
t.Errorf("got %#v, want nil", got)
}
}

0 comments on commit 713c600

Please sign in to comment.