From 01fd3c047a706189d85fe1e38188d95431378294 Mon Sep 17 00:00:00 2001 From: Xavier Goffin Date: Thu, 27 Nov 2025 15:39:05 +0100 Subject: [PATCH 1/4] feat(reporter/sentry): filtering funcs to set custom error level on sentry --- reporter/sentry/options.go | 120 ++++++++++++++++++++++++------- reporter/sentry/reporter.go | 52 ++++++++++---- reporter/sentry/reporter_test.go | 94 ++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 39 deletions(-) diff --git a/reporter/sentry/options.go b/reporter/sentry/options.go index a40811b..f2846c4 100644 --- a/reporter/sentry/options.go +++ b/reporter/sentry/options.go @@ -1,38 +1,55 @@ package sentry import ( + "context" + "io" "os" "strings" "time" "github.com/getsentry/sentry-go" + uerrors "github.com/upfluence/errors" + "github.com/upfluence/errors/base" "github.com/upfluence/errors/reporter" ) -var defaultOptions = Options{ - Tags: make(map[string]string), - SentryOptions: sentry.ClientOptions{ - Dsn: os.Getenv("SENTRY_DSN"), - Environment: os.Getenv("ENV"), - SendDefaultPII: true, - }, - TagWhitelist: toStringMap( - []string{reporter.RemoteIP, reporter.RemotePort, reporter.DomainKey}, - ), - Timeout: time.Minute, - TagBlacklist: []func(string) bool{ - stringEqual(reporter.TransactionKey), - stringEqual(reporter.UserEmailKey), - stringEqual(reporter.UserIDKey), - stringEqual(reporter.HTTPRequestProtoKey), - stringEqual(reporter.HTTPRequestPathKey), - stringEqual(reporter.HTTPRequestHostKey), - stringEqual(reporter.HTTPRequestMethodKey), - stringEqual(reporter.HTTPRequestBodyKey), - stringPrefix(reporter.HTTPRequestHeaderKeyPrefix), - stringPrefix(reporter.HTTPRequestQueryValuesKeyPrefix), - }, -} +type ErrorLevelFunc func(error) sentry.Level + +var ( + defaultErrorLevelFuncs = []ErrorLevelFunc{ + ErrorIsFunc(context.DeadlineExceeded, sentry.LevelWarning), + ErrorIsFunc(context.Canceled, sentry.LevelWarning), + ErrorIsFunc(io.EOF, sentry.LevelWarning), + ErrorCauseTextContainsFunc("net/http: TLS handshake timeout", sentry.LevelWarning), + ErrorCauseTextContainsFunc("operation was canceled", sentry.LevelWarning), + ErrorCauseTextContainsFunc("EOF", sentry.LevelWarning), + } + defaultOptions = Options{ + Tags: make(map[string]string), + SentryOptions: sentry.ClientOptions{ + Dsn: os.Getenv("SENTRY_DSN"), + Environment: os.Getenv("ENV"), + SendDefaultPII: true, + }, + TagWhitelist: toStringMap( + []string{reporter.RemoteIP, reporter.RemotePort, reporter.DomainKey}, + ), + Timeout: time.Minute, + TagBlacklist: []func(string) bool{ + stringEqual(reporter.TransactionKey), + stringEqual(reporter.UserEmailKey), + stringEqual(reporter.UserIDKey), + stringEqual(reporter.HTTPRequestProtoKey), + stringEqual(reporter.HTTPRequestPathKey), + stringEqual(reporter.HTTPRequestHostKey), + stringEqual(reporter.HTTPRequestMethodKey), + stringEqual(reporter.HTTPRequestBodyKey), + stringPrefix(reporter.HTTPRequestHeaderKeyPrefix), + stringPrefix(reporter.HTTPRequestQueryValuesKeyPrefix), + }, + ErrorLevelFuncs: defaultErrorLevelFuncs, + } +) func stringEqual(x string) func(string) bool { return func(y string) bool { return x == y } @@ -52,6 +69,44 @@ func toStringMap(vs []string) map[string]struct{} { return res } +// ErrorIsFunc creates an ErrorLevelFunc of the passed level that checks if +// reported errors are the same as the given sentinel error +func ErrorIsFunc(sentinel error, level sentry.Level) ErrorLevelFunc { + return func(err error) sentry.Level { + if uerrors.Is(err, sentinel) { + return level + } + + return "" + } +} + +// ErrorIsOfTypeFunc creates an ErrorLevelFunc of the passed level that checks +// if reported errors are of the passed generic type +func ErrorIsOfTypeFunc[T error](level sentry.Level) ErrorLevelFunc { + return func(err error) sentry.Level { + if uerrors.IsOfType[T](err) { + return level + } + + return "" + } +} + +// ErrorCauseTextContainsFunc an ErrorLevelFunc of the passed level that checks +// if reported errors' cause's Error() text contains the passed string +func ErrorCauseTextContainsFunc(errorText string, level sentry.Level) ErrorLevelFunc { + return func(err error) sentry.Level { + rootCause := base.UnwrapAll(err).Error() + + if strings.Contains(rootCause, errorText) { + return level + } + + return "" + } +} + type Options struct { Tags map[string]string @@ -60,6 +115,8 @@ type Options struct { TagWhitelist map[string]struct{} TagBlacklist []func(string) bool + + ErrorLevelFuncs []ErrorLevelFunc } func (o Options) client() (*sentry.Client, error) { @@ -74,6 +131,7 @@ func (o Options) client() (*sentry.Client, error) { type Option func(*Options) +// WithTags allows for custom tags to be given to the Reporter func WithTags(tags map[string]string) Option { return func(opts *Options) { for k, v := range tags { @@ -81,3 +139,17 @@ func WithTags(tags map[string]string) Option { } } } + +// AppendErrorLevelFuncs adds the passed funcs to the ErrorLevelFuncs of the Reporter +func AppendErrorLevelFuncs(funcs []ErrorLevelFunc) Option { + return func(opts *Options) { + opts.ErrorLevelFuncs = append(opts.ErrorLevelFuncs, funcs...) + } +} + +// ReplaceErrorLevelFuncs replaces the ErrorLevelFuncs of the Reporter with the passed ones +func ReplaceErrorLevelFuncs(funcs []ErrorLevelFunc) Option { + return func(opts *Options) { + opts.ErrorLevelFuncs = funcs + } +} diff --git a/reporter/sentry/reporter.go b/reporter/sentry/reporter.go index edc8579..819586d 100644 --- a/reporter/sentry/reporter.go +++ b/reporter/sentry/reporter.go @@ -28,8 +28,9 @@ import ( type Reporter struct { cl *sentry.Client - tagWhitelist []func(string) bool - tagBlacklist []func(string) bool + tagWhitelist []func(string) bool + tagBlacklist []func(string) bool + errorLevelFuncs []ErrorLevelFunc timeout time.Duration } @@ -56,8 +57,9 @@ func NewReporter(os ...Option) (*Reporter, error) { return ok }, }, - tagBlacklist: opts.TagBlacklist, - timeout: opts.Timeout, + tagBlacklist: opts.TagBlacklist, + timeout: opts.Timeout, + errorLevelFuncs: opts.ErrorLevelFuncs, }, nil } @@ -71,11 +73,13 @@ func (r *Reporter) WhitelistTag(fns ...func(string) bool) { func (r *Reporter) Report(err error, opts reporter.ReportOptions) { evt := r.buildEvent(err, opts) + fmt.Println(evt) + if evt == nil { return } - r.cl.CaptureEvent(evt, nil, nil) + fmt.Println(*r.cl.CaptureEvent(evt, nil, nil)) } // Close flushes pending events to Sentry and releases resources. @@ -121,26 +125,26 @@ func (r *Reporter) buildEvent(err error, opts reporter.ReportOptions) *sentry.Ev return nil } - ts := tags.GetTags(err) + errorTags := tags.GetTags(err) - if ts == nil && len(opts.Tags) > 0 { - ts = make(map[string]interface{}, len(opts.Tags)) + if errorTags == nil && len(opts.Tags) > 0 { + errorTags = make(map[string]interface{}, len(opts.Tags)) } for k, v := range opts.Tags { - if _, ok := ts[k]; !ok { - ts[k] = v + if _, ok := errorTags[k]; !ok { + errorTags[k] = v } } evt := sentry.NewEvent() - evt.Level = sentry.LevelError + evt.Level = r.computeErrorLevel(err) evt.Timestamp = time.Now() evt.Message = err.Error() - evt.Transaction = transactionName(ts) - evt.User = buildUser(ts) - evt.Request = buildRequest(ts) + evt.Transaction = transactionName(errorTags) + evt.User = buildUser(errorTags) + evt.Request = buildRequest(errorTags) cause := base.UnwrapAll(err) @@ -153,7 +157,7 @@ func (r *Reporter) buildEvent(err error, opts reporter.ReportOptions) *sentry.Ev }, } - for k, v := range ts { + for k, v := range errorTags { r.appendTag(k, v, evt) } @@ -164,6 +168,24 @@ func (r *Reporter) buildEvent(err error, opts reporter.ReportOptions) *sentry.Ev return evt } +var validLevels = []sentry.Level{ + sentry.LevelDebug, + sentry.LevelInfo, + sentry.LevelWarning, + sentry.LevelError, + sentry.LevelFatal, +} + +func (r *Reporter) computeErrorLevel(err error) sentry.Level { + for _, errFunc := range r.errorLevelFuncs { + if level := errFunc(err); level != "" { + return level + } + } + + return sentry.LevelError +} + func extractStacktrace(err error, n int) *sentry.Stacktrace { var s sentry.Stacktrace diff --git a/reporter/sentry/reporter_test.go b/reporter/sentry/reporter_test.go index 5b23201..4bbb18a 100644 --- a/reporter/sentry/reporter_test.go +++ b/reporter/sentry/reporter_test.go @@ -1,6 +1,7 @@ package sentry import ( + "context" "io" "regexp" "testing" @@ -13,6 +14,10 @@ import ( "github.com/upfluence/errors/reporter" ) +type mockError struct{} + +func (*mockError) Error() string { return "mock" } + func TestBuildEvent(t *testing.T) { for _, tt := range []struct { name string @@ -163,6 +168,95 @@ func TestBuildEvent(t *testing.T) { assert.Equal(t, evt.Tags, map[string]string{"foo": "bar", "domain": "github.com/upfluence/errors/reporter/sentry"}) }, }, + { + name: "simple sentinel error with severity func", + err: io.EOF, + modifiers: []func(*Reporter){}, + evtfn: func(t *testing.T, evt *sentry.Event) { + assert.Equal( + t, + sentry.LevelWarning, + evt.Level, + ) + }, + }, + { + name: "wrapped sentinel error with severity func", + err: errors.Wrap(context.DeadlineExceeded, "it took too long, boss"), + modifiers: []func(*Reporter){}, + evtfn: func(t *testing.T, evt *sentry.Event) { + assert.Equal( + t, + sentry.LevelWarning, + evt.Level, + ) + }, + }, + { + name: "simple text error with severity func", + err: errors.New("net/http: TLS handshake timeout with extra bells and whistles"), + modifiers: []func(*Reporter){}, + evtfn: func(t *testing.T, evt *sentry.Event) { + assert.Equal( + t, + sentry.LevelWarning, + evt.Level, + ) + }, + }, + { + name: "wrapped text error with severity func", + err: errors.Wrap( + errors.New("net/http: TLS handshake timeout with extra bells and whistles"), + "more bells and whistles", + ), + modifiers: []func(*Reporter){}, + evtfn: func(t *testing.T, evt *sentry.Event) { + assert.Equal( + t, + sentry.LevelWarning, + evt.Level, + ) + }, + }, + { + name: "simple error type", + err: &mockError{}, + modifiers: []func(*Reporter){ + func(r *Reporter) { + r.errorLevelFuncs = append( + r.errorLevelFuncs, + ErrorIsOfTypeFunc[*mockError](sentry.LevelDebug), + ) + }, + }, + evtfn: func(t *testing.T, evt *sentry.Event) { + assert.Equal( + t, + sentry.LevelDebug, + evt.Level, + ) + }, + }, + { + name: "wrapped error type", + err: errors.Wrap(&mockError{}, "i am being mocked"), + modifiers: []func(*Reporter){ + func(r *Reporter) { + r.errorLevelFuncs = append( + r.errorLevelFuncs, + ErrorIsOfTypeFunc[*mockError](sentry.LevelDebug), + ) + }, + }, + evtfn: func(t *testing.T, evt *sentry.Event) { + assert.Equal( + t, + sentry.LevelDebug, + evt.Level, + ) + }, + }, } { t.Run(tt.name, func(t *testing.T) { r, err := NewReporter(tt.opts...) From 4a03d7a6bed0a7cc6320fc871c2f244beb77ed80 Mon Sep 17 00:00:00 2001 From: Xavier Goffin Date: Thu, 27 Nov 2025 15:43:51 +0100 Subject: [PATCH 2/4] reporter/sentry/reporter.go: cleanup dev artifacts --- reporter/sentry/reporter.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/reporter/sentry/reporter.go b/reporter/sentry/reporter.go index 819586d..0c9bb40 100644 --- a/reporter/sentry/reporter.go +++ b/reporter/sentry/reporter.go @@ -73,13 +73,11 @@ func (r *Reporter) WhitelistTag(fns ...func(string) bool) { func (r *Reporter) Report(err error, opts reporter.ReportOptions) { evt := r.buildEvent(err, opts) - fmt.Println(evt) - if evt == nil { return } - fmt.Println(*r.cl.CaptureEvent(evt, nil, nil)) + r.cl.CaptureEvent(evt, nil, nil) } // Close flushes pending events to Sentry and releases resources. @@ -168,14 +166,6 @@ func (r *Reporter) buildEvent(err error, opts reporter.ReportOptions) *sentry.Ev return evt } -var validLevels = []sentry.Level{ - sentry.LevelDebug, - sentry.LevelInfo, - sentry.LevelWarning, - sentry.LevelError, - sentry.LevelFatal, -} - func (r *Reporter) computeErrorLevel(err error) sentry.Level { for _, errFunc := range r.errorLevelFuncs { if level := errFunc(err); level != "" { From a2fea3d1294a3befbde9ac795d2d4c24ae782a16 Mon Sep 17 00:00:00 2001 From: Xavier Goffin Date: Thu, 27 Nov 2025 17:26:40 +0100 Subject: [PATCH 3/4] reporter/sentry/options.go: fix nits --- reporter/sentry/options.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reporter/sentry/options.go b/reporter/sentry/options.go index f2846c4..d50c1ac 100644 --- a/reporter/sentry/options.go +++ b/reporter/sentry/options.go @@ -93,7 +93,7 @@ func ErrorIsOfTypeFunc[T error](level sentry.Level) ErrorLevelFunc { } } -// ErrorCauseTextContainsFunc an ErrorLevelFunc of the passed level that checks +// ErrorCauseTextContainsFunc creates an ErrorLevelFunc of the passed level that checks // if reported errors' cause's Error() text contains the passed string func ErrorCauseTextContainsFunc(errorText string, level sentry.Level) ErrorLevelFunc { return func(err error) sentry.Level { @@ -141,7 +141,7 @@ func WithTags(tags map[string]string) Option { } // AppendErrorLevelFuncs adds the passed funcs to the ErrorLevelFuncs of the Reporter -func AppendErrorLevelFuncs(funcs []ErrorLevelFunc) Option { +func AppendErrorLevelFuncs(funcs ...ErrorLevelFunc) Option { return func(opts *Options) { opts.ErrorLevelFuncs = append(opts.ErrorLevelFuncs, funcs...) } From cdd19d525afabebf3361b80e2cd6fb8696da858e Mon Sep 17 00:00:00 2001 From: Xavier Goffin Date: Fri, 28 Nov 2025 11:27:05 +0100 Subject: [PATCH 4/4] reporter/sentry: rename ErrorLevelFunc to ErrorLevelMapper --- reporter/sentry/options.go | 44 ++++++++++++++++---------------- reporter/sentry/reporter.go | 14 +++++----- reporter/sentry/reporter_test.go | 12 ++++----- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/reporter/sentry/options.go b/reporter/sentry/options.go index d50c1ac..6dafe4f 100644 --- a/reporter/sentry/options.go +++ b/reporter/sentry/options.go @@ -13,16 +13,16 @@ import ( "github.com/upfluence/errors/reporter" ) -type ErrorLevelFunc func(error) sentry.Level +type ErrorLevelMapper func(error) sentry.Level var ( - defaultErrorLevelFuncs = []ErrorLevelFunc{ - ErrorIsFunc(context.DeadlineExceeded, sentry.LevelWarning), - ErrorIsFunc(context.Canceled, sentry.LevelWarning), - ErrorIsFunc(io.EOF, sentry.LevelWarning), - ErrorCauseTextContainsFunc("net/http: TLS handshake timeout", sentry.LevelWarning), - ErrorCauseTextContainsFunc("operation was canceled", sentry.LevelWarning), - ErrorCauseTextContainsFunc("EOF", sentry.LevelWarning), + defaultErrorLevelMappers = []ErrorLevelMapper{ + ErrorIsLevel(context.DeadlineExceeded, sentry.LevelWarning), + ErrorIsLevel(context.Canceled, sentry.LevelWarning), + ErrorIsLevel(io.EOF, sentry.LevelWarning), + ErrorCauseTextContainsLevel("net/http: TLS handshake timeout", sentry.LevelWarning), + ErrorCauseTextContainsLevel("operation was canceled", sentry.LevelWarning), + ErrorCauseTextContainsLevel("EOF", sentry.LevelWarning), } defaultOptions = Options{ Tags: make(map[string]string), @@ -47,7 +47,7 @@ var ( stringPrefix(reporter.HTTPRequestHeaderKeyPrefix), stringPrefix(reporter.HTTPRequestQueryValuesKeyPrefix), }, - ErrorLevelFuncs: defaultErrorLevelFuncs, + ErrorLevelMappers: defaultErrorLevelMappers, } ) @@ -69,9 +69,9 @@ func toStringMap(vs []string) map[string]struct{} { return res } -// ErrorIsFunc creates an ErrorLevelFunc of the passed level that checks if +// ErrorIsLevel creates an ErrorLevelMapper of the passed level that checks if // reported errors are the same as the given sentinel error -func ErrorIsFunc(sentinel error, level sentry.Level) ErrorLevelFunc { +func ErrorIsLevel(sentinel error, level sentry.Level) ErrorLevelMapper { return func(err error) sentry.Level { if uerrors.Is(err, sentinel) { return level @@ -81,9 +81,9 @@ func ErrorIsFunc(sentinel error, level sentry.Level) ErrorLevelFunc { } } -// ErrorIsOfTypeFunc creates an ErrorLevelFunc of the passed level that checks +// ErrorIsOfTypeLevel creates an ErrorLevelMapper of the passed level that checks // if reported errors are of the passed generic type -func ErrorIsOfTypeFunc[T error](level sentry.Level) ErrorLevelFunc { +func ErrorIsOfTypeLevel[T error](level sentry.Level) ErrorLevelMapper { return func(err error) sentry.Level { if uerrors.IsOfType[T](err) { return level @@ -93,9 +93,9 @@ func ErrorIsOfTypeFunc[T error](level sentry.Level) ErrorLevelFunc { } } -// ErrorCauseTextContainsFunc creates an ErrorLevelFunc of the passed level that checks +// ErrorCauseTextContainsLevel creates an ErrorLevelMapper of the passed level that checks // if reported errors' cause's Error() text contains the passed string -func ErrorCauseTextContainsFunc(errorText string, level sentry.Level) ErrorLevelFunc { +func ErrorCauseTextContainsLevel(errorText string, level sentry.Level) ErrorLevelMapper { return func(err error) sentry.Level { rootCause := base.UnwrapAll(err).Error() @@ -116,7 +116,7 @@ type Options struct { TagWhitelist map[string]struct{} TagBlacklist []func(string) bool - ErrorLevelFuncs []ErrorLevelFunc + ErrorLevelMappers []ErrorLevelMapper } func (o Options) client() (*sentry.Client, error) { @@ -140,16 +140,16 @@ func WithTags(tags map[string]string) Option { } } -// AppendErrorLevelFuncs adds the passed funcs to the ErrorLevelFuncs of the Reporter -func AppendErrorLevelFuncs(funcs ...ErrorLevelFunc) Option { +// AppendErrorLevelMappers adds the passed funcs to the ErrorLevelMappers of the Reporter +func AppendErrorLevelMappers(funcs ...ErrorLevelMapper) Option { return func(opts *Options) { - opts.ErrorLevelFuncs = append(opts.ErrorLevelFuncs, funcs...) + opts.ErrorLevelMappers = append(opts.ErrorLevelMappers, funcs...) } } -// ReplaceErrorLevelFuncs replaces the ErrorLevelFuncs of the Reporter with the passed ones -func ReplaceErrorLevelFuncs(funcs []ErrorLevelFunc) Option { +// ReplaceErrorLevelMappers replaces the ErrorLevelMappers of the Reporter with the passed ones +func ReplaceErrorLevelMappers(funcs []ErrorLevelMapper) Option { return func(opts *Options) { - opts.ErrorLevelFuncs = funcs + opts.ErrorLevelMappers = funcs } } diff --git a/reporter/sentry/reporter.go b/reporter/sentry/reporter.go index 0c9bb40..02d671c 100644 --- a/reporter/sentry/reporter.go +++ b/reporter/sentry/reporter.go @@ -28,9 +28,9 @@ import ( type Reporter struct { cl *sentry.Client - tagWhitelist []func(string) bool - tagBlacklist []func(string) bool - errorLevelFuncs []ErrorLevelFunc + tagWhitelist []func(string) bool + tagBlacklist []func(string) bool + errorLevelMappers []ErrorLevelMapper timeout time.Duration } @@ -57,9 +57,9 @@ func NewReporter(os ...Option) (*Reporter, error) { return ok }, }, - tagBlacklist: opts.TagBlacklist, - timeout: opts.Timeout, - errorLevelFuncs: opts.ErrorLevelFuncs, + tagBlacklist: opts.TagBlacklist, + timeout: opts.Timeout, + errorLevelMappers: opts.ErrorLevelMappers, }, nil } @@ -167,7 +167,7 @@ func (r *Reporter) buildEvent(err error, opts reporter.ReportOptions) *sentry.Ev } func (r *Reporter) computeErrorLevel(err error) sentry.Level { - for _, errFunc := range r.errorLevelFuncs { + for _, errFunc := range r.errorLevelMappers { if level := errFunc(err); level != "" { return level } diff --git a/reporter/sentry/reporter_test.go b/reporter/sentry/reporter_test.go index 4bbb18a..2546394 100644 --- a/reporter/sentry/reporter_test.go +++ b/reporter/sentry/reporter_test.go @@ -224,9 +224,9 @@ func TestBuildEvent(t *testing.T) { err: &mockError{}, modifiers: []func(*Reporter){ func(r *Reporter) { - r.errorLevelFuncs = append( - r.errorLevelFuncs, - ErrorIsOfTypeFunc[*mockError](sentry.LevelDebug), + r.errorLevelMappers = append( + r.errorLevelMappers, + ErrorIsOfTypeLevel[*mockError](sentry.LevelDebug), ) }, }, @@ -243,9 +243,9 @@ func TestBuildEvent(t *testing.T) { err: errors.Wrap(&mockError{}, "i am being mocked"), modifiers: []func(*Reporter){ func(r *Reporter) { - r.errorLevelFuncs = append( - r.errorLevelFuncs, - ErrorIsOfTypeFunc[*mockError](sentry.LevelDebug), + r.errorLevelMappers = append( + r.errorLevelMappers, + ErrorIsOfTypeLevel[*mockError](sentry.LevelDebug), ) }, },