Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
133fcb2
initial support for exception groups
giortzisg Sep 3, 2025
c183ee6
wip changes
giortzisg Sep 3, 2025
00cf3e5
simplify error groups
giortzisg Sep 3, 2025
7ed1116
reverse exception list
giortzisg Sep 3, 2025
4412605
follow natural ordering
giortzisg Sep 3, 2025
590f3f9
fix is_exception_group
giortzisg Sep 4, 2025
263df5f
fix lint
giortzisg Sep 5, 2025
9ee274c
uncap limit for chained errors
giortzisg Sep 10, 2025
1a76769
Merge branch 'master' into feat/issue-grouping
giortzisg Sep 10, 2025
967ed41
handle infinite recursion
giortzisg Sep 10, 2025
1129e87
revert maxErrorDepth changes
giortzisg Sep 10, 2025
a403ec8
misc changes
giortzisg Sep 10, 2025
f608b91
Merge branch 'master' into feat/issue-grouping
giortzisg Sep 10, 2025
9d59402
Merge branch 'master' into feat/issue-grouping
giortzisg Sep 15, 2025
1a95964
skip wrapping error strings
giortzisg Oct 1, 2025
c03564e
add stacktrace on top level
giortzisg Oct 1, 2025
b02379c
Merge branch 'master' into feat/issue-grouping
giortzisg Oct 1, 2025
daa7813
fix: correctly add stacktrace to top level
giortzisg Oct 1, 2025
8b2079d
skip on zero elements
giortzisg Oct 1, 2025
e852a0f
correctly add maxErrorDepth
giortzisg Oct 1, 2025
bde0402
fix: lint
giortzisg Oct 1, 2025
36b0914
rename error_group to exception
giortzisg Oct 2, 2025
4b8ab2c
document breaking changes
giortzisg Oct 2, 2025
d4a0726
Merge branch 'master' into feat/issue-grouping
giortzisg Oct 2, 2025
533a87d
fix unwrap source & maxErrorDepth
giortzisg Oct 2, 2025
359870d
Merge branch 'master' into feat/issue-grouping
giortzisg Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 39 additions & 27 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,23 +164,27 @@ func TestCaptureException(t *testing.T) {
err: pkgErrors.WithStack(&customErr{}),
want: []Exception{
{
Type: "*sentry.customErr",
Value: "wat",
Type: "*sentry.customErr",
Value: "wat",
Stacktrace: nil,
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 0,
IsExceptionGroup: true,
Type: MechanismTypeChained,
ExceptionID: 1,
ParentID: Pointer(0),
Source: "cause",
IsExceptionGroup: false,
},
},
{
Type: "*errors.withStack",
Value: "wat",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 1,
ParentID: Pointer(0),
IsExceptionGroup: true,
Type: MechanismTypeGeneric,
ExceptionID: 0,
ParentID: nil,
Source: "",
IsExceptionGroup: false,
},
},
},
Expand All @@ -201,23 +205,27 @@ func TestCaptureException(t *testing.T) {
err: &customErrWithCause{cause: &customErr{}},
want: []Exception{
{
Type: "*sentry.customErr",
Value: "wat",
Type: "*sentry.customErr",
Value: "wat",
Stacktrace: nil,
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 0,
IsExceptionGroup: true,
Type: MechanismTypeChained,
ExceptionID: 1,
ParentID: Pointer(0),
Source: "cause",
IsExceptionGroup: false,
},
},
{
Type: "*sentry.customErrWithCause",
Value: "err",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 1,
ParentID: Pointer(0),
IsExceptionGroup: true,
Type: MechanismTypeGeneric,
ExceptionID: 0,
ParentID: nil,
Source: "",
IsExceptionGroup: false,
},
},
},
Expand All @@ -227,23 +235,27 @@ func TestCaptureException(t *testing.T) {
err: wrappedError{original: errors.New("original")},
want: []Exception{
{
Type: "*errors.errorString",
Value: "original",
Type: "*errors.errorString",
Value: "original",
Stacktrace: nil,
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 0,
IsExceptionGroup: true,
Type: MechanismTypeChained,
ExceptionID: 1,
ParentID: Pointer(0),
Source: "cause",
IsExceptionGroup: false,
},
},
{
Type: "sentry.wrappedError",
Value: "wrapped: original",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 1,
ParentID: Pointer(0),
IsExceptionGroup: true,
Type: MechanismTypeGeneric,
ExceptionID: 0,
ParentID: nil,
Source: "",
IsExceptionGroup: false,
},
},
},
Expand Down
108 changes: 108 additions & 0 deletions error_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package sentry

import (
"fmt"
"reflect"
"slices"
)

const (
MechanismTypeGeneric string = "generic"
MechanismTypeChained string = "chained"

MechanismSourceCause string = "cause"
)

func convertErrorToExceptions(err error, maxErrorDepth int) []Exception {
var exceptions []Exception
visited := make(map[error]bool)
convertErrorDFS(err, &exceptions, nil, "", visited, maxErrorDepth, 0)

// mechanism type is used for debugging purposes, but since we can't really distinguish the origin of who invoked
// captureException, we set it to nil if the error is not chained.
if len(exceptions) == 1 {
exceptions[0].Mechanism = nil
}

slices.Reverse(exceptions)

// Add a trace of the current stack to the top level(outermost) error in a chain if
// it doesn't have a stack trace yet.
// We only add to the most recent error to avoid duplication and because the
// current stack is most likely unrelated to errors deeper in the chain.
if len(exceptions) > 0 && exceptions[len(exceptions)-1].Stacktrace == nil {
exceptions[len(exceptions)-1].Stacktrace = NewStacktrace()
}

return exceptions
}

func convertErrorDFS(err error, exceptions *[]Exception, parentID *int, source string, visited map[error]bool, maxErrorDepth int, currentDepth int) {
if err == nil {
return
}

if visited[err] {
return
}
visited[err] = true

var isExceptionGroup bool

switch err.(type) {

Check failure on line 52 in error_group.go

View workflow job for this annotation

GitHub Actions / Lint

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
case interface{ Unwrap() []error }:
isExceptionGroup = true
}

exception := Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
}

currentID := len(*exceptions)

var mechanismType string

if parentID == nil {
mechanismType = MechanismTypeGeneric
source = ""
} else {
mechanismType = MechanismTypeChained
}

exception.Mechanism = &Mechanism{
Type: mechanismType,
ExceptionID: currentID,
ParentID: parentID,
Source: source,
IsExceptionGroup: isExceptionGroup,
}

*exceptions = append(*exceptions, exception)

if maxErrorDepth >= 0 && currentDepth >= maxErrorDepth {
return
}

switch v := err.(type) {
case interface{ Unwrap() []error }:
unwrapped := v.Unwrap()
for i := range unwrapped {
if unwrapped[i] != nil {
childSource := fmt.Sprintf("errors[%d]", i)
convertErrorDFS(unwrapped[i], exceptions, &currentID, childSource, visited, maxErrorDepth, currentDepth+1)
}
}
case interface{ Unwrap() error }:
unwrapped := v.Unwrap()
if unwrapped != nil {
convertErrorDFS(unwrapped, exceptions, &currentID, MechanismSourceCause, visited, maxErrorDepth, currentDepth+1)
}
case interface{ Cause() error }:
cause := v.Cause()
if cause != nil {
convertErrorDFS(cause, exceptions, &currentID, MechanismSourceCause, visited, maxErrorDepth, currentDepth+1)
}
}
}
Loading
Loading