Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for errors.Unwrap() in SetException #792

Merged
merged 12 commits into from
Mar 26, 2024
24 changes: 15 additions & 9 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,11 @@ func TestCaptureException(t *testing.T) {
// error in the chain.
},
{
Type: "*errors.withStack",
Value: "wat",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Type: "*errors.withStack",
Value: "wat",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 1,
ParentID: pointerToInt(0),
ribice marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
Expand All @@ -195,9 +197,11 @@ func TestCaptureException(t *testing.T) {
Value: "wat",
},
{
Type: "*sentry.customErrWithCause",
Value: "err",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Type: "*sentry.customErrWithCause",
Value: "err",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 1,
ParentID: pointerToInt(0),
},
},
},
Expand All @@ -210,9 +214,11 @@ func TestCaptureException(t *testing.T) {
Value: "original",
},
{
Type: "sentry.wrappedError",
Value: "wrapped: original",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Type: "sentry.wrappedError",
Value: "wrapped: original",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 1,
ParentID: pointerToInt(0),
},
},
},
Expand Down
59 changes: 43 additions & 16 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sentry
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
Expand Down Expand Up @@ -238,12 +239,15 @@ func (m *Mechanism) SetUnhandled() {

// Exception specifies an error that occurred.
type Exception struct {
Type string `json:"type,omitempty"` // used as the main issue title
Value string `json:"value,omitempty"` // used as the main issue subtitle
Module string `json:"module,omitempty"`
ThreadID uint64 `json:"thread_id,omitempty"`
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
Mechanism *Mechanism `json:"mechanism,omitempty"`
Type string `json:"type,omitempty"` // used as the main issue title
Value string `json:"value,omitempty"` // used as the main issue subtitle
Module string `json:"module,omitempty"`
ThreadID uint64 `json:"thread_id,omitempty"`
IsExceptionGroup bool `json:"is_exception_group,omitempty"`
ribice marked this conversation as resolved.
Show resolved Hide resolved
ExceptionID int `json:"exception_id"`
ParentID *int `json:"parent_id,omitempty"`
ribice marked this conversation as resolved.
Show resolved Hide resolved
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
Mechanism *Mechanism `json:"mechanism,omitempty"`
ribice marked this conversation as resolved.
Show resolved Hide resolved
}

// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
Expand Down Expand Up @@ -341,25 +345,40 @@ type Event struct {
// maxErrorDepth is the maximum depth of the error chain we will look
// into while unwrapping the errors.
func (e *Event) SetException(exception error, maxErrorDepth int) {
err := exception
if err == nil {
if exception == nil {
return
}

for i := 0; i < maxErrorDepth && err != nil; i++ {
err := exception

for i := 0; err != nil && i < maxErrorDepth; i++ {
// Add the current error to the exception slice with its details
e.Exception = append(e.Exception, Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
})
switch previous := err.(type) {
case interface{ Unwrap() error }:
err = previous.Unwrap()
case interface{ Cause() error }:
err = previous.Cause()
default:
err = nil

// Attempt to unwrap the error using the standard library's Unwrap method.
// If errors.Unwrap returns nil, it means either there is no error to unwrap,
// or the error does not implement the Unwrap method.
unwrappedErr := errors.Unwrap(err)

if unwrappedErr != nil {
// The error was successfully unwrapped using the standard library's Unwrap method.
err = unwrappedErr
continue
}

causer, ok := err.(interface{ Cause() error })
if !ok {
// We cannot unwrap the error further.
break
}

// The error implements the Cause method, indicating it may have been wrapped
// using the github.com/pkg/errors package.
err = causer.Cause()
}

// Add a trace of the current stack to the most recent error in a chain if
Expand All @@ -372,6 +391,14 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {

// event.Exception should be sorted such that the most recent error is last.
reverse(e.Exception)

for i := range e.Exception {
if i == 0 {
continue
}
e.Exception[i].ExceptionID = i
e.Exception[i].ParentID = &e.Exception[i-1].ExceptionID
}
}

// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
Expand Down
124 changes: 124 additions & 0 deletions interfaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sentry

import (
"encoding/json"
"errors"
"flag"
"fmt"
"net/http/httptest"
Expand Down Expand Up @@ -215,6 +216,129 @@ func TestEventWithDebugMetaMarshalJSON(t *testing.T) {
}
}

type withCause struct {
msg string
cause error
}

func (w *withCause) Error() string { return w.msg }
func (w *withCause) Cause() error { return w.cause }

type customError struct {
message string
}

func (e *customError) Error() string {
return e.message
}

func pointerToInt(i int) *int {
return &i
}

func TestSetException(t *testing.T) {
testCases := map[string]struct {
exception error
maxErrorDepth int
expected []Exception
}{
"Single error without unwrap": {
exception: errors.New("simple error"),
maxErrorDepth: 1,
expected: []Exception{
{
Value: "simple error",
Type: "*errors.errorString",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 0,
},
},
},
"Nested errors with Unwrap": {
exception: fmt.Errorf("level 2: %w", fmt.Errorf("level 1: %w", errors.New("base error"))),
maxErrorDepth: 3,
expected: []Exception{
{
Value: "base error",
Type: "*errors.errorString",
ExceptionID: 0,
},
{
Value: "level 1: base error",
Type: "*fmt.wrapError",
ExceptionID: 1,
ParentID: pointerToInt(0),
},
{
Value: "level 2: level 1: base error",
Type: "*fmt.wrapError",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 2,
ParentID: pointerToInt(1),
},
},
ribice marked this conversation as resolved.
Show resolved Hide resolved
},
"Custom error types": {
exception: &customError{
message: "custom error message",
},
maxErrorDepth: 1,
expected: []Exception{
{
Value: "custom error message",
Type: "*sentry.customError",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 0,
},
},
},
"Combination of Unwrap and Cause": {
exception: fmt.Errorf("outer error: %w", &withCause{
msg: "error with cause",
cause: errors.New("the cause"),
}),
maxErrorDepth: 3,
expected: []Exception{
{
Value: "the cause",
Type: "*errors.errorString",
ExceptionID: 0,
},
{
Value: "error with cause",
Type: "*sentry.withCause",
ExceptionID: 1,
ParentID: pointerToInt(0),
},
{
Value: "outer error: error with cause",
Type: "*fmt.wrapError",
Stacktrace: &Stacktrace{Frames: []Frame{}},
ExceptionID: 2,
ParentID: pointerToInt(1),
},
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
e := &Event{}
e.SetException(tc.exception, tc.maxErrorDepth)

if len(e.Exception) != len(tc.expected) {
t.Fatalf("Expected %d exceptions, got %d", len(tc.expected), len(e.Exception))
}

for i, exp := range tc.expected {
if diff := cmp.Diff(exp, e.Exception[i]); diff != "" {
t.Errorf("Event mismatch (-want +got):\n%s", diff)
}
}
})
}
}

func TestMechanismMarshalJSON(t *testing.T) {
mechanism := &Mechanism{
Type: "some type",
Expand Down
2 changes: 1 addition & 1 deletion stacktrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func TestEventWithExceptionStacktraceMarshalJSON(t *testing.T) {
}

want := `{"sdk":{},"user":{},` +
`"exception":[{"stacktrace":{"frames":[` +
`"exception":[{"exception_id":0,"stacktrace":{"frames":[` +
`{"function":"gofunc",` +
`"symbol":"gosym",` +
`"module":"gopkg/gopath",` +
Expand Down
Loading