From e51e58a7bddf4f33014bd54d811523229f1d4046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Fri, 8 Mar 2024 18:19:23 +0100 Subject: [PATCH 1/8] Add support for errors.Unwrap() and related changes for RFC 0079 --- client_test.go | 24 +++++---- interfaces.go | 59 +++++++++++++++------ interfaces_test.go | 124 +++++++++++++++++++++++++++++++++++++++++++++ stacktrace_test.go | 2 +- 4 files changed, 183 insertions(+), 26 deletions(-) diff --git a/client_test.go b/client_test.go index ac24361f0..36fff12fa 100644 --- a/client_test.go +++ b/client_test.go @@ -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), }, }, }, @@ -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), }, }, }, @@ -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), }, }, }, diff --git a/interfaces.go b/interfaces.go index c769faa78..b3c0b4b10 100644 --- a/interfaces.go +++ b/interfaces.go @@ -3,6 +3,7 @@ package sentry import ( "context" "encoding/json" + "errors" "fmt" "net" "net/http" @@ -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"` + ExceptionID int `json:"exception_id"` + ParentID *int `json:"parent_id,omitempty"` + Stacktrace *Stacktrace `json:"stacktrace,omitempty"` + Mechanism *Mechanism `json:"mechanism,omitempty"` } // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline @@ -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 @@ -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, diff --git a/interfaces_test.go b/interfaces_test.go index 0feb5b2fe..8adaabbff 100644 --- a/interfaces_test.go +++ b/interfaces_test.go @@ -2,6 +2,7 @@ package sentry import ( "encoding/json" + "errors" "flag" "fmt" "net/http/httptest" @@ -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), + }, + }, + }, + "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", diff --git a/stacktrace_test.go b/stacktrace_test.go index 96d682801..0189293ee 100644 --- a/stacktrace_test.go +++ b/stacktrace_test.go @@ -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",` + From 9b9fb4d8ca8f414738b76f250020e67a25cc7427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Wed, 13 Mar 2024 15:16:11 +0100 Subject: [PATCH 2/8] move exceptionID, parentID and isExceptionGroup to exception.mechanism --- client_test.go | 53 ++++++++++++++++++----------- interfaces.go | 36 +++++++++++--------- interfaces_test.go | 84 +++++++++++++++++++++++++++------------------- stacktrace_test.go | 2 +- util.go | 4 +++ util_test.go | 7 ++++ 6 files changed, 115 insertions(+), 71 deletions(-) diff --git a/client_test.go b/client_test.go index 36fff12fa..6f46afc42 100644 --- a/client_test.go +++ b/client_test.go @@ -162,18 +162,19 @@ func TestCaptureException(t *testing.T) { err: pkgErrors.WithStack(&customErr{}), want: []Exception{ { - 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. + Type: "*sentry.customErr", + Value: "wat", + Mechanism: &Mechanism{IsExceptionGroup: true}, }, { - Type: "*errors.withStack", - Value: "wat", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 1, - ParentID: pointerToInt(0), + Type: "*errors.withStack", + Value: "wat", + Stacktrace: &Stacktrace{Frames: []Frame{}}, + Mechanism: &Mechanism{ + ExceptionID: 1, + IsExceptionGroup: true, + ParentID: pointerToInt(0), + }, }, }, }, @@ -195,13 +196,19 @@ func TestCaptureException(t *testing.T) { { Type: "*sentry.customErr", Value: "wat", + Mechanism: &Mechanism{ + IsExceptionGroup: true, + }, }, { - Type: "*sentry.customErrWithCause", - Value: "err", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 1, - ParentID: pointerToInt(0), + Type: "*sentry.customErrWithCause", + Value: "err", + Stacktrace: &Stacktrace{Frames: []Frame{}}, + Mechanism: &Mechanism{ + ExceptionID: 1, + IsExceptionGroup: true, + ParentID: pointerToInt(0), + }, }, }, }, @@ -212,13 +219,19 @@ func TestCaptureException(t *testing.T) { { Type: "*errors.errorString", Value: "original", + Mechanism: &Mechanism{ + IsExceptionGroup: true, + }, }, { - Type: "sentry.wrappedError", - Value: "wrapped: original", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 1, - ParentID: pointerToInt(0), + Type: "sentry.wrappedError", + Value: "wrapped: original", + Stacktrace: &Stacktrace{Frames: []Frame{}}, + Mechanism: &Mechanism{ + ExceptionID: 1, + IsExceptionGroup: true, + ParentID: pointerToInt(0), + }, }, }, }, diff --git a/interfaces.go b/interfaces.go index b3c0b4b10..a8684bad2 100644 --- a/interfaces.go +++ b/interfaces.go @@ -223,11 +223,15 @@ 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"` + ExceptionID int `json:"exception_id"` + ParentID *int `json:"parent_id,omitempty"` + IsExceptionGroup bool `json:"is_exception_group,omitempty"` + Data map[string]any `json:"data,omitempty"` } // SetUnhandled indicates that the exception is an unhandled exception, i.e. @@ -239,15 +243,12 @@ 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"` - IsExceptionGroup bool `json:"is_exception_group,omitempty"` - ExceptionID int `json:"exception_id"` - ParentID *int `json:"parent_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"` + Stacktrace *Stacktrace `json:"stacktrace,omitempty"` + Mechanism *Mechanism `json:"mechanism,omitempty"` } // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline @@ -393,11 +394,14 @@ func (e *Event) SetException(exception error, maxErrorDepth int) { reverse(e.Exception) for i := range e.Exception { + if len(e.Exception) > 1 { + e.Exception[i].Mechanism = &Mechanism{IsExceptionGroup: true} + } if i == 0 { continue } - e.Exception[i].ExceptionID = i - e.Exception[i].ParentID = &e.Exception[i-1].ExceptionID + e.Exception[i].Mechanism.ExceptionID = i + e.Exception[i].Mechanism.ParentID = Pointer(i - 1) } } diff --git a/interfaces_test.go b/interfaces_test.go index 8adaabbff..77cbeac4a 100644 --- a/interfaces_test.go +++ b/interfaces_test.go @@ -247,10 +247,9 @@ func TestSetException(t *testing.T) { maxErrorDepth: 1, expected: []Exception{ { - Value: "simple error", - Type: "*errors.errorString", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 0, + Value: "simple error", + Type: "*errors.errorString", + Stacktrace: &Stacktrace{Frames: []Frame{}}, }, }, }, @@ -259,22 +258,31 @@ func TestSetException(t *testing.T) { maxErrorDepth: 3, expected: []Exception{ { - Value: "base error", - Type: "*errors.errorString", - ExceptionID: 0, + Value: "base error", + Type: "*errors.errorString", + Mechanism: &Mechanism{ + ExceptionID: 0, + IsExceptionGroup: true, + }, }, { - Value: "level 1: base error", - Type: "*fmt.wrapError", - ExceptionID: 1, - ParentID: pointerToInt(0), + Value: "level 1: base error", + Type: "*fmt.wrapError", + Mechanism: &Mechanism{ + ExceptionID: 1, + IsExceptionGroup: true, + ParentID: pointerToInt(0), + }, }, { - Value: "level 2: level 1: base error", - Type: "*fmt.wrapError", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 2, - ParentID: pointerToInt(1), + Value: "level 2: level 1: base error", + Type: "*fmt.wrapError", + Stacktrace: &Stacktrace{Frames: []Frame{}}, + Mechanism: &Mechanism{ + ExceptionID: 2, + IsExceptionGroup: true, + ParentID: pointerToInt(1), + }, }, }, }, @@ -285,10 +293,9 @@ func TestSetException(t *testing.T) { maxErrorDepth: 1, expected: []Exception{ { - Value: "custom error message", - Type: "*sentry.customError", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 0, + Value: "custom error message", + Type: "*sentry.customError", + Stacktrace: &Stacktrace{Frames: []Frame{}}, }, }, }, @@ -300,22 +307,31 @@ func TestSetException(t *testing.T) { maxErrorDepth: 3, expected: []Exception{ { - Value: "the cause", - Type: "*errors.errorString", - ExceptionID: 0, + Value: "the cause", + Type: "*errors.errorString", + Mechanism: &Mechanism{ + ExceptionID: 0, + IsExceptionGroup: true, + }, }, { - Value: "error with cause", - Type: "*sentry.withCause", - ExceptionID: 1, - ParentID: pointerToInt(0), + Value: "error with cause", + Type: "*sentry.withCause", + Mechanism: &Mechanism{ + ExceptionID: 1, + IsExceptionGroup: true, + ParentID: pointerToInt(0), + }, }, { - Value: "outer error: error with cause", - Type: "*fmt.wrapError", - Stacktrace: &Stacktrace{Frames: []Frame{}}, - ExceptionID: 2, - ParentID: pointerToInt(1), + Value: "outer error: error with cause", + Type: "*fmt.wrapError", + Stacktrace: &Stacktrace{Frames: []Frame{}}, + Mechanism: &Mechanism{ + ExceptionID: 2, + IsExceptionGroup: true, + ParentID: pointerToInt(1), + }, }, }, }, @@ -355,7 +371,7 @@ func TestMechanismMarshalJSON(t *testing.T) { t.Fatal(err) } - want := `{"type":"some type","description":"some description","help_link":"some help link",` + + want := `{"type":"some type","description":"some description","help_link":"some help link","exception_id":0,` + `"data":{"some data":"some value","some numeric data":12345}}` if diff := cmp.Diff(want, string(got)); diff != "" { @@ -381,7 +397,7 @@ func TestMechanismMarshalJSON_withHandled(t *testing.T) { } want := `{"type":"some type","description":"some description","help_link":"some help link",` + - `"handled":false,"data":{"some data":"some value","some numeric data":12345}}` + `"handled":false,"exception_id":0,"data":{"some data":"some value","some numeric data":12345}}` if diff := cmp.Diff(want, string(got)); diff != "" { t.Errorf("Event mismatch (-want +got):\n%s", diff) diff --git a/stacktrace_test.go b/stacktrace_test.go index 0189293ee..96d682801 100644 --- a/stacktrace_test.go +++ b/stacktrace_test.go @@ -200,7 +200,7 @@ func TestEventWithExceptionStacktraceMarshalJSON(t *testing.T) { } want := `{"sdk":{},"user":{},` + - `"exception":[{"exception_id":0,"stacktrace":{"frames":[` + + `"exception":[{"stacktrace":{"frames":[` + `{"function":"gofunc",` + `"symbol":"gosym",` + `"module":"gopkg/gopath",` + diff --git a/util.go b/util.go index 2d2ee4ca1..8f999ed7b 100644 --- a/util.go +++ b/util.go @@ -112,3 +112,7 @@ func revisionFromBuildInfo(info *debug.BuildInfo) string { return "" } + +func Pointer[T any](v T) *T { + return &v +} diff --git a/util_test.go b/util_test.go index e6a5568fb..d2f1ffa7e 100644 --- a/util_test.go +++ b/util_test.go @@ -91,3 +91,10 @@ func TestRevisionFromBuildInfoNoVcsInformation(t *testing.T) { assertEqual(t, revisionFromBuildInfo(info), "") } + +func PointerTest(t *testing.T) { + x := new(int) + *x = 10 + y := Pointer(10) + assertEqual(t, *x, *y) +} From 9620fd490bfb4215f55a9d703c956c7bbc50ad29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Wed, 13 Mar 2024 15:18:05 +0100 Subject: [PATCH 3/8] remove pointerToInt in tests --- client_test.go | 6 +++--- interfaces_test.go | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/client_test.go b/client_test.go index 6f46afc42..4ce25a0bb 100644 --- a/client_test.go +++ b/client_test.go @@ -173,7 +173,7 @@ func TestCaptureException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 1, IsExceptionGroup: true, - ParentID: pointerToInt(0), + ParentID: Pointer(0), }, }, }, @@ -207,7 +207,7 @@ func TestCaptureException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 1, IsExceptionGroup: true, - ParentID: pointerToInt(0), + ParentID: Pointer(0), }, }, }, @@ -230,7 +230,7 @@ func TestCaptureException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 1, IsExceptionGroup: true, - ParentID: pointerToInt(0), + ParentID: Pointer(0), }, }, }, diff --git a/interfaces_test.go b/interfaces_test.go index 77cbeac4a..1c5e6ee90 100644 --- a/interfaces_test.go +++ b/interfaces_test.go @@ -232,10 +232,6 @@ 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 @@ -271,7 +267,7 @@ func TestSetException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 1, IsExceptionGroup: true, - ParentID: pointerToInt(0), + ParentID: Pointer(0), }, }, { @@ -281,7 +277,7 @@ func TestSetException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 2, IsExceptionGroup: true, - ParentID: pointerToInt(1), + ParentID: Pointer(1), }, }, }, @@ -320,7 +316,7 @@ func TestSetException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 1, IsExceptionGroup: true, - ParentID: pointerToInt(0), + ParentID: Pointer(0), }, }, { @@ -330,7 +326,7 @@ func TestSetException(t *testing.T) { Mechanism: &Mechanism{ ExceptionID: 2, IsExceptionGroup: true, - ParentID: pointerToInt(1), + ParentID: Pointer(1), }, }, }, From 3cd3394ed4884c95117445ed75b25e86740861d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Wed, 13 Mar 2024 19:44:01 +0100 Subject: [PATCH 4/8] move parent_id, exception_id and is_exception_group to mechanism.data --- client_test.go | 45 +++++++++++++++++++++++++++++-------------- interfaces.go | 29 +++++++++++++++------------- interfaces_test.go | 48 +++++++++++++++++++++++++++++----------------- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/client_test.go b/client_test.go index 4ce25a0bb..6a36c1018 100644 --- a/client_test.go +++ b/client_test.go @@ -162,18 +162,25 @@ func TestCaptureException(t *testing.T) { err: pkgErrors.WithStack(&customErr{}), want: []Exception{ { - Type: "*sentry.customErr", - Value: "wat", - Mechanism: &Mechanism{IsExceptionGroup: true}, + Type: "*sentry.customErr", + Value: "wat", + Mechanism: &Mechanism{ + Data: map[string]any{ + "is_exception_group": true, + "exception_id": 0, + }, + }, }, { Type: "*errors.withStack", Value: "wat", Stacktrace: &Stacktrace{Frames: []Frame{}}, Mechanism: &Mechanism{ - ExceptionID: 1, - IsExceptionGroup: true, - ParentID: Pointer(0), + Data: map[string]any{ + "exception_id": 1, + "is_exception_group": true, + "parent_id": 0, + }, }, }, }, @@ -197,7 +204,10 @@ func TestCaptureException(t *testing.T) { Type: "*sentry.customErr", Value: "wat", Mechanism: &Mechanism{ - IsExceptionGroup: true, + Data: map[string]any{ + "is_exception_group": true, + "exception_id": 0, + }, }, }, { @@ -205,9 +215,11 @@ func TestCaptureException(t *testing.T) { Value: "err", Stacktrace: &Stacktrace{Frames: []Frame{}}, Mechanism: &Mechanism{ - ExceptionID: 1, - IsExceptionGroup: true, - ParentID: Pointer(0), + Data: map[string]any{ + "exception_id": 1, + "is_exception_group": true, + "parent_id": 0, + }, }, }, }, @@ -220,7 +232,10 @@ func TestCaptureException(t *testing.T) { Type: "*errors.errorString", Value: "original", Mechanism: &Mechanism{ - IsExceptionGroup: true, + Data: map[string]any{ + "is_exception_group": true, + "exception_id": 0, + }, }, }, { @@ -228,9 +243,11 @@ func TestCaptureException(t *testing.T) { Value: "wrapped: original", Stacktrace: &Stacktrace{Frames: []Frame{}}, Mechanism: &Mechanism{ - ExceptionID: 1, - IsExceptionGroup: true, - ParentID: Pointer(0), + Data: map[string]any{ + "exception_id": 1, + "is_exception_group": true, + "parent_id": 0, + }, }, }, }, diff --git a/interfaces.go b/interfaces.go index a8684bad2..17c7679fa 100644 --- a/interfaces.go +++ b/interfaces.go @@ -223,15 +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"` - Source string `json:"source,omitempty"` - Handled *bool `json:"handled,omitempty"` - ExceptionID int `json:"exception_id"` - ParentID *int `json:"parent_id,omitempty"` - IsExceptionGroup bool `json:"is_exception_group,omitempty"` - Data map[string]any `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. @@ -390,18 +387,24 @@ 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 { - if len(e.Exception) > 1 { - e.Exception[i].Mechanism = &Mechanism{IsExceptionGroup: true} + e.Exception[i].Mechanism = &Mechanism{ + Data: map[string]any{ + "is_exception_group": true, + "exception_id": i, + }, } if i == 0 { continue } - e.Exception[i].Mechanism.ExceptionID = i - e.Exception[i].Mechanism.ParentID = Pointer(i - 1) + e.Exception[i].Mechanism.Data["parent_id"] = i - 1 } } diff --git a/interfaces_test.go b/interfaces_test.go index 1c5e6ee90..74bf797c8 100644 --- a/interfaces_test.go +++ b/interfaces_test.go @@ -257,17 +257,21 @@ func TestSetException(t *testing.T) { Value: "base error", Type: "*errors.errorString", Mechanism: &Mechanism{ - ExceptionID: 0, - IsExceptionGroup: true, + Data: map[string]any{ + "is_exception_group": true, + "exception_id": 0, + }, }, }, { Value: "level 1: base error", Type: "*fmt.wrapError", Mechanism: &Mechanism{ - ExceptionID: 1, - IsExceptionGroup: true, - ParentID: Pointer(0), + Data: map[string]any{ + "exception_id": 1, + "is_exception_group": true, + "parent_id": 0, + }, }, }, { @@ -275,9 +279,11 @@ func TestSetException(t *testing.T) { Type: "*fmt.wrapError", Stacktrace: &Stacktrace{Frames: []Frame{}}, Mechanism: &Mechanism{ - ExceptionID: 2, - IsExceptionGroup: true, - ParentID: Pointer(1), + Data: map[string]any{ + "exception_id": 2, + "is_exception_group": true, + "parent_id": 1, + }, }, }, }, @@ -306,17 +312,21 @@ func TestSetException(t *testing.T) { Value: "the cause", Type: "*errors.errorString", Mechanism: &Mechanism{ - ExceptionID: 0, - IsExceptionGroup: true, + Data: map[string]any{ + "is_exception_group": true, + "exception_id": 0, + }, }, }, { Value: "error with cause", Type: "*sentry.withCause", Mechanism: &Mechanism{ - ExceptionID: 1, - IsExceptionGroup: true, - ParentID: Pointer(0), + Data: map[string]any{ + "exception_id": 1, + "is_exception_group": true, + "parent_id": 0, + }, }, }, { @@ -324,9 +334,11 @@ func TestSetException(t *testing.T) { Type: "*fmt.wrapError", Stacktrace: &Stacktrace{Frames: []Frame{}}, Mechanism: &Mechanism{ - ExceptionID: 2, - IsExceptionGroup: true, - ParentID: Pointer(1), + Data: map[string]any{ + "exception_id": 2, + "is_exception_group": true, + "parent_id": 1, + }, }, }, }, @@ -367,7 +379,7 @@ func TestMechanismMarshalJSON(t *testing.T) { t.Fatal(err) } - want := `{"type":"some type","description":"some description","help_link":"some help link","exception_id":0,` + + want := `{"type":"some type","description":"some description","help_link":"some help link",` + `"data":{"some data":"some value","some numeric data":12345}}` if diff := cmp.Diff(want, string(got)); diff != "" { @@ -393,7 +405,7 @@ func TestMechanismMarshalJSON_withHandled(t *testing.T) { } want := `{"type":"some type","description":"some description","help_link":"some help link",` + - `"handled":false,"exception_id":0,"data":{"some data":"some value","some numeric data":12345}}` + `"handled":false,"data":{"some data":"some value","some numeric data":12345}}` if diff := cmp.Diff(want, string(got)); diff != "" { t.Errorf("Event mismatch (-want +got):\n%s", diff) From 83abae9859769abfb88abadf86e8c7e3e1d3247f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Mon, 25 Mar 2024 11:14:31 +0100 Subject: [PATCH 5/8] remove unused Pointer function --- util.go | 4 ---- util_test.go | 7 ------- 2 files changed, 11 deletions(-) diff --git a/util.go b/util.go index 8f999ed7b..2d2ee4ca1 100644 --- a/util.go +++ b/util.go @@ -112,7 +112,3 @@ func revisionFromBuildInfo(info *debug.BuildInfo) string { return "" } - -func Pointer[T any](v T) *T { - return &v -} diff --git a/util_test.go b/util_test.go index d2f1ffa7e..e6a5568fb 100644 --- a/util_test.go +++ b/util_test.go @@ -91,10 +91,3 @@ func TestRevisionFromBuildInfoNoVcsInformation(t *testing.T) { assertEqual(t, revisionFromBuildInfo(info), "") } - -func PointerTest(t *testing.T) { - x := new(int) - *x = 10 - y := Pointer(10) - assertEqual(t, *x, *y) -} From 64c111986059968541e0122269e289b9046629f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Tue, 26 Mar 2024 14:07:36 +0100 Subject: [PATCH 6/8] Update interfaces.go Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- interfaces.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interfaces.go b/interfaces.go index 17c7679fa..d57f753a8 100644 --- a/interfaces.go +++ b/interfaces.go @@ -368,7 +368,7 @@ func (e *Event) SetException(exception error, maxErrorDepth int) { continue } - causer, ok := err.(interface{ Cause() error }) + cause, ok := err.(interface{ Cause() error }) if !ok { // We cannot unwrap the error further. break @@ -376,7 +376,7 @@ func (e *Event) SetException(exception error, maxErrorDepth int) { // The error implements the Cause method, indicating it may have been wrapped // using the github.com/pkg/errors package. - err = causer.Cause() + err = cause.Cause() } // Add a trace of the current stack to the most recent error in a chain if From 23b75974030bdcb73e37f984bd14ded0c810652a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Tue, 26 Mar 2024 17:44:20 +0100 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df584c281..c2071c3a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Automatic transactions for Echo integration ([#722](https://github.com/getsentry/sentry-go/pull/722)) - 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)) +- Add support for errors.Unwrap() when unwrapping exceptions and new exception mechanism fields from [RFC-0079](https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#new-mechanism-fields) ([#792](https://github.com/getsentry/sentry-go/pull/792)) ## 0.27.0 From cc8fda775efe8c775a5ccf975051b75d2fa7a5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Tue, 26 Mar 2024 18:34:05 +0100 Subject: [PATCH 8/8] Update CHANGELOG.md Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12cd0c13d..45d27a4e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +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)) -- Add support for errors.Unwrap() when unwrapping exceptions and new exception mechanism fields from [RFC-0079](https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#new-mechanism-fields) ([#792](https://github.com/getsentry/sentry-go/pull/792)) +- Use `errors.Unwrap()` to create exception groups ([#792](https://github.com/getsentry/sentry-go/pull/792)) ## 0.27.0