webhook

package
v0.75.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 15 Imported by: 0

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

View Source
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

func Send(ep Endpoint, body []byte, text string) (int, error)

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

func SendTest(ep Endpoint) (int, error)

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

func (e Endpoint) Chat() bool

Chat reports whether this endpoint takes a chat envelope rather than signed JSON.

func (Endpoint) DiscordFormat added in v0.60.0

func (e Endpoint) DiscordFormat() bool

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

func (e Endpoint) Health() string

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

func (e Endpoint) Healthy() bool

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

func (e Endpoint) SlackFormat() bool

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 Open

func Open(path string) (*Store, error)

func (*Store) Add

func (s *Store) Add(name, url, format string) (Endpoint, error)

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

func (s *Store) Delete(id string) (found bool, err error)

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

func (s *Store) DeliverAll(payload any, text string)

DeliverAll fires the payload to every enabled endpoint, async + best-effort. text is the plain-text rendering that Slack-format endpoints receive.

func (*Store) Get

func (s *Store) Get(id string) (Endpoint, bool)

func (*Store) List

func (s *Store) List() []Endpoint

func (*Store) SetEnabled added in v0.63.0

func (s *Store) SetEnabled(id string, on bool) (Endpoint, bool, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL