diff --git a/crosstest/http_link_test.go b/crosstest/http_link_test.go index 7df9c0354..abb859ae5 100644 --- a/crosstest/http_link_test.go +++ b/crosstest/http_link_test.go @@ -71,13 +71,13 @@ func TestHTTPFamilyIntegrationsLinkManualErrorsLogsMetricsAndPanicsToOTel(t *tes t.Parallel() sentrytest.Run(t, func(t *testing.T, f *sentrytest.Fixture) { const identifier = "gin" - baseCtx := sentry.SetHubOnContext(context.Background(), f.Hub) + baseCtx := f.NewContext(context.Background()) logger := sentry.NewLogger(baseCtx) meter := sentry.NewMeter(baseCtx) gin.SetMode(gin.ReleaseMode) router := gin.New() router.Use(func(c *gin.Context) { - c.Request = c.Request.WithContext(sentry.SetHubOnContext(otelCtx, f.Hub)) + c.Request = c.Request.WithContext(f.NewContext(otelCtx)) c.Next() }) router.Use(sentrygin.New(sentrygin.Options{WaitForDelivery: true})) @@ -97,14 +97,13 @@ func TestHTTPFamilyIntegrationsLinkManualErrorsLogsMetricsAndPanicsToOTel(t *tes t.Parallel() sentrytest.Run(t, func(t *testing.T, f *sentrytest.Fixture) { const identifier = "echo" - baseCtx := sentry.SetHubOnContext(context.Background(), f.Hub) + baseCtx := f.NewContext(context.Background()) logger := sentry.NewLogger(baseCtx) meter := sentry.NewMeter(baseCtx) e := echo.New() e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { - sentryecho.SetHubOnContext(c, f.Hub) - c.SetRequest(c.Request().WithContext(sentry.SetHubOnContext(otelCtx, f.Hub))) + c.SetRequest(c.Request().WithContext(f.NewContext(otelCtx))) return next(c) } }) @@ -126,12 +125,12 @@ func TestHTTPFamilyIntegrationsLinkManualErrorsLogsMetricsAndPanicsToOTel(t *tes t.Parallel() sentrytest.Run(t, func(t *testing.T, f *sentrytest.Fixture) { const identifier = "negroni" - baseCtx := sentry.SetHubOnContext(context.Background(), f.Hub) + baseCtx := f.NewContext(context.Background()) logger := sentry.NewLogger(baseCtx) meter := sentry.NewMeter(baseCtx) n := negroni.New() n.Use(negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - next(w, r.WithContext(sentry.SetHubOnContext(otelCtx, f.Hub))) + next(w, r.WithContext(f.NewContext(otelCtx))) })) n.Use(sentrynegroni.New(sentrynegroni.Options{WaitForDelivery: true})) n.UseHandler(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { @@ -150,12 +149,12 @@ func TestHTTPFamilyIntegrationsLinkManualErrorsLogsMetricsAndPanicsToOTel(t *tes t.Parallel() sentrytest.Run(t, func(t *testing.T, f *sentrytest.Fixture) { const identifier = "iris" - baseCtx := sentry.SetHubOnContext(context.Background(), f.Hub) + baseCtx := f.NewContext(context.Background()) logger := sentry.NewLogger(baseCtx) meter := sentry.NewMeter(baseCtx) app := iris.New() app.Use(func(ctx iris.Context) { - ctx.ResetRequest(ctx.Request().WithContext(sentry.SetHubOnContext(otelCtx, f.Hub))) + ctx.ResetRequest(ctx.Request().WithContext(f.NewContext(otelCtx))) ctx.Next() }) app.Use(sentryiris.New(sentryiris.Options{WaitForDelivery: true})) diff --git a/echo/README.md b/echo/README.md index 612c886f6..46c3febd6 100644 --- a/echo/README.md +++ b/echo/README.md @@ -20,6 +20,7 @@ go get github.com/getsentry/sentry-go/echo ```go import ( "fmt" + "log" "net/http" "github.com/getsentry/sentry-go" @@ -38,7 +39,7 @@ if err := sentry.Init(sentry.ClientOptions{ // Then create your app app := echo.New() -app.Use(middleware.Logger()) +app.Use(middleware.RequestLogger()) app.Use(middleware.Recover()) // Once it's done, you can attach the handler as one of your middleware @@ -50,7 +51,7 @@ app.GET("/", func(ctx *echo.Context) error { }) // And run it -app.Logger.Fatal(app.Start(":3000")) +log.Fatal(app.Start(":3000")) ``` ## Configuration @@ -73,16 +74,15 @@ Timeout time.Duration ## Usage -`sentryecho` attaches an instance of `*sentry.Hub` (https://pkg.go.dev/github.com/getsentry/sentry-go#Hub) to the `echo.Context`, which makes it available throughout the rest of the request's lifetime. -You can access it by using the `sentryecho.GetHubFromContext()` method on the context itself in any of your proceeding middleware and routes. -And it should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests. +`sentryecho` attaches a request-specific `*sentry.Scope` and transaction to the request context. Pass `ctx.Request().Context()` to capture functions such as `sentry.CaptureMessage` and `sentry.CaptureException` so request data, custom scope data, and trace information are applied to the event. +Use `sentry.ScopeFromContext(ctx.Request().Context())` when you need to add data that should be available to captures made during the request. -**Keep in mind that `*sentry.Hub` won't be available in middleware attached before to `sentryecho`!** +**Keep in mind that the request scope won't be available in middleware attached before `sentryecho`!** ```go app := echo.New() -app.Use(middleware.Logger()) +app.Use(middleware.RequestLogger()) app.Use(middleware.Recover()) app.Use(sentryecho.New(sentryecho.Options{ @@ -91,20 +91,15 @@ app.Use(sentryecho.New(sentryecho.Options{ app.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx *echo.Context) error { - if hub := sentryecho.GetHubFromContext(ctx); hub != nil { - hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") - } + sentry.ScopeFromContext(ctx.Request().Context()).SetTag("someRandomTag", "maybeYouNeedIt") return next(ctx) } }) app.GET("/", func(ctx *echo.Context) error { - if hub := sentryecho.GetHubFromContext(ctx); hub != nil { - hub.WithScope(func(scope *sentry.Scope) { - scope.SetTag("unwantedQuery", "someQueryDataMaybe") - hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") - }) - } + scope := sentry.ScopeFromContext(ctx.Request().Context()) + scope.SetTag("unwantedQuery", "someQueryDataMaybe") + sentry.CaptureMessage(ctx.Request().Context(), "User provided unwanted query string, but we recovered just fine") return ctx.String(http.StatusOK, "Hello, World!") }) @@ -114,7 +109,7 @@ app.GET("/foo", func(ctx *echo.Context) error { panic("y tho") }) -app.Logger.Fatal(app.Start(":3000")) +log.Fatal(app.Start(":3000")) ``` ### Accessing Request in `BeforeSend` callback diff --git a/echo/example_test.go b/echo/example_test.go index 5e41eae35..99a86bb9d 100644 --- a/echo/example_test.go +++ b/echo/example_test.go @@ -9,7 +9,7 @@ import ( "github.com/labstack/echo/v5" ) -func ExampleGetSpanFromContext() { +func Example() { router := echo.New() router.Use(sentryecho.New(sentryecho.Options{})) router.GET("/", func(c *echo.Context) error { @@ -20,12 +20,13 @@ func ExampleGetSpanFromContext() { return nil } - // Acquire transaction on current hub that's created by the SDK. - // Be careful, it might be a nil value if you didn't set up sentryecho middleware. - sentrySpan := sentryecho.GetSpanFromContext(c) - // Pass in the `.Context()` method from `*sentry.Span` struct. - // The `context.Context` instance inherits the context from `echo.Context`. - err := expensiveThing(sentrySpan.Context()) + // Acquire the span from the request context. It may be nil if + // you did not set up the sentryecho middleware. + spanContext := c.Request().Context() + if sentrySpan := sentry.SpanFromContext(spanContext); sentrySpan != nil { + spanContext = sentrySpan.Context() + } + err := expensiveThing(spanContext) if err != nil { return err } diff --git a/echo/sentryecho.go b/echo/sentryecho.go index f654ce248..fe04be998 100644 --- a/echo/sentryecho.go +++ b/echo/sentryecho.go @@ -8,6 +8,7 @@ import ( "github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go/internal/debuglog" + "github.com/getsentry/sentry-go/internal/traceutils" "github.com/labstack/echo/v5" ) @@ -15,12 +16,6 @@ const ( // sdkIdentifier is the identifier of the Echo SDK. sdkIdentifier = "sentry.go.echo" - // valuesKey is used as a key to store the Sentry Hub instance on the *echo.Context. - valuesKey = "sentry" - - // transactionKey is used as a key to store the Sentry transaction on the *echo.Context. - transactionKey = "sentry_transaction" - // errorKey is used as a key to store the error on the *echo.Context. errorKey = "error" ) @@ -59,17 +54,11 @@ func New(options Options) echo.MiddlewareFunc { func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx *echo.Context) error { - hub := GetHubFromContext(ctx) - if hub == nil { - hub = sentry.CurrentHub().Clone() - } - - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } - r := ctx.Request() - requestCtx := sentry.SetHubOnContext(r.Context(), hub) + created := sentry.SpanFromContext(r.Context()) == nil + requestCtx, scope := sentry.WithIsolationScope(r.Context()) + + sentry.ClientFromContext(requestCtx).SetSDKIdentifier(sdkIdentifier) transactionName := r.URL.Path transactionSource := sentry.SourceURL @@ -80,7 +69,7 @@ func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc { } options := []sentry.SpanOption{ - sentry.ContinueTrace(r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)), + traceutils.ContinueFromRequest(r), sentry.WithOpName("http.server"), sentry.WithTransactionSource(transactionSource), sentry.WithSpanOrigin(sentry.SpanOriginEcho), @@ -92,34 +81,34 @@ func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc { options..., ) - transaction.SetData("http.request.method", r.Method) + if created { + requestCtx = transaction.Context() + transaction.SetData("http.request.method", r.Method) + defer func() { + var status int + if resp, err := echo.UnwrapResponse(ctx.Response()); err == nil && resp.Status != 0 { + status = resp.Status + } + if err := ctx.Get(errorKey); err != nil { + if coder, ok := err.(echo.HTTPStatusCoder); ok { + status = coder.StatusCode() + } + } - defer func() { - var status int - if resp, err := echo.UnwrapResponse(ctx.Response()); err == nil && resp.Status != 0 { - status = resp.Status - } - if err := ctx.Get(errorKey); err != nil { - if coder, ok := err.(echo.HTTPStatusCoder); ok { - status = coder.StatusCode() + if status == 0 { + debuglog.Printf("sentryecho: unable to determine HTTP response status code") + } else { + transaction.Status = sentry.HTTPtoSpanStatus(status) + transaction.SetData("http.response.status_code", status) } - } - - if status == 0 { - debuglog.Printf("sentryecho: unable to determine HTTP response status code") - } else { - transaction.Status = sentry.HTTPtoSpanStatus(status) - transaction.SetData("http.response.status_code", status) - } - transaction.Finish() - }() - - hub.Scope().SetRequest(r) - ctx.Set(valuesKey, hub) - ctx.Set(transactionKey, transaction) - r = r.WithContext(transaction.Context()) + transaction.Finish() + }() + } + + r = r.WithContext(requestCtx) ctx.SetRequest(r) - defer h.recoverWithSentry(hub, r) + scope.SetRequest(r) + defer h.recoverWithSentry(r) err := next(ctx) if err != nil { @@ -131,39 +120,15 @@ func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc { } } -func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { +func (h *handler) recoverWithSentry(r *http.Request) { if err := recover(); err != nil { - eventID := hub.RecoverWithContext( - context.WithValue(r.Context(), sentry.RequestContextKey, r), - err, - ) + ctx := context.WithValue(r.Context(), sentry.RequestContextKey, r) + eventID := sentry.Recover(ctx, err) if eventID != nil && h.waitForDelivery { - hub.Flush(h.timeout) + sentry.ClientFromContext(ctx).Flush(h.timeout) } if h.repanic { panic(err) } } } - -// GetHubFromContext retrieves attached *sentry.Hub instance from *echo.Context. -func GetHubFromContext(ctx *echo.Context) *sentry.Hub { - if hub, ok := ctx.Get(valuesKey).(*sentry.Hub); ok { - return hub - } - return nil -} - -// SetHubOnContext attaches *sentry.Hub instance to *echo.Context. -func SetHubOnContext(ctx *echo.Context, hub *sentry.Hub) { - ctx.Set(valuesKey, hub) -} - -// GetSpanFromContext retrieves attached *sentry.Span instance from *echo.Context. -// If there is no transaction on *echo.Context, it will return nil. -func GetSpanFromContext(ctx *echo.Context) *sentry.Span { - if span, ok := ctx.Get(transactionKey).(*sentry.Span); ok { - return span - } - return nil -} diff --git a/echo/sentryecho_test.go b/echo/sentryecho_test.go index 521cec111..a60a1848d 100644 --- a/echo/sentryecho_test.go +++ b/echo/sentryecho_test.go @@ -117,12 +117,11 @@ func TestIntegration(t *testing.T) { Body: `{"safe":"value"}`, ContentType: "application/json", Handler: func(c *echo.Context) error { - hub := sentryecho.GetHubFromContext(c) body, err := io.ReadAll(c.Request().Body) if err != nil { t.Error(err) } - hub.CaptureMessage("post: " + string(body)) + sentry.CaptureMessage(c.Request().Context(), "post: "+string(body)) return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) }, WantEvent: &sentry.Event{ @@ -173,8 +172,7 @@ func TestIntegration(t *testing.T) { Method: "GET", WantStatus: 200, Handler: func(c *echo.Context) error { - hub := sentryecho.GetHubFromContext(c) - hub.CaptureMessage("get") + sentry.CaptureMessage(c.Request().Context(), "get") return c.JSON(http.StatusOK, map[string]string{"status": "get"}) }, WantEvent: &sentry.Event{ @@ -220,12 +218,11 @@ func TestIntegration(t *testing.T) { WantStatus: 200, Body: largePayload, Handler: func(c *echo.Context) error { - hub := sentryecho.GetHubFromContext(c) body, err := io.ReadAll(c.Request().Body) if err != nil { t.Error(err) } - hub.CaptureMessage(fmt.Sprintf("post: %d KB", len(body)/1024)) + sentry.CaptureMessage(c.Request().Context(), fmt.Sprintf("post: %d KB", len(body)/1024)) return nil }, WantEvent: &sentry.Event{ @@ -275,8 +272,7 @@ func TestIntegration(t *testing.T) { WantStatus: 200, Body: "client sends, server ignores, SDK doesn't read", Handler: func(c *echo.Context) error { - hub := sentryecho.GetHubFromContext(c) - hub.CaptureMessage("body ignored") + sentry.CaptureMessage(c.Request().Context(), "body ignored") return nil }, WantEvent: &sentry.Event{ @@ -504,25 +500,27 @@ func TestIntegration(t *testing.T) { } } -func TestSetHubOnContext(t *testing.T) { - err := sentry.Init(sentry.ClientOptions{}) +func TestRequestContextState(t *testing.T) { + err := sentry.Init(sentry.ClientOptions{ + EnableTracing: true, + TracesSampleRate: 1.0, + }) if err != nil { t.Fatal(err) } - hub := sentry.CurrentHub().Clone() router := echo.New() - router.GET("/set-hub", func(c *echo.Context) error { - sentryecho.SetHubOnContext(c, hub) - retrievedHub := sentryecho.GetHubFromContext(c) - if retrievedHub == nil { - t.Error("expecting hub to be set on context") + router.GET("/with-span", func(c *echo.Context) error { + scope := sentry.ScopeFromContext(c.Request().Context()) + if scope == nil { + t.Error("expecting scope to not be nil") } - if retrievedHub != hub { - t.Error("expecting retrieved hub to be the same as the set hub") + span := sentry.SpanFromContext(c.Request().Context()) + if span == nil { + t.Error("expecting span to not be nil") } return c.NoContent(http.StatusOK) - }) + }, sentryecho.New(sentryecho.Options{})) srv := httptest.NewServer(router) defer srv.Close() @@ -530,7 +528,7 @@ func TestSetHubOnContext(t *testing.T) { c := srv.Client() c.Timeout = time.Second - req, err := http.NewRequest("GET", srv.URL+"/set-hub", nil) + req, err := http.NewRequest("GET", srv.URL+"/with-span", nil) if err != nil { t.Fatal(err) } @@ -545,73 +543,9 @@ func TestSetHubOnContext(t *testing.T) { if err != nil { t.Fatal(err) } -} - -func TestGetSpanFromContext(t *testing.T) { - err := sentry.Init(sentry.ClientOptions{ - EnableTracing: true, - TracesSampleRate: 1.0, - }) - if err != nil { - t.Fatal(err) - } - - router := echo.New() - router.GET("/no-span", func(c *echo.Context) error { - span := sentryecho.GetSpanFromContext(c) - if span != nil { - t.Error("expecting span to be nil") - } - return c.NoContent(http.StatusOK) - }) - router.GET("/with-span", func(c *echo.Context) error { - span := sentryecho.GetSpanFromContext(c) - if span == nil { - t.Error("expecting span to not be nil") - } - if requestSpan := sentry.SpanFromContext(c.Request().Context()); requestSpan != span { - t.Error("expecting request context to contain the middleware span") - } - return c.NoContent(http.StatusOK) - }, sentryecho.New(sentryecho.Options{})) - - srv := httptest.NewServer(router) - defer srv.Close() - - c := srv.Client() - - tests := []struct { - RequestPath string - }{ - { - RequestPath: "/no-span", - }, - { - RequestPath: "/with-span", - }, - } - c.Timeout = time.Second - for _, tt := range tests { - req, err := http.NewRequest("GET", srv.URL+tt.RequestPath, nil) - if err != nil { - t.Fatal(err) - } - res, err := c.Do(req) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Errorf("Status code = %d expected: %d", res.StatusCode, 200) - } - err = res.Body.Close() - if err != nil { - t.Fatal(err) - } - - if ok := sentry.Flush(testutils.FlushTimeout()); !ok { - t.Fatal("sentry.Flush timed out") - } + if ok := sentry.Flush(testutils.FlushTimeout()); !ok { + t.Fatal("sentry.Flush timed out") } } diff --git a/gin/README.md b/gin/README.md index aa402c9c5..8f2c88b61 100644 --- a/gin/README.md +++ b/gin/README.md @@ -69,11 +69,10 @@ Timeout time.Duration ## Usage -`sentrygin` attaches an instance of `*sentry.Hub` (https://pkg.go.dev/github.com/getsentry/sentry-go#Hub) to the `*gin.Context`, which makes it available throughout the rest of the request's lifetime. -You can access it by using the `sentrygin.GetHubFromContext()` method on the context itself in any of your proceeding middleware and routes. -And it should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests. +`sentrygin` attaches a request-specific `*sentry.Scope` and transaction to the request context. Pass `ctx.Request.Context()` to capture functions such as `sentry.CaptureMessage` and `sentry.CaptureException` so request data, custom scope data, and trace information are applied to the event. +Use `sentry.ScopeFromContext(ctx.Request.Context())` when you need to add data that should be available to captures made during the request. -**Keep in mind that `*sentry.Hub` won't be available in middleware attached before to `sentrygin`!** +**Keep in mind that the request scope won't be available in middleware attached before `sentrygin`!** ```go app := gin.Default() @@ -83,19 +82,14 @@ app.Use(sentrygin.New(sentrygin.Options{ })) app.Use(func(ctx *gin.Context) { - if hub := sentrygin.GetHubFromContext(ctx); hub != nil { - hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") - } + sentry.ScopeFromContext(ctx.Request.Context()).SetTag("someRandomTag", "maybeYouNeedIt") ctx.Next() }) app.GET("/", func(ctx *gin.Context) { - if hub := sentrygin.GetHubFromContext(ctx); hub != nil { - hub.WithScope(func(scope *sentry.Scope) { - scope.SetTag("unwantedQuery", "someQueryDataMaybe") - hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") - }) - } + scope := sentry.ScopeFromContext(ctx.Request.Context()) + scope.SetTag("unwantedQuery", "someQueryDataMaybe") + sentry.CaptureMessage(ctx.Request.Context(), "User provided unwanted query string, but we recovered just fine") ctx.Status(http.StatusOK) }) diff --git a/gin/sentrygin.go b/gin/sentrygin.go index 9c06b4853..849a018a5 100644 --- a/gin/sentrygin.go +++ b/gin/sentrygin.go @@ -10,18 +10,13 @@ import ( "time" "github.com/getsentry/sentry-go" + "github.com/getsentry/sentry-go/internal/traceutils" "github.com/gin-gonic/gin" ) const ( // sdkIdentifier is the identifier of the Gin SDK. sdkIdentifier = "sentry.go.gin" - - // valuesKey is used as a key to store the Sentry Hub instance on the gin.Context. - valuesKey = "sentry" - - // transactionKey is used as a key to store the Sentry transaction on the gin.Context. - transactionKey = "sentry_transaction" ) type handler struct { @@ -57,16 +52,10 @@ func New(options Options) gin.HandlerFunc { } func (h *handler) handle(c *gin.Context) { - ctx := c.Request.Context() - hub := sentry.GetHubFromContext(ctx) - if hub == nil { - hub = sentry.CurrentHub().Clone() - } + created := sentry.SpanFromContext(c.Request.Context()) == nil + ctx, scope := sentry.WithIsolationScope(c.Request.Context()) - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } - ctx = sentry.SetHubOnContext(ctx, hub) + sentry.ClientFromContext(ctx).SetSDKIdentifier(sdkIdentifier) transactionName := c.Request.URL.Path transactionSource := sentry.SourceURL @@ -77,7 +66,7 @@ func (h *handler) handle(c *gin.Context) { } options := []sentry.SpanOption{ - sentry.ContinueTrace(c.GetHeader(sentry.SentryTraceHeader), c.GetHeader(sentry.SentryBaggageHeader)), + traceutils.ContinueFromRequest(c.Request), sentry.WithOpName("http.server"), sentry.WithTransactionSource(transactionSource), sentry.WithSpanOrigin(sentry.SpanOriginGin), @@ -89,33 +78,31 @@ func (h *handler) handle(c *gin.Context) { options..., ) - transaction.SetData("http.request.method", c.Request.Method) - - defer func() { - status := c.Writer.Status() - transaction.Status = sentry.HTTPtoSpanStatus(status) - transaction.SetData("http.response.status_code", status) - transaction.Finish() - }() + if created { + ctx = transaction.Context() + transaction.SetData("http.request.method", c.Request.Method) + defer func() { + status := c.Writer.Status() + transaction.Status = sentry.HTTPtoSpanStatus(status) + transaction.SetData("http.response.status_code", status) + transaction.Finish() + }() + } - c.Request = c.Request.WithContext(transaction.Context()) - hub.Scope().SetRequest(c.Request) - c.Set(valuesKey, hub) - c.Set(transactionKey, transaction) - defer h.recoverWithSentry(hub, c.Request) + c.Request = c.Request.WithContext(ctx) + scope.SetRequest(c.Request) + defer h.recoverWithSentry(c.Request) c.Next() } -func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { +func (h *handler) recoverWithSentry(r *http.Request) { if err := recover(); err != nil { if !isBrokenPipeError(err) { - eventID := hub.RecoverWithContext( - context.WithValue(r.Context(), sentry.RequestContextKey, r), - err, - ) + ctx := context.WithValue(r.Context(), sentry.RequestContextKey, r) + eventID := sentry.Recover(ctx, err) if eventID != nil && h.waitForDelivery { - hub.Flush(h.timeout) + sentry.ClientFromContext(ctx).Flush(h.timeout) } } if h.repanic { @@ -136,29 +123,3 @@ func isBrokenPipeError(err interface{}) bool { } return false } - -// GetHubFromContext retrieves attached *sentry.Hub instance from gin.Context. -func GetHubFromContext(ctx *gin.Context) *sentry.Hub { - if hub, ok := ctx.Get(valuesKey); ok { - if hub, ok := hub.(*sentry.Hub); ok { - return hub - } - } - return nil -} - -// SetHubOnContext sets *sentry.Hub instance to gin.Context. -func SetHubOnContext(ctx *gin.Context, hub *sentry.Hub) { - ctx.Set(valuesKey, hub) -} - -// GetSpanFromContext retrieves attached *sentry.Span instance from gin.Context. -// If there is no transaction on echo.Context, it will return nil. -func GetSpanFromContext(ctx *gin.Context) *sentry.Span { - if span, ok := ctx.Get(transactionKey); ok { - if span, ok := span.(*sentry.Span); ok { - return span - } - } - return nil -} diff --git a/gin/sentrygin_test.go b/gin/sentrygin_test.go index 63a94491e..bcb91a9e8 100644 --- a/gin/sentrygin_test.go +++ b/gin/sentrygin_test.go @@ -5,7 +5,6 @@ import ( "io" "net/http" "net/http/httptest" - "reflect" "strconv" "strings" "testing" @@ -118,12 +117,11 @@ func TestIntegration(t *testing.T) { Body: `{"safe":"value"}`, ContentType: "application/json", Handler: func(c *gin.Context) { - hub := sentry.GetHubFromContext(c.Request.Context()) body, err := io.ReadAll(c.Request.Body) if err != nil { t.Error(err) } - hub.CaptureMessage("post: " + string(body)) + sentry.CaptureMessage(c.Request.Context(), "post: "+string(body)) c.JSON(http.StatusOK, gin.H{"status": "ok"}) }, WantTransaction: &sentry.Event{ @@ -174,8 +172,7 @@ func TestIntegration(t *testing.T) { Method: "GET", WantStatus: 200, Handler: func(c *gin.Context) { - hub := sentry.GetHubFromContext(c.Request.Context()) - hub.CaptureMessage("get") + sentry.CaptureMessage(c.Request.Context(), "get") c.JSON(http.StatusOK, gin.H{"status": "get"}) }, WantTransaction: &sentry.Event{ @@ -221,12 +218,11 @@ func TestIntegration(t *testing.T) { WantStatus: 200, Body: largePayload, Handler: func(c *gin.Context) { - hub := sentry.GetHubFromContext(c.Request.Context()) body, err := io.ReadAll(c.Request.Body) if err != nil { t.Error(err) } - hub.CaptureMessage(fmt.Sprintf("post: %d KB", len(body)/1024)) + sentry.CaptureMessage(c.Request.Context(), fmt.Sprintf("post: %d KB", len(body)/1024)) }, WantTransaction: &sentry.Event{ Level: sentry.LevelInfo, @@ -275,8 +271,7 @@ func TestIntegration(t *testing.T) { WantStatus: 200, Body: "client sends, server ignores, SDK doesn't read", Handler: func(c *gin.Context) { - hub := sentry.GetHubFromContext(c.Request.Context()) - hub.CaptureMessage("body ignored") + sentry.CaptureMessage(c.Request.Context(), "body ignored") }, WantTransaction: &sentry.Event{ Level: sentry.LevelInfo, @@ -493,13 +488,24 @@ func TestIntegration(t *testing.T) { } } -func TestSetHubOnContext(t *testing.T) { - hub := sentry.CurrentHub() - ctx := &gin.Context{} - sentrygin.SetHubOnContext(ctx, hub) - got := sentrygin.GetHubFromContext(ctx) - - if !reflect.DeepEqual(hub, got) { - t.Fatalf("Hub mismatch: got %v want %v", got, hub) +func TestRequestContextState(t *testing.T) { + if err := sentry.Init(sentry.ClientOptions{}); err != nil { + t.Fatal(err) } + + router := gin.New() + router.Use(sentrygin.New(sentrygin.Options{})) + router.GET("/", func(ctx *gin.Context) { + requestCtx := ctx.Request.Context() + if scope := sentry.ScopeFromContext(requestCtx); scope == nil { + t.Error("expecting scope to be not nil") + } + if span := sentry.SpanFromContext(requestCtx); span == nil { + t.Error("expecting span to be not nil") + } + }) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/", nil) + router.ServeHTTP(recorder, request) } diff --git a/iris/README.md b/iris/README.md index 6966c3766..f64034782 100644 --- a/iris/README.md +++ b/iris/README.md @@ -68,11 +68,10 @@ Timeout time.Duration ## Usage -`sentryiris` attaches an instance of `*sentry.Hub` (https://pkg.go.dev/github.com/getsentry/sentry-go#Hub) to the `iris.Context`, which makes it available throughout the rest of the request's lifetime. -You can access it by using the `sentryiris.GetHubFromContext()` method on the context itself in any of your proceeding middleware and routes. -And it should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests. +`sentryiris` attaches a request-specific `*sentry.Scope` and transaction to the request context. Pass `ctx.Request().Context()` to capture functions such as `sentry.CaptureMessage` and `sentry.CaptureException` so request data, custom scope data, and trace information are applied to the event. +Use `sentry.ScopeFromContext(ctx.Request().Context())` when you need to add data that should be available to captures made during the request. -**Keep in mind that `*sentry.Hub` won't be available in middleware attached before to `sentryiris`!** +**Keep in mind that the request scope won't be available in middleware attached before `sentryiris`!** ```go app := iris.Default() @@ -82,19 +81,14 @@ app.Use(sentryiris.New(sentryiris.Options{ })) app.Use(func(ctx iris.Context) { - if hub := sentryiris.GetHubFromContext(ctx); hub != nil { - hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") - } + sentry.ScopeFromContext(ctx.Request().Context()).SetTag("someRandomTag", "maybeYouNeedIt") ctx.Next() }) app.Get("/", func(ctx iris.Context) { - if hub := sentryiris.GetHubFromContext(ctx); hub != nil { - hub.WithScope(func(scope *sentry.Scope) { - scope.SetTag("unwantedQuery", "someQueryDataMaybe") - hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") - }) - } + scope := sentry.ScopeFromContext(ctx.Request().Context()) + scope.SetTag("unwantedQuery", "someQueryDataMaybe") + sentry.CaptureMessage(ctx.Request().Context(), "User provided unwanted query string, but we recovered just fine") }) app.Get("/foo", func(ctx iris.Context) { diff --git a/iris/example_test.go b/iris/example_test.go index b5a8420f7..6383c5f51 100644 --- a/iris/example_test.go +++ b/iris/example_test.go @@ -9,7 +9,7 @@ import ( "github.com/kataras/iris/v12" ) -func ExampleGetSpanFromContext() { +func Example() { app := iris.New() app.Use(sentryiris.New(sentryiris.Options{})) app.Get("/", func(ctx iris.Context) { @@ -19,12 +19,13 @@ func ExampleGetSpanFromContext() { // do resource intensive thing } - // Acquire transaction on current hub that's created by the SDK. - // Be careful, it might be a nil value if you didn't set up sentryiris middleware. - sentrySpan := sentryiris.GetSpanFromContext(ctx) - // Pass in the `.Context()` method from `*sentry.Span` struct. - // The `context.Context` instance inherits the context from `iris.Context`. - expensiveThing(sentrySpan.Context()) + // Acquire the transaction from the request context. It may be nil if + // you did not set up the sentryiris middleware. + spanContext := ctx.Request().Context() + if sentrySpan := sentry.SpanFromContext(spanContext); sentrySpan != nil { + spanContext = sentrySpan.Context() + } + expensiveThing(spanContext) ctx.StatusCode(http.StatusOK) }) diff --git a/iris/sentryiris.go b/iris/sentryiris.go index 0ac97bd8a..d3ffd3f57 100644 --- a/iris/sentryiris.go +++ b/iris/sentryiris.go @@ -7,6 +7,7 @@ import ( "time" "github.com/getsentry/sentry-go" + "github.com/getsentry/sentry-go/internal/traceutils" "github.com/kataras/iris/v12" ) @@ -14,12 +15,6 @@ import ( const ( // sdkIdentifier is the identifier of the Iris SDK. sdkIdentifier = "sentry.go.iris" - - // valuesKey is used as a key to store the Sentry Hub instance on the iris.Context. - valuesKey = "sentry" - - // transactionKey is used as a key to store the Sentry transaction on the iris.Context. - transactionKey = "sentry_transaction" ) type handler struct { @@ -55,20 +50,14 @@ func New(options Options) iris.Handler { } func (h *handler) handle(ctx iris.Context) { - hub := sentry.GetHubFromContext(ctx.Request().Context()) - if hub == nil { - hub = sentry.CurrentHub().Clone() - } - - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } - r := ctx.Request() - requestCtx := sentry.SetHubOnContext(ctx, hub) + created := sentry.SpanFromContext(r.Context()) == nil + requestCtx, scope := sentry.WithIsolationScope(r.Context()) + + sentry.ClientFromContext(requestCtx).SetSDKIdentifier(sdkIdentifier) options := []sentry.SpanOption{ - sentry.ContinueTrace(r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)), + traceutils.ContinueFromRequest(r), sentry.WithOpName("http.server"), sentry.WithTransactionSource(sentry.SourceRoute), sentry.WithSpanOrigin(sentry.SpanOriginIris), @@ -82,55 +71,32 @@ func (h *handler) handle(ctx iris.Context) { options..., ) - defer func() { - transaction.SetData("http.response.status_code", ctx.GetStatusCode()) - transaction.Status = sentry.HTTPtoSpanStatus(ctx.GetStatusCode()) - transaction.Finish() - }() - - transaction.SetData("http.request.method", r.Method) + if created { + requestCtx = transaction.Context() + transaction.SetData("http.request.method", r.Method) + defer func() { + transaction.SetData("http.response.status_code", ctx.GetStatusCode()) + transaction.Status = sentry.HTTPtoSpanStatus(ctx.GetStatusCode()) + transaction.Finish() + }() + } - hub.Scope().SetRequest(r) - ctx.Values().Set(valuesKey, hub) - ctx.Values().Set(transactionKey, transaction) - defer h.recoverWithSentry(hub, r) + r = r.WithContext(requestCtx) + ctx.ResetRequest(r) + scope.SetRequest(r) + defer h.recoverWithSentry(r) ctx.Next() } -func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { +func (h *handler) recoverWithSentry(r *http.Request) { if err := recover(); err != nil { - eventID := hub.RecoverWithContext( - context.WithValue(r.Context(), sentry.RequestContextKey, r), - err, - ) + ctx := context.WithValue(r.Context(), sentry.RequestContextKey, r) + eventID := sentry.Recover(ctx, err) if eventID != nil && h.waitForDelivery { - hub.Flush(h.timeout) + sentry.ClientFromContext(ctx).Flush(h.timeout) } if h.repanic { panic(err) } } } - -// 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 - } - return nil -} - -// SetHubOnContext attaches a *sentry.Hub instance to iris.Context. -func SetHubOnContext(ctx iris.Context, hub *sentry.Hub) { - ctx.Values().Set(valuesKey, hub) -} - -// GetSpanFromContext retrieves attached *sentry.Span instance from iris.Context. -// If there is no transaction on iris.Context, it will return nil. -func GetSpanFromContext(ctx iris.Context) *sentry.Span { - if span, ok := ctx.Values().Get(transactionKey).(*sentry.Span); ok { - return span - } - - return nil -} diff --git a/iris/sentryiris_test.go b/iris/sentryiris_test.go index 0da939be1..00cfc1dfa 100644 --- a/iris/sentryiris_test.go +++ b/iris/sentryiris_test.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "net/http" - "reflect" "strconv" "strings" "testing" @@ -110,12 +109,11 @@ func TestIntegration(t *testing.T) { Body: `{"safe":"value"}`, ContentType: "application/json", Handler: func(ctx iris.Context) { - hub := sentryiris.GetHubFromContext(ctx) body, err := io.ReadAll(ctx.Request().Body) if err != nil { t.Error(err) } - hub.CaptureMessage("post: " + string(body)) + sentry.CaptureMessage(ctx.Request().Context(), "post: "+string(body)) ctx.StatusCode(http.StatusOK) _ = ctx.JSON(map[string]any{"status": "ok"}) }, @@ -168,8 +166,7 @@ func TestIntegration(t *testing.T) { Method: "GET", WantStatus: 200, Handler: func(ctx iris.Context) { - hub := sentryiris.GetHubFromContext(ctx) - hub.CaptureMessage("get") + sentry.CaptureMessage(ctx.Request().Context(), "get") ctx.StatusCode(http.StatusOK) _ = ctx.JSON(map[string]any{"status": "get"}) }, @@ -217,12 +214,11 @@ func TestIntegration(t *testing.T) { WantStatus: 200, Body: largePayload, Handler: func(ctx iris.Context) { - hub := sentryiris.GetHubFromContext(ctx) body, err := io.ReadAll(ctx.Request().Body) if err != nil { t.Error(err) } - hub.CaptureMessage(fmt.Sprintf("post: %d KB", len(body)/1024)) + sentry.CaptureMessage(ctx.Request().Context(), fmt.Sprintf("post: %d KB", len(body)/1024)) }, WantTransaction: &sentry.Event{ Level: sentry.LevelInfo, @@ -272,8 +268,7 @@ func TestIntegration(t *testing.T) { WantStatus: 200, Body: "client sends, server ignores, SDK doesn't read", Handler: func(ctx iris.Context) { - hub := sentryiris.GetHubFromContext(ctx) - hub.CaptureMessage("body ignored") + sentry.CaptureMessage(ctx.Request().Context(), "body ignored") }, WantTransaction: &sentry.Event{ Level: sentry.LevelInfo, @@ -492,7 +487,7 @@ func TestIntegration(t *testing.T) { } } -func TestGetSpanFromContext(t *testing.T) { +func TestRequestContextState(t *testing.T) { err := sentry.Init(sentry.ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, @@ -502,17 +497,13 @@ func TestGetSpanFromContext(t *testing.T) { } router := iris.New() - router.Get("/no-span", func(ctx iris.Context) { - span := sentryiris.GetSpanFromContext(ctx) - if span != nil { - t.Error("expecting span to be nil") - } - - ctx.StatusCode(http.StatusOK) - }) router.Use(sentryiris.New(sentryiris.Options{})) router.Get("/with-span", func(ctx iris.Context) { - span := sentryiris.GetSpanFromContext(ctx) + scope := sentry.ScopeFromContext(ctx.Request().Context()) + if scope == nil { + t.Error("expecting scope to be not nil") + } + span := sentry.SpanFromContext(ctx.Request().Context()) if span == nil { t.Error("expecting span to be not nil") } @@ -520,46 +511,12 @@ func TestGetSpanFromContext(t *testing.T) { ctx.StatusCode(http.StatusOK) }) - tests := []struct{ RequestPath string }{ - {RequestPath: "/no-span"}, - {RequestPath: "/with-span"}, - } - srv := httptest.New(t, router) + res := srv.Request(http.MethodGet, "/with-span").Expect() - for _, tt := range tests { - res := srv.Request(http.MethodGet, tt.RequestPath).Expect() - - res.Status(http.StatusOK) + res.Status(http.StatusOK) - if ok := sentry.Flush(testutils.FlushTimeout()); !ok { - t.Fatal("sentry.Flush timed out") - } + if ok := sentry.Flush(testutils.FlushTimeout()); !ok { + t.Fatal("sentry.Flush timed out") } } - -func TestSetHubOnContext(t *testing.T) { - app := iris.New() - - app.Get("/with-hub", func(ctx iris.Context) { - hub := sentry.CurrentHub().Clone() - sentryiris.SetHubOnContext(ctx, hub) - - newHub := sentryiris.GetHubFromContext(ctx) - if newHub == nil { - t.Error("expecting hub to be not nil") - } - - if !reflect.DeepEqual(hub, newHub) { - t.Error("expecting hub to be the same") - } - - ctx.StatusCode(http.StatusOK) - }) - - srv := httptest.New(t, app) - - res := srv.Request(http.MethodGet, "/with-hub").Expect() - - res.Status(http.StatusOK) -} diff --git a/negroni/README.md b/negroni/README.md index 31e7a4870..537a899cc 100644 --- a/negroni/README.md +++ b/negroni/README.md @@ -73,11 +73,10 @@ Timeout time.Duration ## Usage -`sentrynegroni` attaches an instance of `*sentry.Hub` (https://pkg.go.dev/github.com/getsentry/sentry-go#Hub) to the request's context, which makes it available throughout the rest of the request's lifetime. -You can access it by using the `sentry.GetHubFromContext()` method on the request itself in any of your proceeding middleware and routes. -And it should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests. +`sentrynegroni` attaches a request-specific `*sentry.Scope` and transaction to the request context. Pass `r.Context()` to capture functions such as `sentry.CaptureMessage` and `sentry.CaptureException` so request data, custom scope data, and trace information are applied to the event. +Use `sentry.ScopeFromContext(r.Context())` when you need to add data that should be available to captures made during the request. -**Keep in mind that `*sentry.Hub` won't be available in middleware attached before to `sentrynegroni`!** +**Keep in mind that the request scope won't be available in middleware attached before `sentrynegroni`!** ```go app := negroni.Classic() @@ -87,19 +86,16 @@ app.Use(sentrynegroni.New(sentrynegroni.Options{ })) app.Use(negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - hub := sentry.GetHubFromContext(r.Context()) - hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") + sentry.ScopeFromContext(r.Context()).SetTag("someRandomTag", "maybeYouNeedIt") next(rw, r) })) mux := http.NewServeMux() mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { - hub := sentry.GetHubFromContext(r.Context()) - hub.WithScope(func(scope *sentry.Scope) { - scope.SetTag("unwantedQuery", "someQueryDataMaybe") - hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") - }) + scope := sentry.ScopeFromContext(r.Context()) + scope.SetTag("unwantedQuery", "someQueryDataMaybe") + sentry.CaptureMessage(r.Context(), "User provided unwanted query string, but we recovered just fine") rw.WriteHeader(http.StatusOK) }) diff --git a/negroni/sentrynegroni.go b/negroni/sentrynegroni.go index 3866d286e..469c42000 100644 --- a/negroni/sentrynegroni.go +++ b/negroni/sentrynegroni.go @@ -47,54 +47,50 @@ func New(options Options) negroni.Handler { } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - hub := sentry.GetHubFromContext(r.Context()) - if hub == nil { - hub = sentry.CurrentHub().Clone() - } + created := sentry.SpanFromContext(r.Context()) == nil + ctx, scope := sentry.WithIsolationScope(r.Context()) - if client := hub.Client(); client != nil { - client.SetSDKIdentifier(sdkIdentifier) - } - requestCtx := sentry.SetHubOnContext(r.Context(), hub) + sentry.ClientFromContext(ctx).SetSDKIdentifier(sdkIdentifier) options := []sentry.SpanOption{ - sentry.ContinueTrace(r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)), + traceutils.ContinueFromRequest(r), sentry.WithOpName("http.server"), sentry.WithTransactionSource(sentry.SourceURL), sentry.WithSpanOrigin(sentry.SpanOriginNegroni), } transaction := sentry.StartTransaction( - requestCtx, + ctx, traceutils.GetHTTPSpanName(r), options..., ) - transaction.SetData("http.request.method", r.Method) rw := httputils.NewWrapResponseWriter(w, r.ProtoMajor) - defer func() { - status := rw.Status() - transaction.Status = sentry.HTTPtoSpanStatus(status) - transaction.SetData("http.response.status_code", status) - transaction.Finish() - }() + if created { + ctx = transaction.Context() + transaction.SetData("http.request.method", r.Method) + defer func() { + status := rw.Status() + transaction.Status = sentry.HTTPtoSpanStatus(status) + transaction.SetData("http.response.status_code", status) + transaction.Finish() + }() + } - hub.Scope().SetRequest(r) - r = r.WithContext(transaction.Context()) - defer h.recoverWithSentry(hub, r) + r = r.WithContext(ctx) + scope.SetRequest(r) + defer h.recoverWithSentry(r) - next(rw, r.WithContext(r.Context())) + next(rw, r) } -func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { +func (h *handler) recoverWithSentry(r *http.Request) { if err := recover(); err != nil { - eventID := hub.RecoverWithContext( - context.WithValue(r.Context(), sentry.RequestContextKey, r), - err, - ) + ctx := context.WithValue(r.Context(), sentry.RequestContextKey, r) + eventID := sentry.Recover(ctx, err) if eventID != nil && h.waitForDelivery { - hub.Flush(h.timeout) + sentry.ClientFromContext(ctx).Flush(h.timeout) } if h.repanic { panic(err) @@ -105,12 +101,9 @@ func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) { // PanicHandlerFunc can be used for Negroni's default Recovery middleware option called PanicHandlerFunc, // which let you "plug-in" to its own handler. func PanicHandlerFunc(info *negroni.PanicInformation) { - hub := sentry.CurrentHub().Clone() - hub.WithScope(func(scope *sentry.Scope) { - scope.SetRequest(info.Request) - hub.RecoverWithContext( - context.WithValue(context.Background(), sentry.RequestContextKey, info.Request), - info.RecoveredPanic, - ) - }) + ctx, scope := sentry.WithIsolationScope(info.Request.Context()) + request := info.Request.WithContext(ctx) + scope.SetRequest(request) + ctx = context.WithValue(ctx, sentry.RequestContextKey, request) + sentry.Recover(ctx, info.RecoveredPanic) } diff --git a/negroni/sentrynegroni_test.go b/negroni/sentrynegroni_test.go index 57c821014..fe9554f39 100644 --- a/negroni/sentrynegroni_test.go +++ b/negroni/sentrynegroni_test.go @@ -1,8 +1,10 @@ package sentrynegroni_test import ( + "context" "fmt" "io" + "log" "net/http" "net/http/httptest" "strings" @@ -10,6 +12,7 @@ import ( "time" "github.com/getsentry/sentry-go" + "github.com/getsentry/sentry-go/internal/sentrytest" "github.com/getsentry/sentry-go/internal/testutils" sentrynegroni "github.com/getsentry/sentry-go/negroni" "github.com/google/go-cmp/cmp" @@ -81,12 +84,11 @@ func TestIntegration(t *testing.T) { Body: `{"safe":"value"}`, ContentType: "application/json", Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - hub := sentry.GetHubFromContext(r.Context()) body, err := io.ReadAll(r.Body) if err != nil { t.Error(err) } - hub.CaptureMessage("post: " + string(body)) + sentry.CaptureMessage(r.Context(), "post: "+string(body)) }), WantStatus: http.StatusOK, @@ -135,8 +137,7 @@ func TestIntegration(t *testing.T) { { Path: "/get", Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - hub := sentry.GetHubFromContext(r.Context()) - hub.CaptureMessage("get") + sentry.CaptureMessage(r.Context(), "get") }), WantStatus: http.StatusOK, @@ -181,12 +182,11 @@ func TestIntegration(t *testing.T) { Method: "POST", Body: largePayload, Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - hub := sentry.GetHubFromContext(r.Context()) body, err := io.ReadAll(r.Body) if err != nil { t.Error(err) } - hub.CaptureMessage(fmt.Sprintf("post: %d KB", len(body)/1024)) + sentry.CaptureMessage(r.Context(), fmt.Sprintf("post: %d KB", len(body)/1024)) }), WantStatus: http.StatusOK, @@ -237,8 +237,7 @@ func TestIntegration(t *testing.T) { Method: "POST", Body: "client sends, server ignores, SDK doesn't read", Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - hub := sentry.GetHubFromContext(r.Context()) - hub.CaptureMessage("body ignored") + sentry.CaptureMessage(r.Context(), "body ignored") }), WantStatus: http.StatusOK, @@ -425,3 +424,27 @@ func TestIntegration(t *testing.T) { t.Fatalf("Transaction status codes mismatch (-want +got):\n%s", diff) } } + +func TestPanicHandlerFuncDoesNotReplaceRequest(t *testing.T) { + t.Parallel() + fixture := sentrytest.NewFixture(t) + request := httptest.NewRequest(http.MethodGet, "http://example.com/panic", nil).WithContext(fixture.NewContext(context.Background())) + recovery := negroni.NewRecovery() + recovery.Logger = log.New(io.Discard, "", 0) + recovery.PrintStack = false + var info *negroni.PanicInformation + recovery.PanicHandlerFunc = func(recovered *negroni.PanicInformation) { + info = recovered + sentrynegroni.PanicHandlerFunc(info) + } + recovery.ServeHTTP(httptest.NewRecorder(), request, func(http.ResponseWriter, *http.Request) { + panic("test") + }) + if info == nil || info.Request != request { + t.Fatal("PanicHandlerFunc did not preserve the recovered request") + } + fixture.Flush() + if got := len(fixture.Events()); got != 1 { + t.Fatalf("captured events = %d, want 1", got) + } +}