diff --git a/core/middleware/sentry.go b/core/middleware/sentry.go new file mode 100644 index 0000000..2152efc --- /dev/null +++ b/core/middleware/sentry.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "github.com/getsentry/sentry-go" + "github.com/gin-gonic/gin" +) + +var ginContextSentryKey = "sentry" + +type Sentry interface { + CaptureException(c *gin.Context, exception error) + CaptureMessage(c *gin.Context, message string) + CaptureEvent(c *gin.Context, event *sentry.Event) +} + +func InjectSentry(sentry Sentry) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set(ginContextSentryKey, sentry) + } +} + +func GetSentry(c *gin.Context) (Sentry, bool) { + sentryValue, ok := c.Get(ginContextSentryKey) + if !ok { + return nil, false + } + obj, ok := sentryValue.(Sentry) + if !ok || obj == nil { + return nil, false + } + return obj, true +} + +func MustGetSentry(c *gin.Context) Sentry { + if obj, ok := GetSentry(c); ok && obj != nil { + return obj + } + panic("obj not found in context") +} + +func CaptureException(c *gin.Context, exception error) { + obj, found := GetSentry(c) + if !found { + return + } + obj.CaptureException(c, exception) +} + +func CaptureEvent(c *gin.Context, event *sentry.Event) { + obj, found := GetSentry(c) + if !found { + return + } + obj.CaptureEvent(c, event) +} + +func CaptureMessage(c *gin.Context, message string) { + obj, found := GetSentry(c) + if !found { + return + } + obj.CaptureMessage(c, message) +} diff --git a/core/middleware/sentry_test.go b/core/middleware/sentry_test.go new file mode 100644 index 0000000..42b9f01 --- /dev/null +++ b/core/middleware/sentry_test.go @@ -0,0 +1,105 @@ +package middleware + +import ( + "errors" + "github.com/getsentry/sentry-go" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "testing" +) + +type SentryMiddlewaresTestSuite struct { + suite.Suite +} + +func TestSentryMiddlewares(t *testing.T) { + suite.Run(t, new(SentryMiddlewaresTestSuite)) +} + +func (s *SentryMiddlewaresTestSuite) ctx(mock Sentry) *gin.Context { + ctx := &gin.Context{} + InjectSentry(mock)(ctx) + return ctx +} + +func (s *SentryMiddlewaresTestSuite) TestGetSentry_Empty() { + item, found := GetSentry(&gin.Context{}) + s.Assert().False(found) + s.Assert().Nil(item) + + item, found = GetSentry(&gin.Context{ + Keys: map[string]interface{}{ + ginContextSentryKey: &gin.Engine{}, + }, + }) + s.Assert().False(found) + s.Assert().Nil(item) +} + +func (s *SentryMiddlewaresTestSuite) TestMustGetSentry_Empty() { + s.Assert().Panics(func() { + MustGetSentry(&gin.Context{}) + }) +} + +func (s *SentryMiddlewaresTestSuite) TestGetSentry_Success() { + item, found := GetSentry(&gin.Context{ + Keys: map[string]interface{}{ + ginContextSentryKey: &sentryMock{}, + }, + }) + s.Assert().True(found) + s.Assert().NotNil(item) +} + +func (s *SentryMiddlewaresTestSuite) TestMustGetSentry_Success() { + s.Assert().NotPanics(func() { + item := MustGetSentry(&gin.Context{ + Keys: map[string]interface{}{ + ginContextSentryKey: &sentryMock{}, + }, + }) + s.Assert().NotNil(item) + }) +} + +func (s *SentryMiddlewaresTestSuite) TestCaptureException() { + err := errors.New("test error") + item := &sentryMock{} + item.On("CaptureException", mock.AnythingOfType("*gin.Context"), err).Return() + CaptureException(s.ctx(item), err) + item.AssertExpectations(s.T()) +} + +func (s *SentryMiddlewaresTestSuite) TestCaptureMessage() { + msg := "test error" + item := &sentryMock{} + item.On("CaptureMessage", mock.AnythingOfType("*gin.Context"), msg).Return() + CaptureMessage(s.ctx(item), msg) + item.AssertExpectations(s.T()) +} + +func (s *SentryMiddlewaresTestSuite) TestCaptureEvent() { + event := &sentry.Event{EventID: "1"} + item := &sentryMock{} + item.On("CaptureEvent", mock.AnythingOfType("*gin.Context"), event).Return() + CaptureEvent(s.ctx(item), event) + item.AssertExpectations(s.T()) +} + +type sentryMock struct { + mock.Mock +} + +func (s *sentryMock) CaptureException(c *gin.Context, exception error) { + s.Called(c, exception) +} + +func (s *sentryMock) CaptureMessage(c *gin.Context, message string) { + s.Called(c, message) +} + +func (s *sentryMock) CaptureEvent(c *gin.Context, event *sentry.Event) { + s.Called(c, event) +} diff --git a/core/sentry.go b/core/sentry.go index 9f6763f..f2bd1c1 100644 --- a/core/sentry.go +++ b/core/sentry.go @@ -126,7 +126,24 @@ func (s *Sentry) CaptureException(c *gin.Context, exception error) { hub.CaptureException(exception) return } - _ = c.Error(exception) +} + +// CaptureMessage and send it to Sentry. +func (s *Sentry) CaptureMessage(c *gin.Context, message string) { + if hub := sentrygin.GetHubFromContext(c); hub != nil { + s.setScopeTags(c, hub.Scope()) + hub.CaptureMessage(message) + return + } +} + +// CaptureEvent and send it to Sentry. +func (s *Sentry) CaptureEvent(c *gin.Context, event *sentry.Event) { + if hub := sentrygin.GetHubFromContext(c); hub != nil { + s.setScopeTags(c, hub.Scope()) + hub.CaptureEvent(event) + return + } } // SentryMiddlewares contain all the middlewares required to process errors and panics and send them to the Sentry. @@ -201,12 +218,14 @@ func (s *Sentry) exceptionCaptureMiddleware() gin.HandlerFunc { // nolint:gocogn for _, err := range publicErrors { messages[index] = err.Error() s.CaptureException(c, err) + _ = c.Error(err) l.Error(err.Error()) index++ } for _, err := range privateErrors { s.CaptureException(c, err) + _ = c.Error(err) l.Error(err.Error()) } diff --git a/core/sentry_test.go b/core/sentry_test.go index 2857725..ca35a24 100644 --- a/core/sentry_test.go +++ b/core/sentry_test.go @@ -256,7 +256,7 @@ func (s *SentryTest) TestSentry_CaptureException_Error() { s.sentry.CaptureException(ctx, errors.New("test error")) s.Require().Nil(transport.lastEvent) - s.Require().Len(ctx.Errors, 1) + s.Require().Len(ctx.Errors, 0) } func (s *SentryTest) TestSentry_CaptureException() { @@ -275,6 +275,38 @@ func (s *SentryTest) TestSentry_CaptureException() { s.Assert().NotNil(transport.lastEvent.Exception[1].Stacktrace) } +func (s *SentryTest) TestSentry_CaptureEvent_Nil() { + defer func() { + s.Assert().Nil(recover()) + }() + s.sentry.CaptureEvent(&gin.Context{}, nil) +} + +func (s *SentryTest) TestSentry_CaptureEvent_Error() { + ctx, transport := s.ginCtxMock() + ctx.Keys = make(map[string]interface{}) + s.sentry.CaptureEvent(ctx, &sentry.Event{}) + + s.Require().Nil(transport.lastEvent) + s.Require().Len(ctx.Errors, 0) +} + +func (s *SentryTest) TestSentry_CaptureMessage_Empty() { + defer func() { + s.Assert().Nil(recover()) + }() + s.sentry.CaptureMessage(&gin.Context{}, "") +} + +func (s *SentryTest) TestSentry_CaptureMessage_Error() { + ctx, transport := s.ginCtxMock() + ctx.Keys = make(map[string]interface{}) + s.sentry.CaptureMessage(ctx, "") + + s.Require().Nil(transport.lastEvent) + s.Require().Len(ctx.Errors, 0) +} + func (s *SentryTest) TestSentry_obtainErrorLogger_Existing() { ctx, _ := s.ginCtxMock() log := testutil.NewBufferedLogger().ForHandler("component").ForConnection("conn").ForAccount("acc")