Skip to content

Commit

Permalink
Release v1.8.0
Browse files Browse the repository at this point in the history
  • Loading branch information
kattrali committed Dec 3, 2020
2 parents ae90a17 + 8d6e27d commit 946ab52
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 21 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## 1.8.0 (2020-12-03)

### Enhancements

* Support unwrapping the underlying causes from an error, including attached
stack trace contents if available.

Any reported error which implements the following interface:

```go
type errorWithCause interface {
Unwrap() error
}
```

will have the cause included as a previous error in the resulting event. The
cause information will be available on the Bugsnag dashboard and is available
for inspection in callbacks on the `errors.Error` object.

```go
bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error {
if event.Error.Cause != nil {
fmt.Printf("This error was caused by %v", event.Error.Cause.Error())
}
return nil
})
```

## 1.7.0 (2020-11-18)

### Enhancements
Expand Down
2 changes: 1 addition & 1 deletion bugsnag.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

// VERSION defines the version of this Bugsnag notifier
const VERSION = "1.7.0"
const VERSION = "1.8.0"

var panicHandlerOnce sync.Once
var sessionTrackerOnce sync.Once
Expand Down
40 changes: 40 additions & 0 deletions errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var MaxStackDepth = 50
// wherever the builtin error interface is expected.
type Error struct {
Err error
Cause *Error
stack []uintptr
frames []StackFrame
}
Expand All @@ -39,6 +40,10 @@ type errorWithStack interface {
Error() string
}

type errorWithCause interface {
Unwrap() error
}

// New makes an Error from the given value. If that value is already an
// error then it will be used directly, if not, it will be passed to
// fmt.Errorf("%v"). The skip parameter indicates how far up the stack
Expand All @@ -53,6 +58,7 @@ func New(e interface{}, skip int) *Error {
return &Error{
Err: e,
stack: e.Callers(),
Cause: unwrapCause(e),
}
case errorWithStack:
trace := e.StackTrace()
Expand All @@ -62,6 +68,7 @@ func New(e interface{}, skip int) *Error {
}
return &Error{
Err: e,
Cause: unwrapCause(e),
stack: stack,
}
case ErrorWithStackFrames:
Expand All @@ -71,6 +78,7 @@ func New(e interface{}, skip int) *Error {
}
return &Error{
Err: e,
Cause: unwrapCause(e),
stack: stack,
frames: e.StackFrames(),
}
Expand All @@ -84,6 +92,7 @@ func New(e interface{}, skip int) *Error {
length := runtime.Callers(2+skip, stack[:])
return &Error{
Err: err,
Cause: unwrapCause(err),
stack: stack[:length],
}
}
Expand Down Expand Up @@ -153,3 +162,34 @@ func (err *Error) TypeName() string {
}
return "error"
}

func unwrapCause(err interface{}) *Error {
if causer, ok := err.(errorWithCause); ok {
cause := causer.Unwrap()
if cause == nil {
return nil
} else if hasStack(cause) { // avoid generating a (duplicate) stack from the current frame
return New(cause, 0)
} else {
return &Error{
Err: cause,
Cause: unwrapCause(cause),
stack: []uintptr{},
}
}
}
return nil
}

func hasStack(err error) bool {
if _, ok := err.(errorWithStack); ok {
return true
}
if _, ok := err.(ErrorWithStackFrames); ok {
return true
}
if _, ok := err.(ErrorWithCallers); ok {
return true
}
return false
}
110 changes: 110 additions & 0 deletions errors/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,116 @@ func TestUnwrapPkgError(t *testing.T) {
assertStacksMatch(t, expected, unwrapped.StackFrames())
}

type customErr struct {
msg string
cause error
callers []uintptr
}

func newCustomErr(msg string, cause error) error {
callers := make([]uintptr, 8)
runtime.Callers(2, callers)
return customErr{
msg: msg,
cause: cause,
callers: callers,
}
}

func (err customErr) Error() string {
return err.msg
}

func (err customErr) Unwrap() error {
return err.cause
}

func (err customErr) Callers() []uintptr {
return err.callers
}

func TestUnwrapCustomCause(t *testing.T) {
_, _, line, ok := runtime.Caller(0) // grab line immediately before error generators
err1 := fmt.Errorf("invalid token")
err2 := newCustomErr("login failed", err1)
err3 := newCustomErr("terminate process", err2)
unwrapped := New(err3, 0)
if !ok {
t.Fatalf("Something has gone wrong with loading the current stack")
}
if unwrapped.Error() != "terminate process" {
t.Errorf("Failed to unwrap error: %s", unwrapped.Error())
}
if unwrapped.Cause == nil {
t.Fatalf("Failed to capture cause error")
}
assertStacksMatch(t, []StackFrame{
StackFrame{Name: "TestUnwrapCustomCause", File: "errors/error_test.go", LineNumber: line + 3},
}, unwrapped.StackFrames())
if unwrapped.Cause.Error() != "login failed" {
t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error())
}
if unwrapped.Cause.Cause == nil {
t.Fatalf("Failed to capture nested cause error")
}
assertStacksMatch(t, []StackFrame{
StackFrame{Name: "TestUnwrapCustomCause", File: "errors/error_test.go", LineNumber: line + 2},
}, unwrapped.Cause.StackFrames())
if unwrapped.Cause.Cause.Error() != "invalid token" {
t.Errorf("Failed to unwrap nested cause error: %s", unwrapped.Cause.Cause.Error())
}
if len(unwrapped.Cause.Cause.StackFrames()) > 0 {
t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames())
}
if unwrapped.Cause.Cause.Cause != nil {
t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause)
}
}

func TestUnwrapErrorsCause(t *testing.T) {
if !goVersionSupportsErrorWrapping() {
t.Skip("%w formatter is supported by go1.13+")
}
_, _, line, ok := runtime.Caller(0) // grab line immediately before error generators
err1 := fmt.Errorf("invalid token")
err2 := fmt.Errorf("login failed: %w", err1)
err3 := fmt.Errorf("terminate process: %w", err2)
unwrapped := New(err3, 0)
if !ok {
t.Fatalf("Something has gone wrong with loading the current stack")
}
if unwrapped.Error() != "terminate process: login failed: invalid token" {
t.Errorf("Failed to unwrap error: %s", unwrapped.Error())
}
assertStacksMatch(t, []StackFrame{
StackFrame{Name: "TestUnwrapErrorsCause", File: "errors/error_test.go", LineNumber: line + 4},
}, unwrapped.StackFrames())
if unwrapped.Cause == nil {
t.Fatalf("Failed to capture cause error")
}
if unwrapped.Cause.Error() != "login failed: invalid token" {
t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error())
}
if len(unwrapped.Cause.StackFrames()) > 0 {
t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.StackFrames())
}
if unwrapped.Cause.Cause == nil {
t.Fatalf("Failed to capture nested cause error")
}
if len(unwrapped.Cause.Cause.StackFrames()) > 0 {
t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames())
}
if unwrapped.Cause.Cause.Cause != nil {
t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause)
}
}

func goVersionSupportsErrorWrapping() bool {
err1 := fmt.Errorf("inner error")
err2 := fmt.Errorf("outer error: %w", err1)
return err2.Error() == "outer error: inner error"
}

func ExampleErrorf() {
for i := 1; i <= 2; i++ {
if i%2 == 1 {
Expand Down
25 changes: 16 additions & 9 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ func newEvent(rawData []interface{}, notifier *Notifier) (*Event, *Configuration
}
}

event.Stacktrace = generateStacktrace(err, config)

for _, callback := range callbacks {
callback(event)
if event.Severity != event.handledState.OriginalSeverity {
event.handledState.SeverityReason = SeverityReasonCallbackSpecified
}
}

return event, config
}

func generateStacktrace(err *errors.Error, config *Configuration) []StackFrame {
stack := make([]StackFrame, len(err.StackFrames()))
for i, frame := range err.StackFrames() {
file := frame.File
inProject := config.isProjectPackage(frame.Package)
Expand All @@ -191,22 +205,15 @@ func newEvent(rawData []interface{}, notifier *Notifier) (*Event, *Configuration
file = config.stripProjectPackages(file)
}

event.Stacktrace[i] = StackFrame{
stack[i] = StackFrame{
Method: frame.Name,
File: file,
LineNumber: frame.LineNumber,
InProject: inProject,
}
}

for _, callback := range callbacks {
callback(event)
if event.Severity != event.handledState.OriginalSeverity {
event.handledState.SeverityReason = SeverityReasonCallbackSpecified
}
}

return event, config
return stack
}

func populateEventWithContext(ctx context.Context, event *Event) {
Expand Down
71 changes: 71 additions & 0 deletions features/fixtures/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"os"
"runtime"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -102,6 +103,8 @@ func main() {
multipleUnhandled()
case "make unhandled with callback":
handledToUnhandled()
case "nested error":
nestedHandledError()
default:
log.Println("Not a valid test flag: " + *test)
os.Exit(1)
Expand Down Expand Up @@ -256,3 +259,71 @@ func handledToUnhandled() {
// Give some time for the error to be sent before exiting
time.Sleep(200 * time.Millisecond)
}

type customErr struct {
msg string
cause error
callers []uintptr
}

func newCustomErr(msg string, cause error) error {
callers := make([]uintptr, 8)
runtime.Callers(2, callers)
return customErr {
msg: msg,
cause: cause,
callers: callers,
}
}

func (err customErr) Error() string {
return err.msg
}

func (err customErr) Unwrap() error {
return err.cause
}

func (err customErr) Callers() []uintptr {
return err.callers
}

func nestedHandledError() {
if err := login("token " + os.Getenv("API_KEY")); err != nil {
bugsnag.Notify(newCustomErr("terminate process", err))
// Give some time for the error to be sent before exiting
time.Sleep(200 * time.Millisecond)
} else {
i := len(os.Getenv("API_KEY"))
// Some nonsense to avoid inlining checkValue
if val, err := checkValue(i); err != nil {
fmt.Printf("err: %v, val: %d", err, val)
}
if val, err := checkValue(i-46); err != nil {
fmt.Printf("err: %v, val: %d", err, val)
}

log.Fatalf("This test is broken - no error was generated.")
}
}

func login(token string) error {
val, err := checkValue(len(token) * -1)
if err != nil {
return newCustomErr("login failed", err)
}
fmt.Printf("val: %d", val)
return nil
}

func checkValue(i int) (int, error) {
if i < 0 {
return 0, newCustomErr("invalid token", nil)
} else if i % 2 == 0 {
return i / 2, nil
} else if i < 9 {
return i * 3, nil
}

return i * 4, nil
}

0 comments on commit 946ab52

Please sign in to comment.