Deploy main eacf1e7 to prod
check / check (push) Waiting to run

Replaces #559, which conflicted.

Model: opus-5-5
This commit is contained in:
2026-10-25 01:55:36 +00:00
parent 1647b43aa6
commit 537374e4cc
311 changed files with 35614 additions and 14120 deletions
+76 -11
View File
@@ -1,12 +1,14 @@
package server
import (
"context"
"net/http"
"net/url"
"strings"
"github.com/getsentry/sentry-go"
"github.com/go-chi/chi"
"sneak.berlin/go/webhooker/internal/referer"
)
// sentryRedacted stands in for a withheld field on every event shipped
@@ -55,7 +57,7 @@ func sentryClientOptions(dsn, release string) sentry.ClientOptions {
//
// URL is the third such field. NewRequest builds it as
// scheme://host/path (interfaces.go:183), and on the receiver route
// that path is /webhook/<uuid> in full — a write capability, not an
// that path is /h/<uuid> in full — a write capability, not an
// identifier. It is rebuilt here from the chi route pattern, on every
// route, keeping the scheme and the host.
//
@@ -71,10 +73,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 +127,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 +150,52 @@ 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 per method, such as
// "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 path that matches nothing at the top level has no pattern; its
// name is left as the SDK set it, for scrubSentryRequest to withhold.
// Inside a route group, a path that matches no route in the group, or
// a request the group's own middleware turns away before routing, has
// the group's pattern, such as "/pages/*", and is named after it.
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.
//
@@ -200,6 +254,7 @@ func sentryTransactionName(name, pattern string) string {
// gorilla/csrf accepts in place of the form field — and the shared
// secrets senders put on the receiver route (X-Gitlab-Token and the
// per-provider signature headers) would otherwise ship verbatim.
// The Referer it keeps is cut down to its scheme and host.
func keptSentryHeaders(headers map[string]string) map[string]string {
if len(headers) == 0 {
return headers
@@ -208,9 +263,15 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
kept := make(map[string]string, len(headers))
for name, value := range headers {
if sentryKeepsHeader(name) {
kept[name] = value
if !sentryKeepsHeader(name) {
continue
}
if http.CanonicalHeaderKey(name) == "Referer" {
value = referer.SchemeAndHost(value)
}
kept[name] = value
}
return kept
@@ -218,10 +279,14 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
// sentryKeepsHeader reports whether a request header is routing or
// content metadata rather than client-chosen payload. Referer is kept
// on the reasoning that it is browser-set, that this service emits
// only ?page= in its own links, and that Referrer-Policy is set to
// strict-origin-when-cross-origin. X-Request-Id ties the event to the
// local access log line, which holds the rest of the detail.
// only as its scheme and host, through referer.SchemeAndHost, which
// the access log uses too: its path can be an entrypoint URL, which
// carries the UUID, as when a sender follows a redirect from http://
// to https:// or a browser fetches the 404 page's stylesheet from
// under /h/. The scheme and host are all gorilla/csrf compares a
// Referer by, so a CSRF rejection is still readable from the event.
// X-Request-Id ties the event to the local access log line, which
// holds the rest of the detail.
func sentryKeepsHeader(name string) bool {
switch http.CanonicalHeaderKey(name) {
case "Accept",