Name Sentry transactions after the route they matched (closes #185)
check / check (push) Successful in 14m13s

sentryhttp names each transaction after the concrete path, which the
BeforeSendTransaction hook has to withhold, so with tracing on every
transaction would land in one Sentry group, POST /(redacted). A
middleware installed beside every sentryhttp handler now renames the
transaction to the method and chi route pattern once the request is
routed, and sets its source to route. The hook keeps a route-sourced
name and withholds every other name as before, so a request that
matched no route still reads POST /(redacted). Tracing stays off.

Model: opus-5-5
This commit is contained in:
2026-10-12 23:46:21 +00:00
parent 9199544999
commit 96d8e7c1aa
5 changed files with 179 additions and 17 deletions
+53 -4
View File
@@ -1,6 +1,7 @@
package server
import (
"context"
"net/http"
"net/url"
"strings"
@@ -71,10 +72,15 @@ func scrubSentryRequest(
pattern := sentryRoutePattern(hint)
// Only transaction events carry a Transaction name, and the SDK
// Only transaction events carry a Transaction name. sentryhttp
// builds it from the concrete path too (sentryhttp.go:105 via
// tracing.go:553). Rewritten on the same terms.
if event.Transaction != "" {
// tracing.go:553) and marks it SourceURL, so it is rewritten on
// the same terms. Only a name nameSentryTransaction set from the
// route pattern, which it marks SourceRoute, is kept.
namedByRoute := event.TransactionInfo != nil &&
event.TransactionInfo.Source == sentry.SourceRoute
if event.Transaction != "" && !namedByRoute {
event.Transaction = sentryTransactionName(
event.Transaction, pattern,
)
@@ -120,7 +126,8 @@ func scrubSentryRequest(
// The transaction dispatch has no such request: Span.doFinish calls
// hub.CaptureEvent (tracing.go:356), which passes a nil hint that the
// client replaces with an empty one (client.go:620-622). The pattern
// is therefore always "" there, and the callers fall back.
// is therefore always "" there, and the callers fall back. A routed
// transaction is named by nameSentryTransaction instead.
func sentryRoutePattern(hint *sentry.EventHint) string {
if hint == nil || hint.Context == nil {
return ""
@@ -142,6 +149,48 @@ func sentryRoutePattern(hint *sentry.EventHint) string {
return rctx.RoutePattern()
}
// nameSentryTransaction renames the request's Sentry transaction to
// "METHOD <route pattern>", such as "POST /h/{uuid}". sentryhttp names
// it after the concrete path, which scrubSentryRequest withholds;
// since Sentry groups transactions by name, every transaction in the
// service would otherwise land in one group, "POST /(redacted)".
//
// It has to run inside the sentryhttp handler, where the transaction
// is on the request context. It renames on the way out, once chi has
// routed the request, and does so in a defer so that a request whose
// handler panics is named too. The context is taken when the defer is
// set up, before routing, but the chi routing context on it is a
// pointer that chi fills in as it routes.
func nameSentryTransaction(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
defer setSentryTransactionName(r.Context(), r.Method)
next.ServeHTTP(w, r)
},
)
}
// setSentryTransactionName does the rename for nameSentryTransaction.
// A request that matched no route has no pattern; its name is left as
// the SDK set it, for scrubSentryRequest to withhold.
func setSentryTransactionName(ctx context.Context, method string) {
transaction := sentry.TransactionFromContext(ctx)
rctx := chi.RouteContext(ctx)
if transaction == nil || rctx == nil {
return
}
pattern := rctx.RoutePattern()
if pattern == "" {
return
}
transaction.Name = method + " " + pattern
transaction.Source = sentry.SourceRoute
}
// sentryRouteURL rebuilds an event's request URL with the route
// pattern in place of the concrete path.
//