Documentation
¶
Overview ¶
Package webhook delivers outbound notifications to operator-configured URLs (used by alerts and the daily digest). Two delivery contracts: HMAC-signed JSON for generic endpoints, and Slack's {"text": ...} shape for Slack incoming webhooks (which reject anything else). Persisted store, best-effort async delivery.
Index ¶
- Constants
- func Send(ep Endpoint, body []byte, text string) (int, error)
- func SendTest(ep Endpoint) (int, error)
- type Endpoint
- type Store
- func (s *Store) Add(name, url, format string) (Endpoint, error)
- func (s *Store) Delete(id string) (found bool, err error)
- func (s *Store) DeliverAll(payload any, text string)
- func (s *Store) Get(id string) (Endpoint, bool)
- func (s *Store) List() []Endpoint
- func (s *Store) SetEnabled(id string, on bool) (Endpoint, bool, error)
Constants ¶
const ( // FormatSlack — {"text": "<plain-text rendering>"}. Also matches Mattermost and Rocket.Chat, // both of which implement Slack's incoming-webhook contract deliberately. FormatSlack = "slack" // FormatDiscord — {"content": ...}. Discord 400s on {"text": ...}, so a Discord URL pasted // into the webhook box was rejected on every single delivery, forever, silently: the send is // fire-and-forget, so the failure went nowhere. Discord is where this ICP actually is, which // made it the most expensive missing line in the file. FormatDiscord = "discord" )
Chat formats. Each is a receiver that will NOT accept our signed-JSON contract: it wants its own tiny envelope and rejects anything else, so an endpoint on one of these hosts gets the human rendering and no signature (there is nowhere to put one).
Variables ¶
This section is empty.
Functions ¶
func Send ¶
Send POSTs one delivery to an endpoint and returns the HTTP status the endpoint answered with (0 when no response arrived). Slack-format endpoints receive {"text": text} — Slack rejects any other body shape and cannot verify signature headers — while every other endpoint keeps the signed-JSON contract unchanged: the body verbatim plus X-Smolanalytics-Signature. text is the plain-text rendering of body; if a caller passes none, the raw JSON body is used as the text so a Slack message still carries the facts instead of failing.
func SendTest ¶ added in v0.8.0
SendTest fires a synthetic delivery through the exact path real alerts and digests take (same format rules, signing, SSRF guard, and HTTP client), so a 2xx here means real deliveries will land. Returns the endpoint's HTTP status. Shared by POST /v1/webhooks/{id}/test and the MCP test_webhook tool.
Types ¶
type Endpoint ¶
type Endpoint struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Secret string `json:"secret"` // signs the payload so the receiver can verify
Format string `json:"format,omitempty"`
Enabled bool `json:"enabled"`
Created time.Time `json:"created"`
// DELIVERY HEALTH. Every outbound delivery was `go func(ep){ _, _ = Send(...) }` — the status
// and the error both discarded, on a goroutine nobody waited for. A webhook that started
// 500ing, or whose URL was revoked, simply stopped working and said nothing anywhere: no log
// line, no dashboard state, no field to inspect. That is how a Discord endpoint could reject
// every single delivery for as long as the feature existed without one person noticing.
//
// "silence = bug" is a rule this product states out loud, and this was the largest violation
// of it in the codebase.
LastStatus int `json:"last_status,omitempty"`
LastError string `json:"last_error,omitempty"`
LastAttempt time.Time `json:"last_attempt,omitempty"`
LastDelivered time.Time `json:"last_delivered,omitempty"`
// Failures counts CONSECUTIVE failures; any success resets it to zero. A webhook that fails
// once a week is a flaky receiver, not a dead one, and auto-disabling on a cumulative count
// would eventually switch off every endpoint that has ever hiccuped.
Failures int `json:"consecutive_failures,omitempty"`
// DisabledAt is set when consecutive failures crossed the limit and delivery was stopped.
// Recorded rather than just flipping Enabled, so the row can say WHY it is off — an endpoint
// that silently turned itself off is a second invisible failure on top of the first.
DisabledAt time.Time `json:"disabled_at,omitempty"`
}
Endpoint is one registered webhook target.
func (Endpoint) Chat ¶ added in v0.60.0
Chat reports whether this endpoint takes a chat envelope rather than signed JSON.
func (Endpoint) DiscordFormat ¶ added in v0.60.0
DiscordFormat reports whether deliveries use Discord's {"content": ...} contract, either because the endpoint says so or because the URL is plainly a Discord webhook — the host check also covers endpoints persisted before this format existed, which is every one of them.
func (Endpoint) Health ¶ added in v0.63.0
Health renders the delivery state in words, for the settings row and list_webhooks. One renderer, for the same reason Cost has one.
func (Endpoint) Healthy ¶ added in v0.63.0
Healthy reports whether the last attempt succeeded. A brand-new endpoint with no attempt yet is healthy: never-tried and known-broken must not render the same.
func (Endpoint) SlackFormat ¶ added in v0.8.0
SlackFormat reports whether deliveries to e use Slack's {"text": ...} contract: either the endpoint was created with format "slack", or the URL is a Slack incoming webhook (hooks.slack.com) — the host check also covers endpoints persisted before the format field existed.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) Add ¶
Add registers a new endpoint. format is "" (auto-detect: Slack contract for hooks.slack.com URLs, signed JSON for everything else) or "slack" to force the Slack text contract for Slack-compatible receivers on other hosts (Mattermost, Rocket.Chat, …).
func (*Store) Delete ¶
Delete removes an endpoint by id. found is true only when an endpoint actually went away, so callers never claim a removal that did not occur. A miss is not an error.
func (*Store) DeliverAll ¶
DeliverAll fires the payload to every enabled endpoint, async + best-effort. text is the plain-text rendering that Slack-format endpoints receive.
func (*Store) SetEnabled ¶ added in v0.63.0
SetEnabled turns an endpoint on or off, clearing the failure state when it is re-enabled.
The field existed from the beginning and no method ever wrote it, so `enabled` was reported to agents as if false were reachable while the only way to stop deliveries was to DELETE the endpoint — which destroys the signing secret and forces every receiver to be reconfigured. Now pausing is a pause.