Skip to content

Commit

Permalink
Add support for errors.Unwrap() in SetException (#792)
Browse files Browse the repository at this point in the history
* Add support for errors.Unwrap() and related changes for RFC 0079

---------

Co-authored-by: Ivan Dlugos <[email protected]>
  • Loading branch information
ribice and vaind authored Mar 26, 2024
1 parent 08cdc59 commit 7b5b621
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Add `http.request.method` attribute for performance span data ([#786](https://github.com/getsentry/sentry-go/pull/786))
- Automatic transactions for Fasthttp integration ([#732](https://github.com/getsentry/sentry-go/pull/723))
- Add `Fiber` integration ([#795](https://github.com/getsentry/sentry-go/pull/795))
- Use `errors.Unwrap()` to create exception groups ([#792](https://github.com/getsentry/sentry-go/pull/792))

## 0.27.0

Expand Down
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
}

cause, 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 = cause.Cause()
}

// 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

0 comments on commit 7b5b621

Please sign in to comment.