From f2279a057df4449d242931b1b425cb8b4ca1892e Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:19:23 +0200 Subject: [PATCH] ref(http)!: migrate frameworks to context scope API Carry isolated request scopes and spans through native request contexts in Echo, Gin, Iris, and Negroni, and capture panics through the context-backed root API. Read all baggage header values and preserve existing caller contexts and transaction ownership when middleware is nested. Echo, Gin, and Iris GetHubFromContext, SetHubOnContext, and GetSpanFromContext helpers are removed. Use sentry.ScopeFromContext and sentry.SpanFromContext with the framework request context. --- crosstest/http_link_test.go | 17 +++--- echo/README.md | 29 ++++------ echo/example_test.go | 15 ++--- echo/sentryecho.go | 105 +++++++++++---------------------- echo/sentryecho_test.go | 106 +++++++--------------------------- gin/README.md | 20 +++---- gin/sentrygin.go | 83 +++++++------------------- gin/sentrygin_test.go | 40 +++++++------ iris/README.md | 20 +++---- iris/example_test.go | 15 ++--- iris/sentryiris.go | 80 ++++++++----------------- iris/sentryiris_test.go | 71 +++++------------------ negroni/README.md | 18 +++--- negroni/sentrynegroni.go | 63 +++++++++----------- negroni/sentrynegroni_test.go | 39 ++++++++++--- 15 files changed, 253 insertions(+), 468 deletions(-) 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) + } +}