From fcdfde91288ed23b90b9ba72a3c6a5ceeaa3ed30 Mon Sep 17 00:00:00 2001 From: ryomak Date: Sun, 31 Mar 2024 16:28:26 +0900 Subject: [PATCH] major renovation --- .github/workflows/golangci-lint.yml | 23 ++++++ .github/workflows/test.yml | 16 ++--- .gitignore | 2 + .golangci.yml | 14 ++++ README.md | 104 ++++++++++++++++++++++------ code.go | 10 +-- code_test.go | 8 +-- error.go | 15 ++-- wrapper.go => error_wrapper.go | 12 ++-- example/send_sentry/main.go | 12 ++-- format.go | 7 +- format_test.go | 8 ++- frame.go | 43 ------------ frames.go | 40 +++++++++++ go.mod | 7 +- go.sum | 8 --- json.go | 24 +++++-- json_test.go | 4 +- sentry.go | 48 +++++++++---- sentry_test.go | 66 ++++++++++++++++++ serrs.go | 6 +- serrs_test.go | 97 ++++++++++++++++++-------- 22 files changed, 401 insertions(+), 173 deletions(-) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .golangci.yml rename wrapper.go => error_wrapper.go (55%) delete mode 100644 frame.go create mode 100644 frames.go create mode 100644 sentry_test.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..7d057fa --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,23 @@ +# ワークフローの名前 +name: golangci-lint ReviewDog + +on: + pull_request: + +# ジョブ定義 +jobs: + golangci-lint: + permissions: + checks: write + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: reviewdog/action-golangci-lint@v2 + with: + github_token: ${{ secrets.github_token }} + go_version: ^1.22 + reporter: github-pr-review + level: warning + golangci_lint_flags: "--config=.golangci.yml" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e07a42..0b9fe8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,8 @@ jobs: fail-fast: false matrix: go: - - '^1.20' - - '^1.21' + - '^1.18' + - '^1.22' steps: - name: Check out repository code uses: actions/checkout@v3 @@ -25,17 +25,11 @@ jobs: go-version: ${{ matrix.go }} - name: vet run: go vet ./... - - name: Declare some variables - id: vars - run: | - echo "coverage_txt=${RUNNER_TEMP}/coverage.txt" >> "$GITHUB_OUTPUT" - - name: Test Coverage (pkg) - run: go test ./... -race -coverprofile=${{ steps.vars.outputs.coverage_txt }} + - name: Run coverage + run: go test ./... -race -coverprofile=coverage.out -covermode=atomic - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 if: ${{ matrix.go == '^1.21' }} with: token: ${{ secrets.CODECOV_TOKEN }} - slug: ryomak/serrs - - name: Fuzzing serrs package - run: go test . -fuzz=Fuzz -fuzztime=300s \ No newline at end of file + slug: ryomak/serrs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 152b08e..f83951d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ go.work .DS_Store tmp + +vendor \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..84e36c9 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,14 @@ +linters: + disable-all: true + enable: + - govet + - errcheck + - staticcheck + - unused + - gosimple + - varcheck + - misspell + - lll + - gofumpt + - paralleltest + - revive \ No newline at end of file diff --git a/README.md b/README.md index aaa6782..414adc9 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,15 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/ryomak/serrs.svg)](https://pkg.go.dev/github.com/ryomak/serrs) [![GitHub Actions](https://github.com/ryomak/serrs/workflows/test/badge.svg)](https://github.com/ryomak/serrs/actions?query=workflows%3Atest) -[![codecov](https://codecov.io/gh/ryomak/serrs/branch/master/graph/badge.svg)](https://codecov.io/gh/ryomak/serrs) +[![codecov](https://codecov.io/gh/ryomak/serrs/branch/main/graph/badge.svg)](https://codecov.io/gh/ryomak/serrs) [![Go Report Card](https://goreportcard.com/badge/github.com/ryomak/serrs)](https://goreportcard.com/report/github.com/ryomak/serrs) # description -serrs is a package that provides a simple error handling mechanism. - - +serrs is a library designed to simplify error handling in your applications. +By using serrs, developers can effortlessly manage stack traces and integrate with monitoring tools like Sentry. +This library enables you to delegate the complex logic of error handling to serrs, maintaining code readability while also ensuring detailed tracking of errors. # Installation @@ -22,8 +22,13 @@ go get -u github.com/ryomak/serrs # Usage ## Create an error ```go +var HogeError = serrs.New(serrs.DefaultCode("unexpected"),"unexpected error") +``` -var HogeError = serrs.New(serrs.Unexpceted,"unexpected error") +or + +```go +var InvalidParameterError = serrs.New(serrs.DefaultCode("invalid_parameter"),"invalid parameter error") ``` ## Wrap an error and add a stack trace @@ -36,10 +41,10 @@ if err := DoSomething(); err != nil { fmt.Printf("%+v",err) -// Output: +// Output Example: // - file: ./serrs/format_test.go:22 // function: github.com/ryomak/serrs_test.TestSerrs_Format -// msg: wrap error +// msg: // - file: ./serrs/format_test.go:14 // function: github.com/ryomak/serrs_test.TestSerrs_Format // code: demo @@ -47,26 +52,81 @@ fmt.Printf("%+v",err) // - error1 ``` -## SendSentry -serrs supports sending errors to Sentry. +### Parameters +The parameters that can be managed with serrs are three: `code`, `message`, and `data`. +`WithXXX` functions can be used to add additional information to the error. +- code: error code +- message: err text +- data: custom data + +**data** +`data` is a custom data that can be added to the error. The data is output to the log. +If the type satisfies the CustomData interface, any type can be added. + +```go +if err := DoSomething(); err != nil { + return serrs.Wrap(err, serrs.WithData(serrs.DefaultCustomData{ + "key": "value", + })) +} +``` + +### Get Additional Data +- GetCustomData: Get custom data from error +- GetErrorCode: Get error code from error +- GetErrorCodeString: Get error code string from error +- ErrorSurface: Get top level error message +- Origin: Get original error + +## check error match +```go +var HogeError = serrs.New(serrs.DefaultCode("unexpected"),"unexpected error") + +if serrs.Is(HogeError) { + +} +``` + +## Send Sentry +supports sending reports to Sentry. +The location where serrs.Wrap is executed is saved as a stack trace and displayed cleanly on Sentry. In addition, any added custom data or messages are also displayed as additional data on Sentry. + ```go + serrs.ReportSentry( - err, - serrs.WithSentryContexts(map[string]sentry.Context{ - "custom": map[string]any{ - "key": "value", - }, - }), - serrs.WithSentryTags(map[string]string{ - "code": serrs.GetErrorCodeString(err), - }), - serrs.WithSentryLevel(sentry.LevelInfo), + err, + // Customize the contexts + serrs.WithSentryContexts(map[string]sentry.Context{ + "custom": map[string]any{ + "key": "value", + }, + }), + // Customize the Sentry tags + serrs.WithSentryTags(map[string]string{ + "code": serrs.GetErrorCodeString(err), + }), + // Customize the Sentry Level + serrs.WithSentryLevel(sentry.LevelInfo), ) ``` or ```go -event := serrs.GenerateSentryEvent(err) -sentry.CaptureEvent(event) -``` \ No newline at end of file + +import ( + "github.com/getsentry/sentry-go" +) + +func main() { + sentry.Init(sentry.ClientOptions{ + Dsn: "your-dsn", + }) + defer sentry.Flush(2 * time.Second) + + if err := DoSomething(); err != nil { + event := serrs.GenerateSentryEvent(err) + sentry.CaptureEvent(event) + } +} +``` diff --git a/code.go b/code.go index cd9b0d6..80bce36 100644 --- a/code.go +++ b/code.go @@ -5,13 +5,9 @@ type Code interface { ErrorCode() string } -// StringCode is a type that represents an error code as a string. -type StringCode string +// DefaultCode is a type that represents an error code as a string. +type DefaultCode string -func (s StringCode) ErrorCode() string { +func (s DefaultCode) ErrorCode() string { return string(s) } - -const ( - StringCodeUnexpected StringCode = "unexpected" -) diff --git a/code_test.go b/code_test.go index c4ec8ed..c054acb 100644 --- a/code_test.go +++ b/code_test.go @@ -6,22 +6,22 @@ import ( "github.com/ryomak/serrs" ) -func TestStringCode_ErrorCode(t *testing.T) { +func TestDefaultCode_ErrorCode(t *testing.T) { tests := []struct { name string - s serrs.StringCode + s serrs.DefaultCode want string }{ { name: "unexpected", - s: serrs.StringCodeUnexpected, + s: serrs.DefaultCode("unexpected"), want: "unexpected", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.s.ErrorCode(); got != tt.want { - t.Errorf("StringCode.GetErrorCode() = %v, want %v", got, tt.want) + t.Errorf("DefaultCode.GetErrorCode() = %v, want %v", got, tt.want) } }) } diff --git a/error.go b/error.go index 471d590..0383d61 100644 --- a/error.go +++ b/error.go @@ -22,8 +22,8 @@ type simpleError struct { // cause is the cause of the error cause error - // frame is the location where the error occurred - frame Frame + // frames is the location where the error occurred + frames Frames // data is the custom data attached to the error data CustomData @@ -32,7 +32,7 @@ type simpleError struct { func newSimpleError(msg string, skip int) *simpleError { e := new(simpleError) e.message = msg - e.frame = caller(skip + 1) + e.frames = caller(skip + 1) return e } @@ -63,9 +63,14 @@ func (s *simpleError) Error() string { func (s *simpleError) Is(target error) bool { if targetErr := asSimpleError(target); targetErr != nil { - return targetErr.getCode() == s.getCode() + targetCode := targetErr.getCode() + sCode := s.getCode() + if targetCode != nil && sCode != nil { + return targetCode.ErrorCode() == sCode.ErrorCode() + } } - return s == target + + return false } func (s *simpleError) Unwrap() error { diff --git a/wrapper.go b/error_wrapper.go similarity index 55% rename from wrapper.go rename to error_wrapper.go index 17b2618..dd8b861 100644 --- a/wrapper.go +++ b/error_wrapper.go @@ -1,10 +1,12 @@ package serrs -type wrapper interface { +// errWrapper is a function that adds information to the error. +type errWrapper interface { wrap(err *simpleError) } -func WithCode(code Code) wrapper { +// WithCode returns an error wrapper that adds a code to the error. +func WithCode(code Code) errWrapper { return codeWrapper{code: code} } @@ -16,7 +18,8 @@ func (c codeWrapper) wrap(err *simpleError) { _ = err.withCode(c.code) } -func WithMessage(msg string) wrapper { +// WithMessage returns an error wrapper that adds a message to the error. +func WithMessage(msg string) errWrapper { return messageWrapper{message: msg} } @@ -28,7 +31,8 @@ func (m messageWrapper) wrap(err *simpleError) { _ = err.withMessage(m.message) } -func WithCustomData(data CustomData) wrapper { +// WithData returns an error wrapper that adds custom data to the error. +func WithData(data CustomData) errWrapper { return customDataWrapper{data: data} } diff --git a/example/send_sentry/main.go b/example/send_sentry/main.go index ffa8339..a7cca94 100644 --- a/example/send_sentry/main.go +++ b/example/send_sentry/main.go @@ -8,6 +8,8 @@ import ( "github.com/ryomak/serrs" ) +const DefaultCodeUnexpected serrs.DefaultCode = "unexpected" + func main() { err := sentry.Init(sentry.ClientOptions{ @@ -38,7 +40,7 @@ func main() { } func Do() error { - return serrs.New(serrs.StringCode("do_err"), "unexpected do error") + return serrs.New(serrs.DefaultCode("do_err"), "unexpected do error") } func Do2() error { @@ -52,9 +54,9 @@ func Do3() error { if err := Do2(); err != nil { return serrs.Wrap( err, - serrs.WithCode(serrs.StringCodeUnexpected), + serrs.WithCode(DefaultCodeUnexpected), serrs.WithMessage("do2 error"), - serrs.WithCustomData(serrs.DefaultCustomData{ + serrs.WithData(serrs.DefaultCustomData{ "key": "value", }), ) @@ -66,9 +68,9 @@ func Do4() error { if err := Do3(); err != nil { return serrs.Wrap( err, - serrs.WithCode(serrs.StringCodeUnexpected), + serrs.WithCode(DefaultCodeUnexpected), serrs.WithMessage("Do4 error"), - serrs.WithCustomData(serrs.DefaultCustomData{ + serrs.WithData(serrs.DefaultCustomData{ "userName": "hoge", }), ) diff --git a/format.go b/format.go index 2acb659..213ef52 100644 --- a/format.go +++ b/format.go @@ -26,8 +26,8 @@ func (s *simpleError) printerFormat(p printer) error { message += fmt.Sprintf("data: %v", s.data) } - // print stack frame - s.frame.format(p) + // print stack frames + s.frames.format(p) // print message p.Print(message) @@ -48,7 +48,7 @@ func formatError(f *simpleError, s fmt.State, verb rune) { switch verb { - case 'v': + case 'v', 's': if s.Flag('#') { if stringer, ok := err.(fmt.GoStringer); ok { _, _ = io.WriteString(&p.buf, stringer.GoString()) @@ -64,7 +64,6 @@ func formatError(f *simpleError, s fmt.State, verb rune) { } } - case 's': case 'q', 'x', 'X': direct = false diff --git a/format_test.go b/format_test.go index d243654..baf9d8a 100644 --- a/format_test.go +++ b/format_test.go @@ -13,8 +13,8 @@ func TestSerrs_Format(t *testing.T) { e1 := errors.New("error1") err := serrs.Wrap( e1, - serrs.WithCode(serrs.StringCode("demo")), - serrs.WithCustomData(serrs.DefaultCustomData{ + serrs.WithCode(serrs.DefaultCode("demo")), + serrs.WithData(serrs.DefaultCustomData{ "key1": "value1", "key2": "value2", }), @@ -32,5 +32,9 @@ func TestSerrs_Format(t *testing.T) { checkEqual(t, fmt.Sprintf("%v", err), "wrap error: error1") checkEqual(t, fmt.Sprintf("%#v", err), "wrap error: error1") + checkEqual(t, fmt.Sprintf("%s", err), "wrap error: error1") + // unexpected format + //nolint + checkEqual(t, fmt.Sprintf("%a", err), "%!a(*serrs.simpleError)") } diff --git a/frame.go b/frame.go deleted file mode 100644 index 9eaac08..0000000 --- a/frame.go +++ /dev/null @@ -1,43 +0,0 @@ -package serrs - -import ( - "runtime" -) - -// A Frame represents a program counter inside a stack frame. -type Frame struct { - // https://go.googlesource.com/go/+/032678e0fb/src/runtime/extern.go#169 - frames [3]uintptr -} - -// caller returns a Frame that describes a frame on the caller's stack. -func caller(skip int) Frame { - var s Frame - runtime.Callers(skip+1, s.frames[:]) - return s -} - -// location returns the function, file, and line number of a Frame. -func (f Frame) location() (function, file string, line int) { - frames := runtime.CallersFrames(f.frames[:]) - if _, ok := frames.Next(); !ok { - return "", "", 0 - } - fr, ok := frames.Next() - if !ok { - return "", "", 0 - } - return fr.Function, fr.File, fr.Line -} - -func (f Frame) format(p printer) { - if p.Detail() { - function, file, line := f.location() - if file != "" { - p.Printf("file: %s:%d\n", file, line) - } - if function != "" { - p.Printf("function: %s\n", function) - } - } -} diff --git a/frames.go b/frames.go new file mode 100644 index 0000000..94977d9 --- /dev/null +++ b/frames.go @@ -0,0 +1,40 @@ +package serrs + +import ( + "runtime" +) + +// A Frames represents a program counter inside a stack frames. +type Frames []uintptr + +// caller returns a Frames that describes a frames on the caller's stack. +func caller(skip int) Frames { + f := [32]uintptr{} + n := runtime.Callers(skip+1, f[:]) + return f[:n] +} + +// location returns the function, file, and line number of a Frames. +func (f Frames) location() (function, file string, line int) { + frames := runtime.CallersFrames(f[:]) + if _, ok := frames.Next(); !ok { + return "", "", 0 + } + fr, ok := frames.Next() + if !ok { + return "", "", 0 + } + return fr.Function, fr.File, fr.Line +} + +func (f Frames) format(p printer) { + if p.Detail() { + function, file, line := f.location() + if file != "" { + p.Printf("file: %s:%d\n", file, line) + } + if function != "" { + p.Printf("function: %s\n", function) + } + } +} diff --git a/go.mod b/go.mod index 5746ecb..3da9e48 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,8 @@ module github.com/ryomak/serrs -go 1.21.1 +go 1.18 -require ( - github.com/getsentry/sentry-go v0.27.0 - github.com/pkg/errors v0.9.1 -) +require github.com/getsentry/sentry-go v0.27.0 require ( golang.org/x/sys v0.6.0 // indirect diff --git a/go.sum b/go.sum index b3daf4a..9869a17 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/json.go b/json.go index 2571ffd..36d11a3 100644 --- a/json.go +++ b/json.go @@ -1,9 +1,21 @@ package serrs -var stackedErrorJsonFormatter = func(msg string, data CustomData) any { +var stackedErrorJsonFormatter = func(err error) any { + if err == nil { + return defaultErrorJson{} + } + + e := asSimpleError(err) + if e == nil { + return defaultErrorJson{ + Message: err.Error(), + Data: nil, + } + } + return defaultErrorJson{ - Message: msg, - Data: data, + Message: e.message, + Data: e.data, } } @@ -22,7 +34,7 @@ func StackedErrorJson(err error) []any { se := asSimpleError(err) if se == nil { - return append(m, stackedErrorJsonFormatter(err.Error(), nil)) + return append(m, stackedErrorJsonFormatter(err)) } if causeErr := asSimpleError(se.cause); causeErr != nil { m = append(m, StackedErrorJson(causeErr)...) @@ -32,10 +44,10 @@ func StackedErrorJson(err error) []any { if se.message == "" && se.data == nil { return m } - return append(m, stackedErrorJsonFormatter(se.message, se.data)) + return append(m, stackedErrorJsonFormatter(se)) } -type StackedErrorJsonFormatter func(msg string, data CustomData) any +type StackedErrorJsonFormatter func(err error) any type defaultErrorJson struct { Message string `json:"message"` diff --git a/json_test.go b/json_test.go index 69fd3aa..e73be9c 100644 --- a/json_test.go +++ b/json_test.go @@ -17,9 +17,9 @@ func TestStackedErrorJson(t *testing.T) { { name: "simpleError", in: serrs.Wrap( - serrs.New(serrs.StringCodeUnexpected, "unexpected error"), + serrs.New(serrs.DefaultCode("unexpected"), "unexpected error"), serrs.WithMessage("wrap error"), - serrs.WithCustomData(serrs.DefaultCustomData{"key1": "value1"}), + serrs.WithData(serrs.DefaultCustomData{"key1": "value1"}), ), want: `[{"message":"unexpected error","data":null},{"message":"wrap error","data":{"key1":"value1"}}]`, }, diff --git a/sentry.go b/sentry.go index cab4a1a..39446ae 100644 --- a/sentry.go +++ b/sentry.go @@ -1,24 +1,22 @@ package serrs import ( + "context" + sentry "github.com/getsentry/sentry-go" - pkgErrors "github.com/pkg/errors" ) // StackTrace is a method to get the stack trace of the error for sentry-go // https://github.com/getsentry/sentry-go/blob/master/stacktrace.go#L84-L87 -func (s *simpleError) StackTrace() pkgErrors.StackTrace { - - f := make([]pkgErrors.Frame, 0, 30) - - if next := asSimpleError(s.cause); next != nil { - f = append(f, next.StackTrace()...) +func (s *simpleError) StackTrace() []uintptr { + origin := originSimpleError(s) + if origin == nil { + return []uintptr{} } - - // frames 0: newSimpleError() frames 1: frame0+1 - f = append(f, pkgErrors.Frame(s.frame.frames[1])) - - return f + if len(origin.frames) <= 1 { + return origin.frames + } + return origin.frames[1:] } // GenerateSentryEvent is a method to generate a sentry event from an error @@ -28,7 +26,7 @@ func GenerateSentryEvent(err error, ws ...sentryWrapper) *sentry.Event { } errCode, ok := GetErrorCode(err) if !ok { - errCode = StringCodeUnexpected + errCode = DefaultCode("unknown") } event := sentry.NewEvent() event.Level = sentry.LevelError @@ -57,6 +55,17 @@ func ReportSentry(err error, ws ...sentryWrapper) { sentry.CaptureEvent(event) } +// ReportSentryWithContext is a method to report an error to sentry with a context +func ReportSentryWithContext(ctx context.Context, err error, ws ...sentryWrapper) { + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + ReportSentry(err, ws...) + return + } + event := GenerateSentryEvent(err, ws...) + hub.CaptureEvent(event) +} + type sentryWrapper interface { wrap(event *sentry.Event) *sentry.Event } @@ -109,3 +118,16 @@ func (s sentryEventLevelWrapper) wrap(event *sentry.Event) *sentry.Event { return event } + +func originSimpleError(err error) *simpleError { + var e *simpleError + for { + if err == nil { + return e + } + if ee := asSimpleError(err); ee != nil { + e = ee + } + err = Unwrap(err) + } +} diff --git a/sentry_test.go b/sentry_test.go new file mode 100644 index 0000000..f0e0ed5 --- /dev/null +++ b/sentry_test.go @@ -0,0 +1,66 @@ +package serrs_test + +import ( + "context" + "errors" + "testing" + + sentry "github.com/getsentry/sentry-go" + "github.com/ryomak/serrs" +) + +func TestGenerateSentryEvent_WithNilError(t *testing.T) { + t.Parallel() + + event := serrs.GenerateSentryEvent(nil) + if event != nil { + t.Errorf("Expected nil, but got %v", event) + } +} + +func TestGenerateSentryEvent_WithUnknownErrorCode(t *testing.T) { + t.Parallel() + + err := errors.New("test error") + event := serrs.GenerateSentryEvent(err) + if event.Contexts["error detail"]["code"] != "unknown" { + t.Errorf("Expected 'unknown', but got %v", event.Contexts["error detail"]["code"]) + } +} + +func TestGenerateSentryEvent_WithKnownErrorCode(t *testing.T) { + t.Parallel() + + err := serrs.Wrap(serrs.New(serrs.DefaultCode("known"), "test error")) + event := serrs.GenerateSentryEvent(err) + if event.Contexts["error detail"]["code"] != "known" { + t.Errorf("Expected 'known', but got %v", event.Contexts["error detail"]["code"]) + } +} + +func TestGenerateSentryEvent_WithSentryOptions(t *testing.T) { + t.Parallel() + + err := serrs.Wrap(serrs.New(serrs.DefaultCode("known"), "test error")) + event := serrs.GenerateSentryEvent( + err, + serrs.WithSentryTags(map[string]string{"key": "value"}), + serrs.WithSentryLevel(sentry.LevelWarning), + serrs.WithSentryContexts(map[string]sentry.Context{ + "requestData": map[string]any{ + "key": "value", + }, + }), + ) + checkEqual(t, event.Contexts["requestData"], map[string]any{"key": "value"}) + checkEqual(t, event.Level, sentry.LevelWarning) + checkEqual(t, event.Tags, map[string]string{"key": "value"}) +} + +func TestReportSentry(t *testing.T) { + t.Parallel() + + err := serrs.New(serrs.DefaultCode("test"), "test error") + serrs.ReportSentry(err) + serrs.ReportSentryWithContext(context.Background(), err) +} diff --git a/serrs.go b/serrs.go index 8f04b11..fcfcc47 100644 --- a/serrs.go +++ b/serrs.go @@ -12,8 +12,8 @@ func New(code Code, msg string) error { } // Wrap returns an error with a stack trace. -// wrapper is a function that adds information to the error. -func Wrap(err error, ws ...wrapper) error { +// errWrapper is a function that adds information to the error. +func Wrap(err error, ws ...errWrapper) error { if err == nil { return nil } @@ -25,7 +25,7 @@ func Wrap(err error, ws ...wrapper) error { return e } -// Is reports whether the error is the same as the target error. +// Is reports whether the error's tree is the same as the target error. func Is(err error, target error) bool { return errors.Is(err, target) } diff --git a/serrs_test.go b/serrs_test.go index ca613ea..162058b 100644 --- a/serrs_test.go +++ b/serrs_test.go @@ -1,6 +1,7 @@ package serrs_test import ( + "errors" "testing" "github.com/ryomak/serrs" @@ -8,7 +9,7 @@ import ( func TestSerrs(t *testing.T) { - baseErr := serrs.New(serrs.StringCode("hoge_error"), "hoge error") + baseErr := serrs.New(serrs.DefaultCode("hoge_error"), "hoge error") type want struct { checkNil bool code serrs.Code @@ -21,14 +22,14 @@ func TestSerrs(t *testing.T) { "New": { in: baseErr, want: want{ - code: serrs.StringCode("hoge_error"), + code: serrs.DefaultCode("hoge_error"), error: "hoge error", }, }, "Wrap": { in: serrs.Wrap(baseErr, serrs.WithMessage("wrap error")), want: want{ - code: serrs.StringCode("hoge_error"), + code: serrs.DefaultCode("hoge_error"), error: "wrap error: hoge error", }, }, @@ -62,33 +63,71 @@ func TestSimpleError_Is(t *testing.T) { target error want bool }{ - "same error": { - err: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), - target: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), + "simpleError -> simpleError: same code": { + err: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), + target: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), want: true, }, - "diff error": { - err: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), - target: serrs.New(serrs.StringCode("fuga_error"), "fuga error"), + "simpleError -> simpleError: other code": { + err: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), + target: serrs.New(serrs.DefaultCode("fuga_error"), "fuga error"), want: false, }, - "wrap error": { - err: serrs.Wrap(serrs.New(serrs.StringCode("hoge_error"), "hoge error"), serrs.WithMessage("wrap error")), - target: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), + "wrap simpleError -> simpleError: same code": { + err: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithMessage("wrap error")), + target: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), + want: true, + }, + "wrap simpleError -> simpleError : same code WithCode": { + err: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithCode(serrs.DefaultCode("fuga_error"))), + target: serrs.New(serrs.DefaultCode("fuga_error"), "fuga error"), + want: true, + }, + "normal error -> normal error": { + err: errors.ErrUnsupported, + target: errors.ErrUnsupported, want: true, }, - "wrap error: withCode match": { - err: serrs.Wrap(serrs.New(serrs.StringCode("hoge_error"), "hoge error"), serrs.WithCode(serrs.StringCode("fuga_error"))), - target: serrs.New(serrs.StringCode("fuga_error"), "fuga error"), + "wrap normal error -> normal error": { + err: serrs.Wrap(errors.ErrUnsupported, serrs.WithMessage("wrap error")), + target: errors.ErrUnsupported, + want: true, + }, + "normal error -> wrap normal error": { + err: errors.ErrUnsupported, + target: serrs.Wrap(errors.ErrUnsupported, serrs.WithMessage("wrap error")), + want: false, + }, + "wrap normal error -> other wrap normal error": { + err: serrs.Wrap(errors.ErrUnsupported, serrs.WithCode(serrs.DefaultCode("fuga_error"))), + target: serrs.Wrap(errors.ErrUnsupported, serrs.WithCode(serrs.DefaultCode("hoge_error"))), + want: false, + }, + "wrap simpleError-> simpleError: same code": { + err: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithCode(serrs.DefaultCode("fuga_error"))), + target: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), want: true, }, + "simpleError -> wrap simpleError: same code": { + err: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), + target: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithCode(serrs.DefaultCode("fuga_error"))), + want: false, + }, + "wrap normal error -> nil": { + err: serrs.Wrap(errors.ErrUnsupported), + target: nil, + want: false, + }, + "nil -> wrap normal error": { + err: nil, + target: serrs.Wrap(errors.ErrUnsupported), + want: false, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - if serrs.Is(tt.err, tt.target) != tt.want { - t.Errorf("got %v, want %v", !tt.want, tt.want) - } + checkEqual(t, serrs.Is(tt.err, tt.target), tt.want) }) } } @@ -99,12 +138,12 @@ func TestOrigin(t *testing.T) { want error }{ "simple error": { - in: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), - want: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), + in: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), + want: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), }, "wrap error": { - in: serrs.Wrap(serrs.New(serrs.StringCode("hoge_error"), "hoge error"), serrs.WithMessage("wrap error")), - want: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), + in: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithMessage("wrap error")), + want: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), }, "nil error": { in: nil, @@ -126,7 +165,7 @@ func TestWrap_WithCustomData(t *testing.T) { want []serrs.CustomData }{ "simple error": { - in: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), + in: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), data: serrs.DefaultCustomData{ "key": "value", }, @@ -147,7 +186,7 @@ func TestWrap_WithCustomData(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - checkEqual(t, serrs.GetCustomData(serrs.Wrap(tt.in, serrs.WithCustomData(tt.data))), tt.want) + checkEqual(t, serrs.GetCustomData(serrs.Wrap(tt.in, serrs.WithData(tt.data))), tt.want) }) } } @@ -155,13 +194,13 @@ func TestWrap_WithCustomData(t *testing.T) { func TestGetCustomData_Example(t *testing.T) { var in error if err := func() error { - return serrs.Wrap(serrs.New(serrs.StringCode("hoge_error"), "hoge error"), serrs.WithCustomData(serrs.DefaultCustomData{ + return serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithData(serrs.DefaultCustomData{ "key": "value", })) }(); err != nil { in = serrs.Wrap( err, - serrs.WithCustomData(serrs.DefaultCustomData{ + serrs.WithData(serrs.DefaultCustomData{ "key2": "value2", }), ) @@ -183,15 +222,15 @@ func TestErrorSurface(t *testing.T) { want string }{ "simple error": { - in: serrs.New(serrs.StringCode("hoge_error"), "hoge error"), + in: serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), want: "hoge error", }, "wrap error": { - in: serrs.Wrap(serrs.New(serrs.StringCode("hoge_error"), "hoge error"), serrs.WithMessage("wrap error")), + in: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error"), serrs.WithMessage("wrap error")), want: "wrap error", }, "wrap error without msg": { - in: serrs.Wrap(serrs.New(serrs.StringCode("hoge_error"), "hoge error")), + in: serrs.Wrap(serrs.New(serrs.DefaultCode("hoge_error"), "hoge error")), want: "hoge error", }, "nil error": {