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

Commit 1a29fa7

Browse files
committed
feat: Support multiple errors packages and fix Go 1.12 tests
1 parent 04157b8 commit 1a29fa7

File tree

1 file changed

+74
-48
lines changed

1 file changed

+74
-48
lines changed

stacktrace.go

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go/build"
1111
"io/ioutil"
1212
"path/filepath"
13+
"reflect"
1314
"runtime"
1415
"strings"
1516
"sync"
@@ -52,35 +53,63 @@ type StacktraceFrame struct {
5253
InApp bool `json:"in_app"`
5354
}
5455

55-
// GetOrNewStacktrace tries to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
56-
func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {
57-
type stackTracer interface {
58-
StackTrace() []runtime.Frame
59-
}
60-
stacktrace, ok := err.(stackTracer)
61-
if !ok {
62-
return NewStacktrace(skip+1, context, appPackagePrefixes)
63-
}
56+
// extractFramesFromPcs translates slice of stack trace pointers into usable frames
57+
func extractFramesFromPcs(pcs []uintptr, context int, appPackagePrefixes []string) []*StacktraceFrame {
6458
var frames []*StacktraceFrame
65-
for f := range stacktrace.StackTrace() {
66-
pc := uintptr(f) - 1
67-
fn := runtime.FuncForPC(pc)
68-
var fName string
69-
var file string
70-
var line int
71-
if fn != nil {
72-
file, line = fn.FileLine(pc)
73-
fName = fn.Name()
74-
} else {
75-
file = "unknown"
76-
fName = "unknown"
77-
}
78-
frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes)
59+
callersFrames := runtime.CallersFrames(pcs)
60+
61+
for {
62+
fr, more := callersFrames.Next()
63+
frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
7964
if frame != nil {
8065
frames = append([]*StacktraceFrame{frame}, frames...)
8166
}
67+
if !more {
68+
break
69+
}
70+
}
71+
72+
return frames
73+
}
74+
75+
// GetOrNewStacktrace tries to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
76+
// Use of reflection allows us to not have a hard dependency on any given package, so we don't have to import it
77+
func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {
78+
// https://github.com/pkg/errors
79+
// https://github.com/pingcap/errors
80+
methodStackTrace := reflect.ValueOf(err).MethodByName("StackTrace")
81+
// https://github.com/go-errors/errors
82+
methodStackFrames := reflect.ValueOf(err).MethodByName("StackFrames")
83+
84+
if methodStackTrace.IsValid() {
85+
stacktrace := methodStackTrace.Call(make([]reflect.Value, 0))[0]
86+
if stacktrace.Kind() != reflect.Slice {
87+
return NewStacktrace(skip+1, context, appPackagePrefixes)
88+
}
89+
90+
pcs := make([]uintptr, stacktrace.Len())
91+
for i := 0; i < stacktrace.Len(); i++ {
92+
pcs = append(pcs, uintptr(stacktrace.Index(i).Uint()))
93+
}
94+
frames := extractFramesFromPcs(pcs, context, appPackagePrefixes)
95+
96+
return &Stacktrace{frames}
97+
} else if methodStackFrames.IsValid() {
98+
stacktrace := methodStackFrames.Call(make([]reflect.Value, 0))[0]
99+
if stacktrace.Kind() != reflect.Slice {
100+
return NewStacktrace(skip+1, context, appPackagePrefixes)
101+
}
102+
103+
pcs := make([]uintptr, stacktrace.Len())
104+
for i := 0; i < stacktrace.Len(); i++ {
105+
pcs = append(pcs, uintptr(stacktrace.Index(i).FieldByName("ProgramCounter").Uint()))
106+
}
107+
frames := extractFramesFromPcs(pcs, context, appPackagePrefixes)
108+
109+
return &Stacktrace{frames}
82110
}
83-
return &Stacktrace{Frames: frames}
111+
112+
return NewStacktrace(skip+1, context, appPackagePrefixes)
84113
}
85114

86115
// NewStacktrace intializes and populates a new stacktrace, skipping skip frames.
@@ -92,8 +121,6 @@ func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []s
92121
// appPackagePrefixes is a list of prefixes used to check whether a package should
93122
// be considered "in app".
94123
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
95-
var frames []*StacktraceFrame
96-
97124
callerPcs := make([]uintptr, 100)
98125
numCallers := runtime.Callers(skip+2, callerPcs)
99126

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

105-
callersFrames := runtime.CallersFrames(callerPcs)
132+
frames := extractFramesFromPcs(callerPcs, context, appPackagePrefixes)
106133

107-
for {
108-
fr, more := callersFrames.Next()
109-
if fr.Func != nil {
110-
frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
111-
if frame != nil {
112-
frames = append(frames, frame)
113-
}
114-
}
115-
if !more {
116-
break
117-
}
118-
}
119134
// If there are no frames, the entire stacktrace is nil
120135
if len(frames) == 0 {
121136
return nil
122137
}
138+
123139
// Optimize the path where there's only 1 frame
124140
if len(frames) == 1 {
125141
return &Stacktrace{frames}
126142
}
127-
// Sentry wants the frames with the oldest first, so reverse them
128-
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
129-
frames[i], frames[j] = frames[j], frames[i]
130-
}
143+
131144
return &Stacktrace{frames}
132145
}
133146

@@ -140,6 +153,14 @@ func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktra
140153
// appPackagePrefixes is a list of prefixes used to check whether a package should
141154
// be considered "in app".
142155
func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
156+
if file == "" {
157+
file = "unknown"
158+
}
159+
160+
if fName == "" {
161+
fName = "unknown"
162+
}
163+
143164
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line}
144165
frame.Module, frame.Function = functionName(fName)
145166
frame.InApp = isInAppFrame(*frame, appPackagePrefixes)
@@ -150,6 +171,11 @@ func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPa
150171
return nil
151172
}
152173

174+
// Skip useless frames
175+
if frame.Filename == "unknown" && frame.Function == "unknown" && frame.Lineno == 0 && frame.Colno == 0 {
176+
return nil
177+
}
178+
153179
if context > 0 {
154180
contextLines, lineIdx := sourceCodeLoader.Load(file, line, context)
155181
if len(contextLines) > 0 {
@@ -179,11 +205,11 @@ func isInAppFrame(frame StacktraceFrame, appPackagePrefixes []string) bool {
179205
if frame.Module == "main" {
180206
return true
181207
}
182-
for _, prefix := range appPackagePrefixes {
183-
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
184-
return true
185-
}
186-
}
208+
for _, prefix := range appPackagePrefixes {
209+
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
210+
return true
211+
}
212+
}
187213
return false
188214
}
189215

0 commit comments

Comments
 (0)