Skip to content
This repository has been archived by the owner on Aug 30, 2023. It is now read-only.

feat: Support multiple errors packages and fix Go 1.12 tests #242

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go:
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- tip

before_install:
Expand Down Expand Up @@ -37,6 +38,9 @@ matrix:
- name: "golint 1.11.x"
go: 1.11.x
script: ./scripts/lint.sh
- name: "golint 1.12.x"
go: 1.12.x
script: ./scripts/lint.sh
allow_failures:
- go: tip

Expand Down
72 changes: 72 additions & 0 deletions example/error_packages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// +build errors_packages

package main

import (
"fmt"

"github.com/davecgh/go-spew/spew"
raven "github.com/getsentry/raven-go"
goErrors "github.com/go-errors/errors"
pingcapErrors "github.com/pingcap/errors"
pkgErrors "github.com/pkg/errors"
)

//==============================
// https://github.com/pkg/errors
//==============================

func pkgBar() error {
return pkgErrors.New("this is bad from pkgErrors")
}

func pkgFoo() error {
return pkgBar()
}

//==================================
// https://github.com/pingcap/errors
//==================================

func pingcapBar() error {
return pingcapErrors.New("this is bad from pingcapErrors")
}

func pingcapFoo() error {
return pingcapBar()
}

//====================================
// https://github.com/go-errors/errors
//====================================

func goErrorsBar() error {
return goErrors.New(goErrors.Errorf("this is bad from goErrors"))
}

func goErrorsFoo() error {
return goErrorsBar()
}

//==============================

func main() {
pkgErr := pkgFoo()
pkgStacktrace := raven.GetOrNewStacktrace(pkgErr, 0, 3, []string{})
spew.Dump(pkgStacktrace)
spew.Dump(len(pkgStacktrace.Frames))

fmt.Print("\n\n\n")

pingcapErr := pingcapFoo()
pingcapStacktrace := raven.GetOrNewStacktrace(pingcapErr, 0, 3, []string{})
spew.Dump(pingcapStacktrace)
spew.Dump(len(pingcapStacktrace.Frames))

fmt.Print("\n\n\n")

goErrorsErr := goErrorsFoo()
goErrorsStacktrace := raven.GetOrNewStacktrace(goErrorsErr, 0, 3, []string{})
spew.Dump(goErrorsStacktrace)
spew.Dump(len(goErrorsStacktrace.Frames))
}
122 changes: 74 additions & 48 deletions stacktrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"go/build"
"io/ioutil"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -52,35 +53,63 @@ type StacktraceFrame struct {
InApp bool `json:"in_app"`
}

// GetOrNewStacktrace tries to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {
type stackTracer interface {
StackTrace() []runtime.Frame
}
stacktrace, ok := err.(stackTracer)
if !ok {
return NewStacktrace(skip+1, context, appPackagePrefixes)
}
// extractFramesFromPcs translates slice of stack trace pointers into usable frames
func extractFramesFromPcs(pcs []uintptr, context int, appPackagePrefixes []string) []*StacktraceFrame {
var frames []*StacktraceFrame
for f := range stacktrace.StackTrace() {
pc := uintptr(f) - 1
fn := runtime.FuncForPC(pc)
var fName string
var file string
var line int
if fn != nil {
file, line = fn.FileLine(pc)
fName = fn.Name()
} else {
file = "unknown"
fName = "unknown"
}
frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes)
callersFrames := runtime.CallersFrames(pcs)

for {
fr, more := callersFrames.Next()
frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
if frame != nil {
frames = append([]*StacktraceFrame{frame}, frames...)
}
if !more {
break
}
}

return frames
}

// GetOrNewStacktrace tries to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
// Use of reflection allows us to not have a hard dependency on any given package, so we don't have to import it
func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing! Is it possible though to have the Client have a configurable GetStackTrace method so that anyone can pas in the error implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make it happen in the sentry-go

// https://github.com/pkg/errors
// https://github.com/pingcap/errors

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pingcap/errors provides GetStackTracer as the preferred way to get a StackTrace because it will recur through the error chain to find a StackTrace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

methodStackTrace := reflect.ValueOf(err).MethodByName("StackTrace")
// https://github.com/go-errors/errors
methodStackFrames := reflect.ValueOf(err).MethodByName("StackFrames")

if methodStackTrace.IsValid() {
stacktrace := methodStackTrace.Call(make([]reflect.Value, 0))[0]
if stacktrace.Kind() != reflect.Slice {
return NewStacktrace(skip+1, context, appPackagePrefixes)
}

pcs := make([]uintptr, stacktrace.Len())
for i := 0; i < stacktrace.Len(); i++ {
pcs = append(pcs, uintptr(stacktrace.Index(i).Uint()))
}
frames := extractFramesFromPcs(pcs, context, appPackagePrefixes)

return &Stacktrace{frames}
} else if methodStackFrames.IsValid() {
stacktrace := methodStackFrames.Call(make([]reflect.Value, 0))[0]
if stacktrace.Kind() != reflect.Slice {
return NewStacktrace(skip+1, context, appPackagePrefixes)
}

pcs := make([]uintptr, stacktrace.Len())
for i := 0; i < stacktrace.Len(); i++ {
pcs = append(pcs, uintptr(stacktrace.Index(i).FieldByName("ProgramCounter").Uint()))
}
frames := extractFramesFromPcs(pcs, context, appPackagePrefixes)

return &Stacktrace{frames}
}
return &Stacktrace{Frames: frames}

return NewStacktrace(skip+1, context, appPackagePrefixes)
}

// NewStacktrace intializes and populates a new stacktrace, skipping skip frames.
Expand All @@ -92,8 +121,6 @@ func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []s
// appPackagePrefixes is a list of prefixes used to check whether a package should
// be considered "in app".
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
var frames []*StacktraceFrame

callerPcs := make([]uintptr, 100)
numCallers := runtime.Callers(skip+2, callerPcs)

Expand All @@ -102,32 +129,18 @@ func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktra
return nil
}

callersFrames := runtime.CallersFrames(callerPcs)
frames := extractFramesFromPcs(callerPcs, context, appPackagePrefixes)

for {
fr, more := callersFrames.Next()
if fr.Func != nil {
frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
if frame != nil {
frames = append(frames, frame)
}
}
if !more {
break
}
}
// If there are no frames, the entire stacktrace is nil
if len(frames) == 0 {
return nil
}

// Optimize the path where there's only 1 frame
if len(frames) == 1 {
return &Stacktrace{frames}
}
// Sentry wants the frames with the oldest first, so reverse them
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}

return &Stacktrace{frames}
}

Expand All @@ -140,6 +153,14 @@ func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktra
// appPackagePrefixes is a list of prefixes used to check whether a package should
// be considered "in app".
func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
if file == "" {
file = "unknown"
}

if fName == "" {
fName = "unknown"
}

frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line}
frame.Module, frame.Function = functionName(fName)
frame.InApp = isInAppFrame(*frame, appPackagePrefixes)
Expand All @@ -150,6 +171,11 @@ func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPa
return nil
}

// Skip useless frames
if frame.Filename == "unknown" && frame.Function == "unknown" && frame.Lineno == 0 && frame.Colno == 0 {
return nil
}

if context > 0 {
contextLines, lineIdx := sourceCodeLoader.Load(file, line, context)
if len(contextLines) > 0 {
Expand Down Expand Up @@ -179,11 +205,11 @@ func isInAppFrame(frame StacktraceFrame, appPackagePrefixes []string) bool {
if frame.Module == "main" {
return true
}
for _, prefix := range appPackagePrefixes {
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
return true
}
}
for _, prefix := range appPackagePrefixes {
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
return true
}
}
return false
}

Expand Down