package observability import ( "strings" "github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go/attribute" ) // redacted replaces any secret value found in outgoing Sentry payloads. const redacted = "[REDACTED]" // minSecretLength guards against redacting short, common values that happen to // be configured as a secret and would otherwise mangle every event. const minSecretLength = 8 // scrubber removes configured secrets from payloads before they leave the // process. Credentials reach Sentry through ordinary paths -- the Telegram // webhook embeds the bot token in its URL, and failed API calls quote the // request URL in their error text -- so scrubbing happens at the last moment, // on the fully built event, rather than at each call site. type scrubber struct { secrets []string } // newScrubber returns nil when there is nothing worth scrubbing, which lets // callers skip the BeforeSend hooks entirely. func newScrubber(secrets []string) *scrubber { var usable []string for _, secret := range secrets { if len(secret) >= minSecretLength { usable = append(usable, secret) } } if len(usable) == 0 { return nil } return &scrubber{secrets: usable} } // text redacts every configured secret found in s. func (s *scrubber) text(value string) string { for _, secret := range s.secrets { value = strings.ReplaceAll(value, secret, redacted) } return value } // value redacts secrets inside arbitrary structured data. func (s *scrubber) value(value any) any { switch typed := value.(type) { case string: return s.text(typed) case error: return s.text(typed.Error()) case []any: for i, item := range typed { typed[i] = s.value(item) } return typed case map[string]any: for key, item := range typed { typed[key] = s.value(item) } return typed default: return value } } // event redacts secrets from every field that can carry free-form text. func (s *scrubber) event(event *sentry.Event) *sentry.Event { if event == nil { return nil } event.Message = s.text(event.Message) event.Transaction = s.text(event.Transaction) for i := range event.Exception { event.Exception[i].Type = s.text(event.Exception[i].Type) event.Exception[i].Value = s.text(event.Exception[i].Value) } for i := range event.Breadcrumbs { event.Breadcrumbs[i].Message = s.text(event.Breadcrumbs[i].Message) for key, item := range event.Breadcrumbs[i].Data { event.Breadcrumbs[i].Data[key] = s.value(item) } } for name, context := range event.Contexts { for key, item := range context { event.Contexts[name][key] = s.value(item) } } for key, item := range event.Tags { event.Tags[key] = s.text(item) } if event.Request != nil { event.Request.URL = s.text(event.Request.URL) event.Request.QueryString = s.text(event.Request.QueryString) event.Request.Cookies = s.text(event.Request.Cookies) event.Request.Data = s.text(event.Request.Data) for key, item := range event.Request.Headers { event.Request.Headers[key] = s.text(item) } for key, item := range event.Request.Env { event.Request.Env[key] = s.text(item) } } return event } // log redacts secrets from a structured log entry. func (s *scrubber) log(entry *sentry.Log) *sentry.Log { if entry == nil { return nil } entry.Body = s.text(entry.Body) for key, value := range entry.Attributes { text, ok := value.AsInterface().(string) if !ok { continue } entry.Attributes[key] = attribute.StringValue(s.text(text)) } return entry }