mirror of
https://github.com/retailcrm/mg-transport-core.git
synced 2024-11-22 05:06:04 +03:00
Better stacktrace generation system, better tests for Sentry, new badge
This commit is contained in:
parent
e6dd1643cf
commit
1de2e5626f
@ -4,9 +4,12 @@ env:
|
||||
go:
|
||||
- '1.12'
|
||||
- '1.13'
|
||||
- '1.14'
|
||||
before_install:
|
||||
- go mod tidy
|
||||
script:
|
||||
- go test ./... -v -cpu 2 -timeout 2m -race -cover -coverprofile=coverage.txt -covermode=atomic
|
||||
- go get -v -u github.com/axw/gocov/gocov
|
||||
- gocov convert ./coverage.txt | gocov report
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
@ -1,7 +1,7 @@
|
||||
## MG Transport Library
|
||||
[![Build Status](https://travis-ci.org/retailcrm/mg-transport-core.svg?branch=master)](https://travis-ci.org/retailcrm/mg-transport-core)
|
||||
[![codecov](https://codecov.io/gh/retailcrm/mg-transport-core/branch/master/graph/badge.svg)](https://codecov.io/gh/retailcrm/mg-transport-core)
|
||||
[![GoDoc](https://godoc.org/github.com/retailcrm/mg-transport-core/core?status.svg)](https://godoc.org/github.com/retailcrm/mg-transport-core/core)
|
||||
[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/mg-transport-core/core)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/mg-transport-core)](https://goreportcard.com/report/github.com/retailcrm/mg-transport-core)
|
||||
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/retailcrm/mg-transport-core/blob/master/LICENSE.md)
|
||||
This library provides different functions like error-reporting, logging, localization, etc. in order to make it easier to create transports.
|
||||
|
@ -7,6 +7,11 @@ type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// ErrorsResponse struct
|
||||
type ErrorsResponse struct {
|
||||
Error []string `json:"error"`
|
||||
}
|
||||
|
||||
// GetErrorResponse returns ErrorResponse with specified status code
|
||||
// Usage (with gin):
|
||||
// context.JSON(GetErrorResponse(http.StatusPaymentRequired, "Not enough money"))
|
||||
|
@ -452,7 +452,7 @@ func (t *JobManagerTest) Test_RunJobOnce() {
|
||||
require.NotNil(t.T(), t.manager.jobs)
|
||||
go func() { t.runnerFlag <- false }()
|
||||
err := t.manager.RunJobOnce("job")
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
require.NoError(t.T(), err)
|
||||
assert.True(t.T(), t.ranFlag())
|
||||
}
|
||||
|
@ -290,7 +290,7 @@ func (l *Localizer) Localize(messageID string) (string, error) {
|
||||
return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{MessageID: messageID})
|
||||
}
|
||||
|
||||
// LocalizedTemplateMessage will return localized message with specified data, or error if message wasn't found
|
||||
// LocalizeTemplateMessage will return localized message with specified data, or error if message wasn't found
|
||||
// It uses text/template syntax: https://golang.org/pkg/text/template/
|
||||
func (l *Localizer) LocalizeTemplateMessage(messageID string, templateData map[string]interface{}) (string, error) {
|
||||
return l.getCurrentLocalizer().Localize(&i18n.LocalizeConfig{
|
||||
@ -313,12 +313,10 @@ func GetContextLocalizer(c *gin.Context) (*Localizer, bool) {
|
||||
if item, ok := c.Get(LocalizerContextKey); ok {
|
||||
if localizer, ok := item.(*Localizer); ok {
|
||||
return localizer, true
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// MustGetContextLocalizer returns Localizer instance if it exists in provided context. Panics otherwise.
|
||||
|
117
core/sentry.go
117
core/sentry.go
@ -3,13 +3,12 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/retailcrm/mg-transport-core/core/stacktrace"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -39,7 +38,7 @@ type Sentry struct {
|
||||
DefaultError string
|
||||
Localizer *Localizer
|
||||
Logger LoggerInterface
|
||||
Client *raven.Client
|
||||
Client stacktrace.RavenClientInterface
|
||||
}
|
||||
|
||||
// SentryTaggedStruct holds information about type, it's key in gin.Context (for middleware), and it's properties
|
||||
@ -215,24 +214,31 @@ func (s *Sentry) ErrorCaptureHandler() ErrorHandlerFunc {
|
||||
}
|
||||
|
||||
if recovery != nil {
|
||||
stacktrace := raven.NewStacktrace(4, 3, nil)
|
||||
stack := raven.NewStacktrace(4, 3, nil)
|
||||
recStr := fmt.Sprint(recovery)
|
||||
err := errors.New(recStr)
|
||||
go s.Client.CaptureMessageAndWait(
|
||||
recStr,
|
||||
tags,
|
||||
raven.NewException(err, stacktrace),
|
||||
raven.NewException(err, stack),
|
||||
raven.NewHttp(c.Request),
|
||||
)
|
||||
}
|
||||
|
||||
for _, err := range c.Errors {
|
||||
if s.Stacktrace {
|
||||
stacktrace := newRavenStackTrace(s.Client, err.Err, 0)
|
||||
stackBuilder := stacktrace.GetStackBuilderByErrorType(err.Err)
|
||||
stackBuilder.SetClient(s.Client)
|
||||
stack, buildErr := stackBuilder.Build().GetResult()
|
||||
if buildErr != nil {
|
||||
go s.Client.CaptureErrorAndWait(buildErr, tags)
|
||||
stack = stacktrace.GenericStack(s.Client)
|
||||
}
|
||||
|
||||
go s.Client.CaptureMessageAndWait(
|
||||
err.Error(),
|
||||
tags,
|
||||
raven.NewException(err.Err, stacktrace),
|
||||
raven.NewException(err.Err, stack),
|
||||
raven.NewHttp(c.Request),
|
||||
)
|
||||
} else {
|
||||
@ -381,100 +387,3 @@ func (t *SentryTaggedScalar) BuildTags(v interface{}) (items map[string]string,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newRavenStackTrace generate stacktrace compatible with raven-go format
|
||||
// It tries to extract better stacktrace from error from package "github.com/pkg/errors"
|
||||
// In case of fail it will fallback to default stacktrace generation from raven-go.
|
||||
// Default stacktrace highly likely will be useless, because it will not include call
|
||||
// which returned error. This occurs because default stacktrace doesn't include any call
|
||||
// before stacktrace generation, and raven-go will generate stacktrace here, which will end
|
||||
// in trace to this file. But errors from "github.com/pkg/errors" will generate stacktrace
|
||||
// immediately, it will include call which returned error, and we can fetch this trace.
|
||||
// Also we can wrap default errors with error from this package, like this:
|
||||
// errors.Wrap(err, err.Error)
|
||||
func newRavenStackTrace(client *raven.Client, myerr error, skip int) *raven.Stacktrace {
|
||||
st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths())
|
||||
if st == nil {
|
||||
st = raven.NewStacktrace(skip, 3, client.IncludePaths())
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// getErrorStackTraceConverted will return converted stacktrace from custom error, or nil in case of default error
|
||||
func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace {
|
||||
st := getErrorCauseStackTrace(err)
|
||||
if st == nil {
|
||||
return nil
|
||||
}
|
||||
return convertStackTrace(st, context, appPackagePrefixes)
|
||||
}
|
||||
|
||||
// getErrorCauseStackTrace tries to extract stacktrace from custom error, returns nil in case of failure
|
||||
func getErrorCauseStackTrace(err error) errors.StackTrace {
|
||||
// This code is inspired by github.com/pkg/errors.Cause().
|
||||
var st errors.StackTrace
|
||||
for err != nil {
|
||||
s := getErrorStackTrace(err)
|
||||
if s != nil {
|
||||
st = s
|
||||
}
|
||||
err = getErrorCause(err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// convertStackTrace converts github.com/pkg/errors.StackTrace to github.com/getsentry/raven-go.Stacktrace
|
||||
func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace {
|
||||
// This code is borrowed from github.com/getsentry/raven-go.NewStacktrace().
|
||||
var frames []*raven.StacktraceFrame
|
||||
for _, f := range st {
|
||||
frame := convertFrame(f, context, appPackagePrefixes)
|
||||
if frame != nil {
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
}
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
||||
frames[i], frames[j] = frames[j], frames[i]
|
||||
}
|
||||
return &raven.Stacktrace{Frames: frames}
|
||||
}
|
||||
|
||||
// convertFrame converts single frame from github.com/pkg/errors.Frame to github.com/pkg/errors.Frame
|
||||
func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame {
|
||||
// This code is borrowed from github.com/pkg/errors.Frame.
|
||||
pc := uintptr(f) - 1
|
||||
fn := runtime.FuncForPC(pc)
|
||||
var file string
|
||||
var line int
|
||||
if fn != nil {
|
||||
file, line = fn.FileLine(pc)
|
||||
} else {
|
||||
file = "unknown"
|
||||
}
|
||||
return raven.NewStacktraceFrame(pc, path.Dir(file), file, line, context, appPackagePrefixes)
|
||||
}
|
||||
|
||||
// getErrorStackTrace will try to extract stacktrace from error using StackTrace method (default errors doesn't have it)
|
||||
func getErrorStackTrace(err error) errors.StackTrace {
|
||||
ster, ok := err.(interface {
|
||||
StackTrace() errors.StackTrace
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return ster.StackTrace()
|
||||
}
|
||||
|
||||
// getErrorCause will try to extract original error from wrapper - it is used only if stacktrace is not present
|
||||
func getErrorCause(err error) error {
|
||||
cer, ok := err.(interface {
|
||||
Cause() error
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return cer.Cause()
|
||||
}
|
||||
|
@ -1,31 +1,156 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type SampleStruct struct {
|
||||
type sampleStruct struct {
|
||||
ID int
|
||||
Pointer *int
|
||||
Field string
|
||||
}
|
||||
|
||||
type ravenPacket struct {
|
||||
EventID string
|
||||
Message string
|
||||
Tags map[string]string
|
||||
Interfaces []raven.Interface
|
||||
}
|
||||
|
||||
func (r ravenPacket) getInterface(class string) (raven.Interface, bool) {
|
||||
for _, v := range r.Interfaces {
|
||||
if v.Class() == class {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r ravenPacket) getException() (*raven.Exception, bool) {
|
||||
if i, ok := r.getInterface("exception"); ok {
|
||||
if r, ok := i.(*raven.Exception); ok {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r ravenPacket) getRequest() (*raven.Http, bool) {
|
||||
if i, ok := r.getInterface("request"); ok {
|
||||
if r, ok := i.(*raven.Http); ok {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type ravenClientMock struct {
|
||||
captured []ravenPacket
|
||||
mu sync.RWMutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func newRavenMock() *ravenClientMock {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return &ravenClientMock{captured: []ravenPacket{}}
|
||||
}
|
||||
|
||||
func (r *ravenClientMock) Reset() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.captured = []ravenPacket{}
|
||||
}
|
||||
|
||||
func (r *ravenClientMock) last() (ravenPacket, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if len(r.captured) > 0 {
|
||||
return r.captured[len(r.captured)-1], nil
|
||||
}
|
||||
|
||||
return ravenPacket{}, errors.New("empty packet list")
|
||||
}
|
||||
|
||||
func (r *ravenClientMock) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...raven.Interface) string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
defer r.wg.Done()
|
||||
eventID := string(rand.Uint64())
|
||||
r.captured = append(r.captured, ravenPacket{
|
||||
EventID: eventID,
|
||||
Message: message,
|
||||
Tags: tags,
|
||||
Interfaces: interfaces,
|
||||
})
|
||||
return eventID
|
||||
}
|
||||
|
||||
func (r *ravenClientMock) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...raven.Interface) string {
|
||||
return r.CaptureMessageAndWait(err.Error(), tags, interfaces...)
|
||||
}
|
||||
|
||||
func (r *ravenClientMock) IncludePaths() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// simpleError is a simplest error implementation possible. The only reason why it's here is tests.
|
||||
type simpleError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func newSimpleError(msg string) error {
|
||||
return &simpleError{msg: msg}
|
||||
}
|
||||
|
||||
func (n *simpleError) Error() string {
|
||||
return n.msg
|
||||
}
|
||||
|
||||
// wrappableError is a simple implementation of wrappable error
|
||||
type wrappableError struct {
|
||||
msg string
|
||||
err error
|
||||
}
|
||||
|
||||
func newWrappableError(msg string, child error) error {
|
||||
return &wrappableError{msg: msg, err: child}
|
||||
}
|
||||
|
||||
func (e *wrappableError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *wrappableError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
type SentryTest struct {
|
||||
suite.Suite
|
||||
sentry *Sentry
|
||||
gin *gin.Engine
|
||||
structTags *SentryTaggedStruct
|
||||
scalarTags *SentryTaggedScalar
|
||||
}
|
||||
|
||||
func (s *SentryTest) SetupSuite() {
|
||||
s.structTags = NewTaggedStruct(SampleStruct{}, "struct", map[string]string{"fake": "prop"})
|
||||
s.structTags = NewTaggedStruct(sampleStruct{}, "struct", map[string]string{"fake": "prop"})
|
||||
s.scalarTags = NewTaggedScalar("", "scalar", "Scalar")
|
||||
require.Equal(s.T(), "struct", s.structTags.GetContextKey())
|
||||
require.Equal(s.T(), "scalar", s.scalarTags.GetContextKey())
|
||||
@ -33,13 +158,16 @@ func (s *SentryTest) SetupSuite() {
|
||||
require.Equal(s.T(), "Scalar", s.scalarTags.GetName())
|
||||
s.structTags.Tags = map[string]string{}
|
||||
s.sentry = NewSentry("dsn", "unknown_error", SentryTaggedTypes{}, nil, nil)
|
||||
s.sentry.Client = newRavenMock()
|
||||
s.gin = gin.New()
|
||||
s.gin.Use(s.sentry.ErrorMiddleware())
|
||||
}
|
||||
|
||||
func (s *SentryTest) TestStruct_AddTag() {
|
||||
s.structTags.AddTag("test field", "Field")
|
||||
require.NotEmpty(s.T(), s.structTags.GetTags())
|
||||
|
||||
tags, err := s.structTags.BuildTags(SampleStruct{Field: "value"})
|
||||
tags, err := s.structTags.BuildTags(sampleStruct{Field: "value"})
|
||||
require.NoError(s.T(), err)
|
||||
require.NotEmpty(s.T(), tags)
|
||||
|
||||
@ -50,7 +178,7 @@ func (s *SentryTest) TestStruct_AddTag() {
|
||||
|
||||
func (s *SentryTest) TestStruct_GetProperty() {
|
||||
s.structTags.AddTag("test field", "Field")
|
||||
name, value, err := s.structTags.GetProperty(SampleStruct{Field: "test"}, "Field")
|
||||
name, value, err := s.structTags.GetProperty(sampleStruct{Field: "test"}, "Field")
|
||||
require.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), "test field", name)
|
||||
assert.Equal(s.T(), "test", value)
|
||||
@ -71,18 +199,18 @@ func (s *SentryTest) TestStruct_GetProperty_GotScalar() {
|
||||
func (s *SentryTest) TestStruct_GetProperty_InvalidType() {
|
||||
_, _, err := s.structTags.GetProperty(Sentry{}, "Field")
|
||||
require.Error(s.T(), err)
|
||||
assert.Equal(s.T(), "passed value should be of type `core.SampleStruct`, got `core.Sentry` instead", err.Error())
|
||||
assert.Equal(s.T(), "passed value should be of type `core.sampleStruct`, got `core.Sentry` instead", err.Error())
|
||||
}
|
||||
|
||||
func (s *SentryTest) TestStruct_GetProperty_CannotFindProperty() {
|
||||
_, _, err := s.structTags.GetProperty(SampleStruct{ID: 1}, "ID")
|
||||
_, _, err := s.structTags.GetProperty(sampleStruct{ID: 1}, "ID")
|
||||
require.Error(s.T(), err)
|
||||
assert.Equal(s.T(), "cannot find property `ID`", err.Error())
|
||||
}
|
||||
|
||||
func (s *SentryTest) TestStruct_GetProperty_InvalidProperty() {
|
||||
s.structTags.AddTag("test invalid", "Pointer")
|
||||
_, _, err := s.structTags.GetProperty(SampleStruct{Pointer: nil}, "Pointer")
|
||||
_, _, err := s.structTags.GetProperty(sampleStruct{Pointer: nil}, "Pointer")
|
||||
require.Error(s.T(), err)
|
||||
assert.Equal(s.T(), "invalid property, got <invalid Value>", err.Error())
|
||||
}
|
||||
@ -97,7 +225,7 @@ func (s *SentryTest) TestStruct_BuildTags_Fail() {
|
||||
func (s *SentryTest) TestStruct_BuildTags() {
|
||||
s.structTags.Tags = map[string]string{}
|
||||
s.structTags.AddTag("test", "Field")
|
||||
tags, err := s.structTags.BuildTags(SampleStruct{Field: "value"})
|
||||
tags, err := s.structTags.BuildTags(sampleStruct{Field: "value"})
|
||||
|
||||
require.NoError(s.T(), err)
|
||||
require.NotEmpty(s.T(), tags)
|
||||
@ -169,27 +297,125 @@ func (s *SentryTest) TestSentry_ErrorCaptureHandler() {
|
||||
assert.NotNil(s.T(), s.sentry.ErrorCaptureHandler())
|
||||
}
|
||||
|
||||
func TestSentry_newRavenStackTrace_Fail(t *testing.T) {
|
||||
defer func() {
|
||||
assert.NotNil(t, recover())
|
||||
}()
|
||||
func (s *SentryTest) TestSentry_CaptureRegularError() {
|
||||
s.gin.GET("/test_regularError", func(c *gin.Context) {
|
||||
c.Error(newSimpleError("test"))
|
||||
})
|
||||
|
||||
newRavenStackTrace(nil, errors.New("error"), 0)
|
||||
var resp ErrorsResponse
|
||||
req, err := http.NewRequest(http.MethodGet, "/test_regularError", nil)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
ravenMock := s.sentry.Client.(*ravenClientMock)
|
||||
ravenMock.wg.Add(1)
|
||||
rec := httptest.NewRecorder()
|
||||
s.gin.ServeHTTP(rec, req)
|
||||
require.NoError(s.T(), json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
assert.NotEmpty(s.T(), resp.Error)
|
||||
assert.Equal(s.T(), s.sentry.DefaultError, resp.Error[0])
|
||||
|
||||
ravenMock.wg.Wait()
|
||||
last, err := ravenMock.last()
|
||||
require.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), "test", last.Message)
|
||||
|
||||
exception, ok := last.getException()
|
||||
require.True(s.T(), ok, "cannot find exception")
|
||||
require.NotNil(s.T(), exception.Stacktrace)
|
||||
assert.NotEmpty(s.T(), exception.Stacktrace.Frames)
|
||||
}
|
||||
|
||||
func TestSentry_newRavenStackTrace(t *testing.T) {
|
||||
st := newRavenStackTrace(&raven.Client{}, errors.New("error"), 0)
|
||||
// TestSentry_CaptureWrappedError is used to check if Sentry component calls stacktrace builders properly
|
||||
// Actual stacktrace builder tests can be found in the corresponding package.
|
||||
func (s *SentryTest) TestSentry_CaptureWrappedError() {
|
||||
third := newWrappableError("third", nil)
|
||||
second := newWrappableError("second", third)
|
||||
first := newWrappableError("first", second)
|
||||
|
||||
require.NotNil(t, st)
|
||||
assert.NotEmpty(t, st.Frames)
|
||||
s.gin.GET("/test_wrappableError", func(c *gin.Context) {
|
||||
c.Error(first)
|
||||
})
|
||||
|
||||
var resp ErrorsResponse
|
||||
req, err := http.NewRequest(http.MethodGet, "/test_wrappableError", nil)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
ravenMock := s.sentry.Client.(*ravenClientMock)
|
||||
ravenMock.wg.Add(1)
|
||||
rec := httptest.NewRecorder()
|
||||
s.gin.ServeHTTP(rec, req)
|
||||
require.NoError(s.T(), json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
assert.NotEmpty(s.T(), resp.Error)
|
||||
assert.Equal(s.T(), s.sentry.DefaultError, resp.Error[0])
|
||||
|
||||
ravenMock.wg.Wait()
|
||||
last, err := ravenMock.last()
|
||||
require.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), "first", last.Message)
|
||||
|
||||
exception, ok := last.getException()
|
||||
require.True(s.T(), ok, "cannot find exception")
|
||||
require.NotNil(s.T(), exception.Stacktrace)
|
||||
assert.NotEmpty(s.T(), exception.Stacktrace.Frames)
|
||||
assert.Len(s.T(), exception.Stacktrace.Frames, 3)
|
||||
|
||||
// Error messages will be put into function names by parser
|
||||
assert.Contains(s.T(), exception.Stacktrace.Frames[0].Function, third.Error())
|
||||
assert.Contains(s.T(), exception.Stacktrace.Frames[1].Function, second.Error())
|
||||
assert.Contains(s.T(), exception.Stacktrace.Frames[2].Function, first.Error())
|
||||
}
|
||||
|
||||
func TestSentry_newRavenStackTrace_ErrorsPkg(t *testing.T) {
|
||||
err := pkgErrors.New("error")
|
||||
st := newRavenStackTrace(&raven.Client{}, err, 0)
|
||||
func (s *SentryTest) TestSentry_CaptureTags() {
|
||||
s.gin.GET("/test_taggedError", func(c *gin.Context) {
|
||||
var intPointer = 147
|
||||
c.Set("text_tag", "text contents")
|
||||
c.Set("sample_struct", sampleStruct{
|
||||
ID: 12,
|
||||
Pointer: &intPointer,
|
||||
Field: "field content",
|
||||
})
|
||||
}, func(c *gin.Context) {
|
||||
c.Error(newSimpleError("test"))
|
||||
})
|
||||
|
||||
require.NotNil(t, st)
|
||||
assert.NotEmpty(t, st.Frames)
|
||||
s.sentry.TaggedTypes = SentryTaggedTypes{
|
||||
NewTaggedScalar("", "text_tag", "TextTag"),
|
||||
NewTaggedStruct(sampleStruct{}, "sample_struct", map[string]string{
|
||||
"id": "ID",
|
||||
"pointer": "Pointer",
|
||||
"field item": "Field",
|
||||
}),
|
||||
}
|
||||
|
||||
var resp ErrorsResponse
|
||||
req, err := http.NewRequest(http.MethodGet, "/test_taggedError", nil)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
ravenMock := s.sentry.Client.(*ravenClientMock)
|
||||
ravenMock.wg.Add(1)
|
||||
rec := httptest.NewRecorder()
|
||||
s.gin.ServeHTTP(rec, req)
|
||||
require.NoError(s.T(), json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
assert.NotEmpty(s.T(), resp.Error)
|
||||
assert.Equal(s.T(), s.sentry.DefaultError, resp.Error[0])
|
||||
|
||||
ravenMock.wg.Wait()
|
||||
last, err := ravenMock.last()
|
||||
require.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), "test", last.Message)
|
||||
|
||||
exception, ok := last.getException()
|
||||
require.True(s.T(), ok, "cannot find exception")
|
||||
require.NotNil(s.T(), exception.Stacktrace)
|
||||
assert.NotEmpty(s.T(), exception.Stacktrace.Frames)
|
||||
|
||||
// endpoint tag is present by default
|
||||
require.NotEmpty(s.T(), last.Tags)
|
||||
assert.True(s.T(), len(last.Tags) == 5)
|
||||
assert.Equal(s.T(), "text contents", last.Tags["TextTag"])
|
||||
assert.Equal(s.T(), "12", last.Tags["id"])
|
||||
assert.Equal(s.T(), "147", last.Tags["pointer"])
|
||||
assert.Equal(s.T(), "field content", last.Tags["field item"])
|
||||
}
|
||||
|
||||
func TestSentry_Suite(t *testing.T) {
|
||||
|
52
core/stacktrace/abstract_stack_builder.go
Normal file
52
core/stacktrace/abstract_stack_builder.go
Normal file
@ -0,0 +1,52 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrUnfeasibleBuilder will be returned if builder for stacktrace was chosen incorrectly
|
||||
var ErrUnfeasibleBuilder = errors.New("unfeasible builder for this error type")
|
||||
|
||||
// StackBuilderInterface is an interface for every stacktrace builder
|
||||
type StackBuilderInterface interface {
|
||||
SetClient(RavenClientInterface) StackBuilderInterface
|
||||
SetError(error) StackBuilderInterface
|
||||
Build() StackBuilderInterface
|
||||
GetResult() (*raven.Stacktrace, error)
|
||||
}
|
||||
|
||||
// AbstractStackBuilder contains methods, which would be implemented in every builder anyway
|
||||
type AbstractStackBuilder struct {
|
||||
err error
|
||||
buildErr error
|
||||
client RavenClientInterface
|
||||
stack *raven.Stacktrace
|
||||
}
|
||||
|
||||
// SetClient sets *raven.Client into builder. RavenClientInterface is used, so, any client might be used via facade.
|
||||
func (a *AbstractStackBuilder) SetClient(client RavenClientInterface) StackBuilderInterface {
|
||||
a.client = client
|
||||
return a
|
||||
}
|
||||
|
||||
// SetError sets error in builder, which will be processed
|
||||
func (a *AbstractStackBuilder) SetError(err error) StackBuilderInterface {
|
||||
a.err = err
|
||||
return a
|
||||
}
|
||||
|
||||
// Build stacktrace. Only implemented in the children.
|
||||
func (a *AbstractStackBuilder) Build() StackBuilderInterface {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// GetResult returns builder result.
|
||||
func (a *AbstractStackBuilder) GetResult() (*raven.Stacktrace, error) {
|
||||
return a.stack, a.buildErr
|
||||
}
|
||||
|
||||
// FallbackToGeneric fallbacks to GenericStackBuilder method
|
||||
func (a *AbstractStackBuilder) FallbackToGeneric() {
|
||||
a.stack, a.err = GenericStack(a.client), nil
|
||||
}
|
60
core/stacktrace/abstract_stack_builder_test.go
Normal file
60
core/stacktrace/abstract_stack_builder_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type AbstractStackBuilderSuite struct {
|
||||
builder *AbstractStackBuilder
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestAbstractStackBuilder(t *testing.T) {
|
||||
suite.Run(t, new(AbstractStackBuilderSuite))
|
||||
}
|
||||
|
||||
func (s *AbstractStackBuilderSuite) SetupSuite() {
|
||||
s.builder = &AbstractStackBuilder{}
|
||||
}
|
||||
|
||||
func (s *AbstractStackBuilderSuite) Test_SetClient() {
|
||||
require.Nil(s.T(), s.builder.client)
|
||||
client, _ := raven.New("fake dsn")
|
||||
s.builder.SetClient(client)
|
||||
assert.NotNil(s.T(), s.builder.client)
|
||||
}
|
||||
|
||||
func (s *AbstractStackBuilderSuite) Test_SetError() {
|
||||
require.Nil(s.T(), s.builder.err)
|
||||
s.builder.SetError(errors.New("test err"))
|
||||
assert.NotNil(s.T(), s.builder.err)
|
||||
}
|
||||
|
||||
func (s *AbstractStackBuilderSuite) Test_Build() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
require.NotNil(s.T(), r)
|
||||
require.IsType(s.T(), "", r)
|
||||
assert.Equal(s.T(), "not implemented", r.(string))
|
||||
}()
|
||||
|
||||
s.builder.Build()
|
||||
}
|
||||
|
||||
func (s *AbstractStackBuilderSuite) Test_GetResult() {
|
||||
buildErr := errors.New("build err")
|
||||
stack := raven.NewStacktrace(0, 3, []string{})
|
||||
s.builder.buildErr = buildErr
|
||||
s.builder.stack = stack
|
||||
resultStack, resultErr := s.builder.GetResult()
|
||||
|
||||
assert.Error(s.T(), resultErr)
|
||||
assert.Equal(s.T(), buildErr, resultErr)
|
||||
assert.Equal(s.T(), *stack, *resultStack)
|
||||
}
|
21
core/stacktrace/generic_stack_builder.go
Normal file
21
core/stacktrace/generic_stack_builder.go
Normal file
@ -0,0 +1,21 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"github.com/getsentry/raven-go"
|
||||
)
|
||||
|
||||
// GenericStackBuilder uses raven.NewStacktrace to build stacktrace. Only client is needed here.
|
||||
type GenericStackBuilder struct {
|
||||
AbstractStackBuilder
|
||||
}
|
||||
|
||||
// Build returns generic stacktrace.
|
||||
func (b *GenericStackBuilder) Build() StackBuilderInterface {
|
||||
b.stack = GenericStack(b.client)
|
||||
return b
|
||||
}
|
||||
|
||||
// GenericStack returns generic stacktrace.
|
||||
func GenericStack(client RavenClientInterface) *raven.Stacktrace {
|
||||
return raven.NewStacktrace(0, 3, client.IncludePaths())
|
||||
}
|
32
core/stacktrace/generic_stack_builder_test.go
Normal file
32
core/stacktrace/generic_stack_builder_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type GenericStackBuilderSuite struct {
|
||||
builder *GenericStackBuilder
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestGenericStack(t *testing.T) {
|
||||
suite.Run(t, new(GenericStackBuilderSuite))
|
||||
}
|
||||
|
||||
func (s *GenericStackBuilderSuite) SetupSuite() {
|
||||
client, _ := raven.New("fake dsn")
|
||||
s.builder = &GenericStackBuilder{AbstractStackBuilder{
|
||||
client: client,
|
||||
}}
|
||||
}
|
||||
|
||||
func (s *GenericStackBuilderSuite) Test_Build() {
|
||||
stack, err := s.builder.Build().GetResult()
|
||||
require.Nil(s.T(), err)
|
||||
assert.NotEmpty(s.T(), stack)
|
||||
}
|
88
core/stacktrace/pkg_errors_builder.go
Normal file
88
core/stacktrace/pkg_errors_builder.go
Normal file
@ -0,0 +1,88 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PkgErrorCauseable is an interface for checking Cause() method existence in the error
|
||||
type PkgErrorCauseable interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
// PkgErrorTraceable is an interface for checking StackTrace() method existence in the error
|
||||
type PkgErrorTraceable interface {
|
||||
StackTrace() pkgErrors.StackTrace
|
||||
}
|
||||
|
||||
// PkgErrorsStackTransformer transforms stack data from github.com/pkg/errors error to stacktrace.Stacktrace
|
||||
type PkgErrorsStackTransformer struct {
|
||||
stack pkgErrors.StackTrace
|
||||
}
|
||||
|
||||
// NewPkgErrorsStackTransformer is a PkgErrorsStackTransformer constructor
|
||||
func NewPkgErrorsStackTransformer(stack pkgErrors.StackTrace) *PkgErrorsStackTransformer {
|
||||
return &PkgErrorsStackTransformer{stack: stack}
|
||||
}
|
||||
|
||||
// Stack returns stacktrace (which is []uintptr internally, each uintptc is a pc)
|
||||
func (p *PkgErrorsStackTransformer) Stack() Stacktrace {
|
||||
if p.stack == nil {
|
||||
return Stacktrace{}
|
||||
}
|
||||
|
||||
result := make(Stacktrace, len(p.stack))
|
||||
for i, frame := range p.stack {
|
||||
result[i] = Frame(uintptr(frame) - 1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PkgErrorsBuilder builds stacktrace with data from github.com/pkg/errors error
|
||||
type PkgErrorsBuilder struct {
|
||||
AbstractStackBuilder
|
||||
}
|
||||
|
||||
// Build stacktrace
|
||||
func (b *PkgErrorsBuilder) Build() StackBuilderInterface {
|
||||
if !isPkgErrors(b.err) {
|
||||
b.buildErr = ErrUnfeasibleBuilder
|
||||
return b
|
||||
}
|
||||
|
||||
var stack pkgErrors.StackTrace
|
||||
err := b.err
|
||||
|
||||
for err != nil {
|
||||
s := b.getErrorStack(err)
|
||||
if s != nil {
|
||||
stack = s
|
||||
}
|
||||
err = b.getErrorCause(err)
|
||||
}
|
||||
|
||||
if len(stack) > 0 {
|
||||
b.stack = NewRavenStacktraceBuilder(NewPkgErrorsStackTransformer(stack)).Build(3, b.client.IncludePaths())
|
||||
} else {
|
||||
b.buildErr = ErrUnfeasibleBuilder
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// getErrorCause will try to extract original error from wrapper - it is used only if stacktrace is not present
|
||||
func (b *PkgErrorsBuilder) getErrorCause(err error) error {
|
||||
causeable, ok := err.(PkgErrorCauseable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return causeable.Cause()
|
||||
}
|
||||
|
||||
// getErrorStackTrace will try to extract stacktrace from error using StackTrace method (default errors doesn't have it)
|
||||
func (b *PkgErrorsBuilder) getErrorStack(err error) pkgErrors.StackTrace {
|
||||
traceable, ok := err.(PkgErrorTraceable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return traceable.StackTrace()
|
||||
}
|
101
core/stacktrace/pkg_errors_builder_test.go
Normal file
101
core/stacktrace/pkg_errors_builder_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// errorWithCause has Cause() method, but doesn't have StackTrace() method
|
||||
type errorWithCause struct {
|
||||
msg string
|
||||
cause error
|
||||
}
|
||||
|
||||
func newErrorWithCause(msg string, cause error) error {
|
||||
return &errorWithCause{
|
||||
msg: msg,
|
||||
cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *errorWithCause) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *errorWithCause) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
type PkgErrorsStackProviderSuite struct {
|
||||
transformer *PkgErrorsStackTransformer
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (s *PkgErrorsStackProviderSuite) SetupSuite() {
|
||||
s.transformer = &PkgErrorsStackTransformer{}
|
||||
}
|
||||
|
||||
func (s *PkgErrorsStackProviderSuite) Test_Nil() {
|
||||
s.transformer.stack = nil
|
||||
assert.Empty(s.T(), s.transformer.Stack())
|
||||
}
|
||||
|
||||
func (s *PkgErrorsStackProviderSuite) Test_Empty() {
|
||||
s.transformer.stack = pkgErrors.StackTrace{}
|
||||
assert.Empty(s.T(), s.transformer.Stack())
|
||||
}
|
||||
|
||||
func (s *PkgErrorsStackProviderSuite) Test_Full() {
|
||||
testErr := pkgErrors.New("test")
|
||||
s.transformer.stack = testErr.(PkgErrorTraceable).StackTrace()
|
||||
assert.NotEmpty(s.T(), s.transformer.Stack())
|
||||
}
|
||||
|
||||
type PkgErrorsBuilderSuite struct {
|
||||
builder *PkgErrorsBuilder
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (s *PkgErrorsBuilderSuite) SetupTest() {
|
||||
s.builder = &PkgErrorsBuilder{}
|
||||
client, _ := raven.New("fake dsn")
|
||||
s.builder.SetClient(client)
|
||||
}
|
||||
|
||||
func (s *PkgErrorsBuilderSuite) Test_Stackless() {
|
||||
s.builder.SetError(errors.New("simple"))
|
||||
stack, err := s.builder.Build().GetResult()
|
||||
require.Error(s.T(), err)
|
||||
assert.Equal(s.T(), ErrUnfeasibleBuilder, err)
|
||||
assert.Empty(s.T(), stack)
|
||||
}
|
||||
|
||||
func (s *PkgErrorsBuilderSuite) Test_WithStack() {
|
||||
s.builder.SetError(pkgErrors.New("with stack"))
|
||||
stack, err := s.builder.Build().GetResult()
|
||||
require.NoError(s.T(), err)
|
||||
require.NotEmpty(s.T(), stack)
|
||||
assert.NotEmpty(s.T(), stack.Frames)
|
||||
}
|
||||
|
||||
func (s *PkgErrorsBuilderSuite) Test_CauseWithStack() {
|
||||
s.builder.SetError(newErrorWithCause("cause with stack", pkgErrors.New("with stack")))
|
||||
stack, err := s.builder.Build().GetResult()
|
||||
require.NoError(s.T(), err)
|
||||
require.NotEmpty(s.T(), stack)
|
||||
assert.NotEmpty(s.T(), stack.Frames)
|
||||
}
|
||||
|
||||
func TestPkgErrorsStackProvider(t *testing.T) {
|
||||
suite.Run(t, new(PkgErrorsStackProviderSuite))
|
||||
}
|
||||
|
||||
func TestPkgErrorsBuilder(t *testing.T) {
|
||||
suite.Run(t, new(PkgErrorsBuilderSuite))
|
||||
}
|
10
core/stacktrace/raven_client_interface.go
Normal file
10
core/stacktrace/raven_client_interface.go
Normal file
@ -0,0 +1,10 @@
|
||||
package stacktrace
|
||||
|
||||
import "github.com/getsentry/raven-go"
|
||||
|
||||
// RavenClientInterface includes all necessary calls from *raven.Client. Therefore, it can be mocked or replaced.
|
||||
type RavenClientInterface interface {
|
||||
CaptureMessageAndWait(message string, tags map[string]string, interfaces ...raven.Interface) string
|
||||
CaptureErrorAndWait(err error, tags map[string]string, interfaces ...raven.Interface) string
|
||||
IncludePaths() []string
|
||||
}
|
69
core/stacktrace/raven_stacktrace_builder.go
Normal file
69
core/stacktrace/raven_stacktrace_builder.go
Normal file
@ -0,0 +1,69 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
)
|
||||
|
||||
// Frame is a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// Stacktrace is stack of Frames
|
||||
type Stacktrace []Frame
|
||||
|
||||
// RavenStackTransformer is an interface for any component, which will transform some unknown stacktrace data to stacktrace.Stacktrace
|
||||
type RavenStackTransformer interface {
|
||||
Stack() Stacktrace
|
||||
}
|
||||
|
||||
// RavenStacktraceBuilder builds *raven.Stacktrace for any generic stack data
|
||||
type RavenStacktraceBuilder struct {
|
||||
transformer RavenStackTransformer
|
||||
}
|
||||
|
||||
// NewRavenStacktraceBuilder is a RavenStacktraceBuilder constructor
|
||||
func NewRavenStacktraceBuilder(p RavenStackTransformer) *RavenStacktraceBuilder {
|
||||
return (&RavenStacktraceBuilder{}).SetTransformer(p)
|
||||
}
|
||||
|
||||
// SetTransformer sets stack transformer into stacktrace builder
|
||||
func (b *RavenStacktraceBuilder) SetTransformer(p RavenStackTransformer) *RavenStacktraceBuilder {
|
||||
b.transformer = p
|
||||
return b
|
||||
}
|
||||
|
||||
// Build converts generic stacktrace to to github.com/getsentry/raven-go.Stacktrace
|
||||
func (b *RavenStacktraceBuilder) Build(context int, appPackagePrefixes []string) *raven.Stacktrace {
|
||||
// This code is borrowed from github.com/getsentry/raven-go.NewStacktrace().
|
||||
var frames []*raven.StacktraceFrame
|
||||
for _, f := range b.transformer.Stack() {
|
||||
frame := b.convertFrame(f, context, appPackagePrefixes)
|
||||
if frame != nil {
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
}
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
||||
frames[i], frames[j] = frames[j], frames[i]
|
||||
}
|
||||
return &raven.Stacktrace{Frames: frames}
|
||||
}
|
||||
|
||||
// convertFrame converts single generic stacktrace frame to github.com/pkg/errors.Frame
|
||||
func (b *RavenStacktraceBuilder) convertFrame(f Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame {
|
||||
// This code is borrowed from github.com/pkg/errors.Frame.
|
||||
pc := uintptr(f) - 1
|
||||
fn := runtime.FuncForPC(pc)
|
||||
var file string
|
||||
var line int
|
||||
if fn != nil {
|
||||
file, line = fn.FileLine(pc)
|
||||
} else {
|
||||
file = "unknown"
|
||||
}
|
||||
return raven.NewStacktraceFrame(pc, path.Dir(file), file, line, context, appPackagePrefixes)
|
||||
}
|
55
core/stacktrace/raven_stacktrace_builder_test.go
Normal file
55
core/stacktrace/raven_stacktrace_builder_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ravenMockTransformer struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (r *ravenMockTransformer) Stack() Stacktrace {
|
||||
args := r.Called()
|
||||
return args.Get(0).(Stacktrace)
|
||||
}
|
||||
|
||||
type RavenStacktraceBuilderSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (s *RavenStacktraceBuilderSuite) callers() Stacktrace {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
st := make(Stacktrace, n)
|
||||
for i := 0; i < n; i++ {
|
||||
st[i] = Frame(pcs[i])
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func (s *RavenStacktraceBuilderSuite) Test_BuildEmpty() {
|
||||
testTransformer := new(ravenMockTransformer)
|
||||
testTransformer.On("Stack", mock.Anything).Return(Stacktrace{})
|
||||
|
||||
assert.Nil(s.T(), NewRavenStacktraceBuilder(testTransformer).Build(3, []string{}))
|
||||
}
|
||||
|
||||
func (s *RavenStacktraceBuilderSuite) Test_BuildActual() {
|
||||
testTransformer := new(ravenMockTransformer)
|
||||
testTransformer.On("Stack", mock.Anything).Return(s.callers())
|
||||
stack := NewRavenStacktraceBuilder(testTransformer).Build(3, []string{})
|
||||
|
||||
require.NotNil(s.T(), stack)
|
||||
assert.NotEmpty(s.T(), stack.Frames)
|
||||
}
|
||||
|
||||
func TestRavenStacktraceBuilder(t *testing.T) {
|
||||
suite.Run(t, new(RavenStacktraceBuilderSuite))
|
||||
}
|
22
core/stacktrace/stack_builder_factory.go
Normal file
22
core/stacktrace/stack_builder_factory.go
Normal file
@ -0,0 +1,22 @@
|
||||
package stacktrace
|
||||
|
||||
// GetStackBuilderByErrorType tries to guess which stacktrace builder would be feasible for passed error.
|
||||
// For example, errors from github.com/pkg/errors have StackTrace() method, and Go 1.13 errors can be unwrapped.
|
||||
func GetStackBuilderByErrorType(err error) StackBuilderInterface {
|
||||
if isPkgErrors(err) {
|
||||
return &PkgErrorsBuilder{AbstractStackBuilder{err: err}}
|
||||
}
|
||||
|
||||
if _, ok := err.(Unwrappable); ok {
|
||||
return &UnwrapBuilder{AbstractStackBuilder{err: err}}
|
||||
}
|
||||
|
||||
return &GenericStackBuilder{AbstractStackBuilder{err: err}}
|
||||
}
|
||||
|
||||
// isPkgErrors returns true if passed error might be github.com/pkg/errors error
|
||||
func isPkgErrors(err error) bool {
|
||||
_, okTraceable := err.(PkgErrorTraceable)
|
||||
_, okCauseable := err.(PkgErrorCauseable)
|
||||
return okTraceable || okCauseable
|
||||
}
|
27
core/stacktrace/stack_builder_factory_test.go
Normal file
27
core/stacktrace/stack_builder_factory_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetStackBuilderByErrorType_PkgErrors(t *testing.T) {
|
||||
testErr := pkgErrors.New("pkg/errors err")
|
||||
builder := GetStackBuilderByErrorType(testErr)
|
||||
assert.IsType(t, &PkgErrorsBuilder{}, builder)
|
||||
}
|
||||
|
||||
func TestGetStackBuilderByErrorType_UnwrapBuilder(t *testing.T) {
|
||||
testErr := newWrappableError("first", newWrappableError("second", errors.New("third")))
|
||||
builder := GetStackBuilderByErrorType(testErr)
|
||||
assert.IsType(t, &UnwrapBuilder{}, builder)
|
||||
}
|
||||
|
||||
func TestGetStackBuilderByErrorType_Generic(t *testing.T) {
|
||||
defaultErr := errors.New("default err")
|
||||
builder := GetStackBuilderByErrorType(defaultErr)
|
||||
assert.IsType(t, &GenericStackBuilder{}, builder)
|
||||
}
|
56
core/stacktrace/unwrap_builder.go
Normal file
56
core/stacktrace/unwrap_builder.go
Normal file
@ -0,0 +1,56 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"github.com/getsentry/raven-go"
|
||||
)
|
||||
|
||||
// Unwrappable is the interface for errors with Unwrap() method
|
||||
type Unwrappable interface {
|
||||
Unwrap() error
|
||||
}
|
||||
|
||||
// UnwrapBuilder builds stacktrace from the chain of wrapped errors
|
||||
type UnwrapBuilder struct {
|
||||
AbstractStackBuilder
|
||||
}
|
||||
|
||||
// Build stacktrace
|
||||
func (b *UnwrapBuilder) Build() StackBuilderInterface {
|
||||
if _, ok := b.err.(Unwrappable); !ok {
|
||||
b.buildErr = ErrUnfeasibleBuilder
|
||||
return b
|
||||
}
|
||||
|
||||
err := b.err
|
||||
frames := []*raven.StacktraceFrame{}
|
||||
|
||||
for err != nil {
|
||||
frames = append(frames, raven.NewStacktraceFrame(
|
||||
0,
|
||||
"<message>: "+err.Error(),
|
||||
"<wrapped>",
|
||||
0,
|
||||
3,
|
||||
b.client.IncludePaths(),
|
||||
))
|
||||
|
||||
if item, ok := err.(Unwrappable); ok {
|
||||
err = item.Unwrap()
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(frames) <= 1 {
|
||||
b.buildErr = ErrUnfeasibleBuilder
|
||||
return b
|
||||
}
|
||||
|
||||
// Sentry wants the frames with the oldest first, so reverse them
|
||||
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
||||
frames[i], frames[j] = frames[j], frames[i]
|
||||
}
|
||||
|
||||
b.stack = &raven.Stacktrace{Frames: frames}
|
||||
return b
|
||||
}
|
88
core/stacktrace/unwrap_builder_test.go
Normal file
88
core/stacktrace/unwrap_builder_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// simpleError is a simplest error implementation possible. The only reason why it's here is tests.
|
||||
type simpleError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func newSimpleError(msg string) error {
|
||||
return &simpleError{msg: msg}
|
||||
}
|
||||
|
||||
func (n *simpleError) Error() string {
|
||||
return n.msg
|
||||
}
|
||||
|
||||
// wrappableError is a simple implementation of wrappable error
|
||||
type wrappableError struct {
|
||||
msg string
|
||||
err error
|
||||
}
|
||||
|
||||
func newWrappableError(msg string, child error) error {
|
||||
return &wrappableError{msg: msg, err: child}
|
||||
}
|
||||
|
||||
func (e *wrappableError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *wrappableError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
type UnwrapBuilderSuite struct {
|
||||
builder *UnwrapBuilder
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestUnwrapBuilder(t *testing.T) {
|
||||
suite.Run(t, new(UnwrapBuilderSuite))
|
||||
}
|
||||
|
||||
func (s *UnwrapBuilderSuite) SetupTest() {
|
||||
client, _ := raven.New("fake dsn")
|
||||
s.builder = &UnwrapBuilder{}
|
||||
s.builder.SetClient(client)
|
||||
}
|
||||
|
||||
func (s *UnwrapBuilderSuite) TestBuild_Nil() {
|
||||
stack, err := s.builder.Build().GetResult()
|
||||
require.Error(s.T(), err)
|
||||
if stack != nil {
|
||||
assert.Empty(s.T(), stack.Frames)
|
||||
}
|
||||
assert.Equal(s.T(), ErrUnfeasibleBuilder, err)
|
||||
}
|
||||
|
||||
func (s *UnwrapBuilderSuite) TestBuild_NoUnwrap() {
|
||||
s.builder.SetError(newSimpleError("fake"))
|
||||
stack, buildErr := s.builder.Build().GetResult()
|
||||
require.Error(s.T(), buildErr)
|
||||
require.Equal(s.T(), ErrUnfeasibleBuilder, buildErr)
|
||||
assert.Empty(s.T(), stack)
|
||||
}
|
||||
|
||||
func (s *UnwrapBuilderSuite) TestBuild_WrappableHasWrapped() {
|
||||
testErr := newWrappableError("first", newWrappableError("second", errors.New("third")))
|
||||
_, ok := testErr.(Unwrappable)
|
||||
require.True(s.T(), ok)
|
||||
|
||||
s.builder.SetError(testErr)
|
||||
stack, buildErr := s.builder.Build().GetResult()
|
||||
require.NoError(s.T(), buildErr)
|
||||
require.NotNil(s.T(), stack)
|
||||
require.NotNil(s.T(), stack.Frames)
|
||||
assert.NotEmpty(s.T(), stack.Frames)
|
||||
assert.True(s.T(), len(stack.Frames) > 1)
|
||||
}
|
2
go.mod
2
go.mod
@ -33,7 +33,7 @@ require (
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
|
||||
golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/go-playground/validator.v9 v9.30.2 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
||||
|
29
go.sum
29
go.sum
@ -48,20 +48,14 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
@ -87,8 +81,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
@ -103,6 +95,7 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
@ -139,10 +132,6 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
@ -157,8 +146,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
@ -167,9 +154,6 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
@ -298,10 +282,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200428200454-593003d681fa h1:yMbJOvnfYkO1dSAviTu/ZguZWLBTXx4xE3LYrxUCCiA=
|
||||
@ -321,6 +301,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
@ -348,18 +329,12 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/go-playground/validator.v9 v9.30.2 h1:icxYLlYflpazIV3ufMoNB9h9SYMQ37DZ8CTwkU4pnOs=
|
||||
gopkg.in/go-playground/validator.v9 v9.30.2/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
|
||||
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
Loading…
Reference in New Issue
Block a user