Skip to content

Commit 99cebd5

Browse files
authored
feat: properly support error capturing (#1075)
1 parent 49d29a6 commit 99cebd5

File tree

10 files changed

+713
-116
lines changed

10 files changed

+713
-116
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@
22

33
## 0.36.0
44

5+
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.0.
6+
57
### Breaking Changes
68

79
- Behavioral change for the `MaxBreadcrumbs` client option. Removed the hard limit of 100 breadcrumbs, allowing users to set a larger limit and also changed the default limit from 30 to 100 ([#1106](https://github.com/getsentry/sentry-go/pull/1106)))
810

11+
- The changes to error handling ([#1075](https://github.com/getsentry/sentry-go/pull/1075)) will affect issue grouping. It is expected that any wrapped and complex errors will be grouped under a new issue group.
12+
13+
### Features
14+
15+
- Add support for improved issue grouping with enhanced error chain handling ([#1075](https://github.com/getsentry/sentry-go/pull/1075))
16+
17+
The SDK now provides better handling of complex error scenarios, particularly when dealing with multiple related errors or error chains. This feature automatically detects and properly structures errors created with Go's `errors.Join()` function and other multi-error patterns.
18+
19+
```go
20+
// Multiple errors are now properly grouped and displayed in Sentry
21+
err1 := errors.New("err1")
22+
err2 := errors.New("err2")
23+
combinedErr := errors.Join(err1, err2)
24+
25+
// When captured, these will be shown as related exceptions in Sentry
26+
sentry.CaptureException(combinedErr)
27+
```
28+
929
## 0.35.3
1030

1131
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.3.

client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const (
2828
// is of little use when debugging production errors with Sentry. The Sentry UI
2929
// is not optimized for long chains either. The top-level error together with a
3030
// stack trace is often the most useful information.
31-
maxErrorDepth = 10
31+
maxErrorDepth = 100
3232

3333
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
3434
// meant to bound memory usage and prevent too large transaction events that

client_test.go

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -164,23 +164,27 @@ func TestCaptureException(t *testing.T) {
164164
err: pkgErrors.WithStack(&customErr{}),
165165
want: []Exception{
166166
{
167-
Type: "*sentry.customErr",
168-
Value: "wat",
167+
Type: "*sentry.customErr",
168+
Value: "wat",
169+
Stacktrace: nil,
169170
Mechanism: &Mechanism{
170-
Type: "generic",
171-
ExceptionID: 0,
172-
IsExceptionGroup: true,
171+
Type: MechanismTypeChained,
172+
ExceptionID: 1,
173+
ParentID: Pointer(0),
174+
Source: MechanismTypeUnwrap,
175+
IsExceptionGroup: false,
173176
},
174177
},
175178
{
176179
Type: "*errors.withStack",
177180
Value: "wat",
178181
Stacktrace: &Stacktrace{Frames: []Frame{}},
179182
Mechanism: &Mechanism{
180-
Type: "generic",
181-
ExceptionID: 1,
182-
ParentID: Pointer(0),
183-
IsExceptionGroup: true,
183+
Type: MechanismTypeGeneric,
184+
ExceptionID: 0,
185+
ParentID: nil,
186+
Source: "",
187+
IsExceptionGroup: false,
184188
},
185189
},
186190
},
@@ -201,23 +205,27 @@ func TestCaptureException(t *testing.T) {
201205
err: &customErrWithCause{cause: &customErr{}},
202206
want: []Exception{
203207
{
204-
Type: "*sentry.customErr",
205-
Value: "wat",
208+
Type: "*sentry.customErr",
209+
Value: "wat",
210+
Stacktrace: nil,
206211
Mechanism: &Mechanism{
207-
Type: "generic",
208-
ExceptionID: 0,
209-
IsExceptionGroup: true,
212+
Type: MechanismTypeChained,
213+
ExceptionID: 1,
214+
ParentID: Pointer(0),
215+
Source: "cause",
216+
IsExceptionGroup: false,
210217
},
211218
},
212219
{
213220
Type: "*sentry.customErrWithCause",
214221
Value: "err",
215222
Stacktrace: &Stacktrace{Frames: []Frame{}},
216223
Mechanism: &Mechanism{
217-
Type: "generic",
218-
ExceptionID: 1,
219-
ParentID: Pointer(0),
220-
IsExceptionGroup: true,
224+
Type: MechanismTypeGeneric,
225+
ExceptionID: 0,
226+
ParentID: nil,
227+
Source: "",
228+
IsExceptionGroup: false,
221229
},
222230
},
223231
},
@@ -227,23 +235,27 @@ func TestCaptureException(t *testing.T) {
227235
err: wrappedError{original: errors.New("original")},
228236
want: []Exception{
229237
{
230-
Type: "*errors.errorString",
231-
Value: "original",
238+
Type: "*errors.errorString",
239+
Value: "original",
240+
Stacktrace: nil,
232241
Mechanism: &Mechanism{
233-
Type: "generic",
234-
ExceptionID: 0,
235-
IsExceptionGroup: true,
242+
Type: MechanismTypeChained,
243+
ExceptionID: 1,
244+
ParentID: Pointer(0),
245+
Source: MechanismTypeUnwrap,
246+
IsExceptionGroup: false,
236247
},
237248
},
238249
{
239250
Type: "sentry.wrappedError",
240251
Value: "wrapped: original",
241252
Stacktrace: &Stacktrace{Frames: []Frame{}},
242253
Mechanism: &Mechanism{
243-
Type: "generic",
244-
ExceptionID: 1,
245-
ParentID: Pointer(0),
246-
IsExceptionGroup: true,
254+
Type: MechanismTypeGeneric,
255+
ExceptionID: 0,
256+
ParentID: nil,
257+
Source: "",
258+
IsExceptionGroup: false,
247259
},
248260
},
249261
},

exception.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package sentry
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"slices"
7+
)
8+
9+
const (
10+
MechanismTypeGeneric string = "generic"
11+
MechanismTypeChained string = "chained"
12+
MechanismTypeUnwrap string = "unwrap"
13+
MechanismSourceCause string = "cause"
14+
)
15+
16+
func convertErrorToExceptions(err error, maxErrorDepth int) []Exception {
17+
var exceptions []Exception
18+
visited := make(map[error]bool)
19+
convertErrorDFS(err, &exceptions, nil, "", visited, maxErrorDepth, 0)
20+
21+
// mechanism type is used for debugging purposes, but since we can't really distinguish the origin of who invoked
22+
// captureException, we set it to nil if the error is not chained.
23+
if len(exceptions) == 1 {
24+
exceptions[0].Mechanism = nil
25+
}
26+
27+
slices.Reverse(exceptions)
28+
29+
// Add a trace of the current stack to the top level(outermost) error in a chain if
30+
// it doesn't have a stack trace yet.
31+
// We only add to the most recent error to avoid duplication and because the
32+
// current stack is most likely unrelated to errors deeper in the chain.
33+
if len(exceptions) > 0 && exceptions[len(exceptions)-1].Stacktrace == nil {
34+
exceptions[len(exceptions)-1].Stacktrace = NewStacktrace()
35+
}
36+
37+
return exceptions
38+
}
39+
40+
func convertErrorDFS(err error, exceptions *[]Exception, parentID *int, source string, visited map[error]bool, maxErrorDepth int, currentDepth int) {
41+
if err == nil {
42+
return
43+
}
44+
45+
if visited[err] {
46+
return
47+
}
48+
visited[err] = true
49+
50+
_, isExceptionGroup := err.(interface{ Unwrap() []error })
51+
52+
exception := Exception{
53+
Value: err.Error(),
54+
Type: reflect.TypeOf(err).String(),
55+
Stacktrace: ExtractStacktrace(err),
56+
}
57+
58+
currentID := len(*exceptions)
59+
60+
var mechanismType string
61+
62+
if parentID == nil {
63+
mechanismType = MechanismTypeGeneric
64+
source = ""
65+
} else {
66+
mechanismType = MechanismTypeChained
67+
}
68+
69+
exception.Mechanism = &Mechanism{
70+
Type: mechanismType,
71+
ExceptionID: currentID,
72+
ParentID: parentID,
73+
Source: source,
74+
IsExceptionGroup: isExceptionGroup,
75+
}
76+
77+
*exceptions = append(*exceptions, exception)
78+
79+
if maxErrorDepth >= 0 && currentDepth >= maxErrorDepth {
80+
return
81+
}
82+
83+
switch v := err.(type) {
84+
case interface{ Unwrap() []error }:
85+
unwrapped := v.Unwrap()
86+
for i := range unwrapped {
87+
if unwrapped[i] != nil {
88+
childSource := fmt.Sprintf("errors[%d]", i)
89+
convertErrorDFS(unwrapped[i], exceptions, &currentID, childSource, visited, maxErrorDepth, currentDepth+1)
90+
}
91+
}
92+
case interface{ Unwrap() error }:
93+
unwrapped := v.Unwrap()
94+
if unwrapped != nil {
95+
convertErrorDFS(unwrapped, exceptions, &currentID, MechanismTypeUnwrap, visited, maxErrorDepth, currentDepth+1)
96+
}
97+
case interface{ Cause() error }:
98+
cause := v.Cause()
99+
if cause != nil {
100+
convertErrorDFS(cause, exceptions, &currentID, MechanismSourceCause, visited, maxErrorDepth, currentDepth+1)
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)