Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
12 changes: 5 additions & 7 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ import (
// The identifier of the SDK.
const sdkIdentifier = "sentry.go"

// maxErrorDepth is the maximum number of errors reported in a chain of errors.
// MaxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
const maxErrorDepth = 10
// with the addition of exception groups, it doesn't make sense to keep a
// global limit for chained errors, because they get hidden if inside a group.
const MaxErrorDepth = -1

// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
// meant to bound memory usage and prevent too large transaction events that
Expand Down Expand Up @@ -297,7 +295,7 @@ func NewClient(options ClientOptions) (*Client, error) {
}

if options.MaxErrorDepth == 0 {
options.MaxErrorDepth = maxErrorDepth
options.MaxErrorDepth = MaxErrorDepth
}

if options.MaxSpans == 0 {
Expand Down
61 changes: 35 additions & 26 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestCaptureMessageEmptyString(t *testing.T) {
{
Type: "sentry.usageError",
Value: "CaptureMessage called with empty message",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Stacktrace: nil,
},
},
}
Expand Down Expand Up @@ -141,7 +141,7 @@ func TestCaptureException(t *testing.T) {
{
Type: "sentry.usageError",
Value: "CaptureException called with nil error",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Stacktrace: nil,
},
},
},
Expand All @@ -152,7 +152,7 @@ func TestCaptureException(t *testing.T) {
{
Type: "*errors.errorString",
Value: "custom error",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Stacktrace: nil,
},
},
},
Expand All @@ -167,19 +167,22 @@ func TestCaptureException(t *testing.T) {
Type: "*sentry.customErr",
Value: "wat",
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),
Type: MechanismTypeGeneric,
ExceptionID: 0,
ParentID: nil,
Source: "",
IsExceptionGroup: true,
},
},
Expand All @@ -192,7 +195,7 @@ func TestCaptureException(t *testing.T) {
{
Type: "*sentry.customErrWithCause",
Value: "err",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Stacktrace: nil,
},
},
},
Expand All @@ -204,19 +207,22 @@ func TestCaptureException(t *testing.T) {
Type: "*sentry.customErr",
Value: "wat",
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{}},
Stacktrace: nil,
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 1,
ParentID: Pointer(0),
Type: MechanismTypeGeneric,
ExceptionID: 0,
ParentID: nil,
Source: "",
IsExceptionGroup: true,
},
},
Expand All @@ -230,19 +236,22 @@ func TestCaptureException(t *testing.T) {
Type: "*errors.errorString",
Value: "original",
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{}},
Stacktrace: nil,
Mechanism: &Mechanism{
Type: "generic",
ExceptionID: 1,
ParentID: Pointer(0),
Type: MechanismTypeGeneric,
ExceptionID: 0,
ParentID: nil,
Source: "",
IsExceptionGroup: true,
},
},
Expand Down Expand Up @@ -341,7 +350,7 @@ func TestCaptureEventNil(t *testing.T) {
{
Type: "sentry.usageError",
Value: "CaptureEvent called with nil event",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Stacktrace: nil,
},
},
}
Expand Down Expand Up @@ -779,7 +788,7 @@ func TestRecover(t *testing.T) {
{
Type: "*errors.errorString",
Value: "panic error",
Stacktrace: &Stacktrace{Frames: []Frame{}},
Stacktrace: nil,
},
},
},
Expand Down
97 changes: 97 additions & 0 deletions error_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package sentry

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

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

MechanismSourceCause string = "cause"
)

func convertErrorToExceptions(err error) []Exception {
if err == nil {
return nil
}

var exceptions []Exception
convertErrorDFS(err, &exceptions, nil, "")

if len(exceptions) == 1 {
exceptions[0].Mechanism = nil
}

slices.Reverse(exceptions)

return exceptions
}

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

var isExceptionGroup bool

switch err.(type) {
case interface{ Unwrap() []error }:
isExceptionGroup = true
case interface{ Unwrap() error }:
isExceptionGroup = true
case interface{ Cause() 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)

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)
}
}
case interface{ Unwrap() error }:
unwrapped := v.Unwrap()
if unwrapped != nil {
convertErrorDFS(unwrapped, exceptions, &currentID, MechanismSourceCause)
}
case interface{ Cause() error }:
cause := v.Cause()
if cause != nil && !errors.Is(cause, err) { // Avoid infinite recursion
convertErrorDFS(cause, exceptions, &currentID, MechanismSourceCause)
}
}
}
Loading
Loading