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
42 changes: 39 additions & 3 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,24 @@ func TestCaptureException(t *testing.T) {
{
Type: "*sentry.customErr",
Value: "wat",
// No Stacktrace, because we can't tell where the error came
// from and because we have a stack trace in the most recent
// error in the chain.
Mechanism: &Mechanism{
Data: map[string]any{
"is_exception_group": true,
"exception_id": 0,
},
},
},
{
Type: "*errors.withStack",
Value: "wat",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 1,
"is_exception_group": true,
"parent_id": 0,
},
},
},
},
},
Expand All @@ -193,11 +203,24 @@ func TestCaptureException(t *testing.T) {
{
Type: "*sentry.customErr",
Value: "wat",
Mechanism: &Mechanism{
Data: map[string]any{
"is_exception_group": true,
"exception_id": 0,
},
},
},
{
Type: "*sentry.customErrWithCause",
Value: "err",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 1,
"is_exception_group": true,
"parent_id": 0,
},
},
},
},
},
Expand All @@ -208,11 +231,24 @@ func TestCaptureException(t *testing.T) {
{
Type: "*errors.errorString",
Value: "original",
Mechanism: &Mechanism{
Data: map[string]any{
"is_exception_group": true,
"exception_id": 0,
},
},
},
{
Type: "sentry.wrappedError",
Value: "wrapped: original",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 1,
"is_exception_group": true,
"parent_id": 0,
},
},
},
},
},
Expand Down
64 changes: 49 additions & 15 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 @@ -222,11 +223,12 @@ func NewRequest(r *http.Request) *Request {

// Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct {
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Handled *bool `json:"handled,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Source string `json:"source,omitempty"`
Handled *bool `json:"handled,omitempty"`
Data map[string]any `json:"data,omitempty"`
}

// SetUnhandled indicates that the exception is an unhandled exception, i.e.
Expand Down Expand Up @@ -341,25 +343,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()
ribice marked this conversation as resolved.
Show resolved Hide resolved
}

// Add a trace of the current stack to the most recent error in a chain if
Expand All @@ -370,8 +387,25 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {
e.Exception[0].Stacktrace = NewStacktrace()
}

if len(e.Exception) <= 1 {
return
}

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

for i := range e.Exception {
e.Exception[i].Mechanism = &Mechanism{
Data: map[string]any{
"is_exception_group": true,
"exception_id": i,
},
}
if i == 0 {
continue
}
e.Exception[i].Mechanism.Data["parent_id"] = i - 1
}
}

// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
Expand Down
148 changes: 148 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,153 @@ 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 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{}},
},
},
},
"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",
Mechanism: &Mechanism{
Data: map[string]any{
"is_exception_group": true,
"exception_id": 0,
},
},
},
{
Value: "level 1: base error",
Type: "*fmt.wrapError",
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 1,
"is_exception_group": true,
"parent_id": 0,
},
},
},
{
Value: "level 2: level 1: base error",
Type: "*fmt.wrapError",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 2,
"is_exception_group": true,
"parent_id": 1,
},
},
},
},
},
"Custom error types": {
exception: &customError{
message: "custom error message",
},
maxErrorDepth: 1,
expected: []Exception{
{
Value: "custom error message",
Type: "*sentry.customError",
Stacktrace: &Stacktrace{Frames: []Frame{}},
},
},
},
"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",
Mechanism: &Mechanism{
Data: map[string]any{
"is_exception_group": true,
"exception_id": 0,
},
},
},
{
Value: "error with cause",
Type: "*sentry.withCause",
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 1,
"is_exception_group": true,
"parent_id": 0,
},
},
},
{
Value: "outer error: error with cause",
Type: "*fmt.wrapError",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Data: map[string]any{
"exception_id": 2,
"is_exception_group": true,
"parent_id": 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
Loading