diff --git a/CHANGELOG.md b/CHANGELOG.md index d58e46161..b5e8cba57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.0.1-beta.4 - feat: `IgnoreErrors` client option and corresponding integration +- ref: Reworked `net/http` integration, wrote better example and complete readme - ref: Reworked `Gin` integration, wrote better example and complete readme - ref: Reworked `Iris` integration, wrote better example and complete readme - ref: Reworked `Negroni` integration, wrote better example and complete readme diff --git a/example/http/main.go b/example/http/main.go index f402e0a5b..a86587d85 100644 --- a/example/http/main.go +++ b/example/http/main.go @@ -1,133 +1,63 @@ package main import ( - "context" - "encoding/json" - "errors" "fmt" - "log" "net/http" - "strconv" - "time" "github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http" ) -func prettyPrint(v interface{}) string { - pp, _ := json.MarshalIndent(v, "", " ") - return string(pp) -} - -type ctxKey int - -const UserCtxKey = ctxKey(1337) - -type devNullTransport struct{} - -func (t *devNullTransport) Configure(options sentry.ClientOptions) { - dsn, _ := sentry.NewDsn(options.Dsn) - fmt.Println() - fmt.Println("Store Endpoint:", dsn.StoreAPIURL()) - fmt.Println("Headers:", dsn.RequestHeaders()) - fmt.Println() -} -func (t *devNullTransport) SendEvent(event *sentry.Event) { - fmt.Println("Faked Transport") - log.Println(prettyPrint(event)) -} - -func (t *devNullTransport) Flush(timeout time.Duration) bool { - return true -} - -func customHandlerFunc(w http.ResponseWriter, r *http.Request) { - if sentry.HasHubOnContext(r.Context()) { - hub := sentry.GetHubFromContext(r.Context()) - hub.AddBreadcrumb(&sentry.Breadcrumb{Message: "BreadcrumbFunc #1 - " + strconv.Itoa(int(time.Now().Unix()))}, nil) - hub.AddBreadcrumb(&sentry.Breadcrumb{Message: "BreadcrumbFunc #2 - " + strconv.Itoa(int(time.Now().Unix()))}, nil) - } - - panic(errors.New("HTTPPanicHandler Error")) -} - -type User struct { - id int - name string -} +type handler struct{} -func attachUser(handler http.HandlerFunc) http.HandlerFunc { - return func(response http.ResponseWriter, request *http.Request) { - ctx := request.Context() - ctx = context.WithValue(ctx, UserCtxKey, User{ - id: 42, - name: "PickleRick", +func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + if hub := sentry.GetHubFromContext(r.Context()); hub != nil { + hub.WithScope(func(scope *sentry.Scope) { + scope.SetExtra("unwantedQuery", "someQueryDataMaybe") + hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") }) - handler(response, request.WithContext(ctx)) } + rw.WriteHeader(http.StatusOK) } -type customHandler struct{} - -func (th *customHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if sentry.HasHubOnContext(r.Context()) { - hub := sentry.GetHubFromContext(r.Context()) - hub.AddBreadcrumb(&sentry.Breadcrumb{Message: "Breadcrumb #1 - " + strconv.Itoa(int(time.Now().Unix()))}, nil) - hub.AddBreadcrumb(&sentry.Breadcrumb{Message: "Breadcrumb #2 - " + strconv.Itoa(int(time.Now().Unix()))}, nil) - } - - sentry.CaptureMessage("CaptureMessage") - sentry.CaptureException(errors.New("CaptureMessage")) - panic("HTTPPanicHandler Message") -} - -type extractUser struct{} - -func (eu extractUser) Name() string { - return "extractUser" -} - -func (eu extractUser) SetupOnce(client *sentry.Client) { - client.AddEventProcessor(eu.processor) -} - -func (eu extractUser) processor(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { - if hint != nil && hint.Context != nil { - if u, ok := hint.Context.Value(UserCtxKey).(User); ok { - event.User = sentry.User{ - ID: strconv.Itoa(u.id), - Username: u.name, - } +func enhanceSentryEvent(handler http.HandlerFunc) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if hub := sentry.GetHubFromContext(r.Context()); hub != nil { + hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") } + handler(rw, r) } - - return event } func main() { - err := sentry.Init(sentry.ClientOptions{ - Dsn: "https://hello@world.io/1337", - Transport: new(devNullTransport), - Integrations: func(i []sentry.Integration) []sentry.Integration { - return append(i, new(extractUser)) + _ = sentry.Init(sentry.ClientOptions{ + Dsn: "https://363a337c11a64611be4845ad6e24f3ac@sentry.io/297378", + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + if hint.Context != nil { + if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok { + // You have access to the original Request + fmt.Println(req) + } + } + fmt.Println(event) + return event }, + Debug: true, + AttachStacktrace: true, }) - if err != nil { - panic(err) - } else { - fmt.Print("[Sentry] SDK initialized successfully\n\n") - } - sentryHandler := sentryhttp.New(sentryhttp.Options{ - Repanic: true, - WaitForDelivery: true, + Repanic: true, }) - http.Handle("/handle", sentryHandler.Handle(&customHandler{})) - http.HandleFunc("/handlefunc", attachUser(sentryHandler.HandleFunc(customHandlerFunc))) + http.Handle("/", sentryHandler.Handle(&handler{})) + http.HandleFunc("/foo", sentryHandler.HandleFunc( + enhanceSentryEvent(func(rw http.ResponseWriter, r *http.Request) { + panic("y tho") + }), + )) - log.Println("Please call me at localhost:3000/handle or localhost:3000/handlefunc") + fmt.Println("Listening and serving HTTP on :3000") if err := http.ListenAndServe(":3000", nil); err != nil { panic(err) diff --git a/gin/sentrygin.go b/gin/sentrygin.go index 8ae9e0f37..2d8c1e959 100644 --- a/gin/sentrygin.go +++ b/gin/sentrygin.go @@ -72,7 +72,7 @@ func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { } } -// GetHubFromContext retrieves attached *sentry.Hub instance from iris.Context +// GetHubFromContext retrieves attached *sentry.Hub instance from iris.Context. func GetHubFromContext(ctx *gin.Context) *sentry.Hub { if hub, ok := ctx.Get(valuesKey); ok { if hub, ok := hub.(*sentry.Hub); ok { diff --git a/http/README.md b/http/README.md new file mode 100644 index 000000000..905b8f048 --- /dev/null +++ b/http/README.md @@ -0,0 +1,135 @@ +

+ + + +
+

+ +# Official Sentry net/http Handler for Sentry-go SDK + +**Godoc:** https://godoc.org/github.com/getsentry/sentry-go/http + +**Example:** https://github.com/getsentry/sentry-go/tree/master/example/http + +## Installation + +```sh +go get github.com/getsentry/sentry-go/http +``` + +```go +import ( + "fmt" + "net/http" + + "github.com/getsentry/sentry-go" + sentryhttp "github.com/getsentry/sentry-go/http" +) + +// In order to initialize Sentry's handler, you need to initialize Sentry itself beforehand +if err := sentry.Init(sentry.ClientOptions{ + Dsn: "your-public-dsn", +}); err != nil { + fmt.Printf("Sentry initialization failed: %v\n", err) +} + +// Create an instance of sentryhttp +sentryHandler := sentryhttp.New(sentryhttp.Options{}) + +// Once it's done, you can setup routes and attach the handler as one of your middlewares +http.Handle("/", sentryHandler.Handle(&handler{})) +http.HandleFunc("/foo", sentryHandler.HandleFunc(func(rw http.ResponseWriter, r *http.Request) { + panic("y tho") +})) + +fmt.Println("Listening and serving HTTP on :3000") + +// And run it +if err := http.ListenAndServe(":3000", nil); err != nil { + panic(err) +} +``` + +## Configuration + +`sentryhttp` accepts a struct of `Options` that allows you to configure how the handler will behave. + +Currently it respects 3 options: + +```go +// Whether Sentry should repanic after recovery, in most cases it should be set to true, +// and you should gracefully handle http responses. +Repanic bool +// Whether you want to block the request before moving forward with the response. +// Useful, when you want to restart the process after it panics. +WaitForDelivery bool +// Timeout for the event delivery requests. +Timeout time.Duration +``` + +## Usage + +`sentryhttp` attaches an instance of `*sentry.Hub` (https://godoc.org/github.com/getsentry/sentry-go#Hub) to the request's context, which makes it available throughout the rest of request's lifetime. +You can access it by using `sentry.GetHubFromContext()` method on the request itself in any of your proceeding middlewares and routes. +And it should be used instead of global `sentry.CaptureMessage`, `sentry.CaptureException` or any other calls, as it keeps the separation of data between the requests. + +**Keep in mind that `*sentry.Hub` won't be available in middlewares attached prior to `sentryhttp`!** + +```go +type handler struct{} + +func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + if hub := sentry.GetHubFromContext(r.Context()); hub != nil { + hub.WithScope(func(scope *sentry.Scope) { + scope.SetExtra("unwantedQuery", "someQueryDataMaybe") + hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") + }) + } + rw.WriteHeader(http.StatusOK) +} + +func enhanceSentryEvent(handler http.HandlerFunc) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if hub := sentry.GetHubFromContext(r.Context()); hub != nil { + hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") + } + handler(rw, r) + } +} + +// Later in the code + +sentryHandler := sentryhttp.New(sentryhttp.Options{ + Repanic: true, +}) + +http.Handle("/", sentryHandler.Handle(&handler{})) +http.HandleFunc("/foo", sentryHandler.HandleFunc( + enhanceSentryEvent(func(rw http.ResponseWriter, r *http.Request) { + panic("y tho") + }), +)) + +fmt.Println("Listening and serving HTTP on :3000") + +if err := http.ListenAndServe(":3000", nil); err != nil { + panic(err) +} +``` + +### Accessing Request in `BeforeSend` callback + +```go +sentry.Init(sentry.ClientOptions{ + Dsn: "your-public-dsn", + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + if hint.Context != nil { + if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok { + // You have access to the orihttpal Request here + } + } + + return event + }, +}) +``` diff --git a/http/sentryhttp.go b/http/sentryhttp.go index af8627c2d..71a6568fa 100644 --- a/http/sentryhttp.go +++ b/http/sentryhttp.go @@ -8,20 +8,28 @@ import ( "github.com/getsentry/sentry-go" ) -type Handler struct { +type handler struct { repanic bool waitForDelivery bool timeout time.Duration } type Options struct { - Repanic bool + // Repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true, + // as iris.Default includes it's own Recovery middleware what handles http responses. + Repanic bool + // WaitForDelivery configures whether you want to block the request before moving forward with the response. + // Because Iris's default `Recovery` handler doesn't restart the application, + // it's safe to either skip this option or set it to `false`. WaitForDelivery bool - Timeout time.Duration + // Timeout for the event delivery requests. + Timeout time.Duration } -func New(options Options) *Handler { - handler := Handler{ +// New returns a struct that provides Handle and HandleFunc methods +// that satisfy http.Handler and http.HandlerFunc interfaces. +func New(options Options) *handler { + handler := handler{ repanic: false, timeout: time.Second * 2, waitForDelivery: false, @@ -38,35 +46,39 @@ func New(options Options) *Handler { return &handler } -func (h *Handler) Handle(handler http.Handler) http.Handler { +// Handle wraps http.Handler and recovers from caught panics. +func (h *handler) Handle(handler http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + hub := sentry.CurrentHub().Clone() ctx := sentry.SetHubOnContext( - context.WithValue(r.Context(), sentry.RequestContextKey, r), - sentry.CurrentHub().Clone(), + r.Context(), + hub, ) - defer h.recoverWithSentry(ctx, r) + defer h.recoverWithSentry(hub, r) handler.ServeHTTP(rw, r.WithContext(ctx)) }) } -func (h *Handler) HandleFunc(handler http.HandlerFunc) http.HandlerFunc { +// HandleFunc wraps http.HandleFunc and recovers from caught panics. +func (h *handler) HandleFunc(handler http.HandlerFunc) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { + hub := sentry.CurrentHub().Clone() ctx := sentry.SetHubOnContext( - context.WithValue(r.Context(), sentry.RequestContextKey, r), - sentry.CurrentHub().Clone(), + r.Context(), + hub, ) - defer h.recoverWithSentry(ctx, r) + defer h.recoverWithSentry(hub, r) handler(rw, r.WithContext(ctx)) } } -func (h *Handler) recoverWithSentry(ctx context.Context, r *http.Request) { +func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { if err := recover(); err != nil { - hub := sentry.GetHubFromContext(ctx) - hub.ConfigureScope(func(scope *sentry.Scope) { - scope.SetRequest(sentry.Request{}.FromHTTPRequest(r)) - }) - eventID := hub.RecoverWithContext(ctx, err) + hub.Scope().SetRequest(sentry.Request{}.FromHTTPRequest(r)) + eventID := hub.RecoverWithContext( + context.WithValue(r.Context(), sentry.RequestContextKey, r), + err, + ) if eventID != nil && h.waitForDelivery { hub.Flush(h.timeout) } diff --git a/iris/sentryiris.go b/iris/sentryiris.go index 89463102f..47e0270ab 100644 --- a/iris/sentryiris.go +++ b/iris/sentryiris.go @@ -72,7 +72,7 @@ func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { } } -// GetHubFromContext retrieves attached *sentry.Hub instance from iris.Context +// GetHubFromContext retrieves attached *sentry.Hub instance from iris.Context. func GetHubFromContext(ctx iris.Context) *sentry.Hub { if hub, ok := ctx.Values().Get(valuesKey).(*sentry.Hub); ok { return hub