oto_slack_unknown_action_total
| Type | counter |
| Labels | none, deliberately (see below) |
| Registered in | internal/channels/service/metrics.go |
| Alertable | yes — any increment. Both causes are oto’s fault |
| Rule | OtoSlackUnknownAction in deploy/prometheus/oto-rules.yaml |
SPEC §H.8 spells this metric
slack_unknown_action_total, without the prefix. The code adds theoto_prefix on purpose — the SPEC line is an omission, not a second rule, and an unprefixed metric would be the only one in oto’s namespace a scrape config could not select with{__name__=~"oto_.*"}.
What it counts
Section titled “What it counts”Verified Slack interactions naming an action_id oto has no branch for. The signature was valid,
the payload was genuine, a human really did press the button — and oto had nowhere to route it.
The known ids are the four that write — oto.ack, oto.unack, oto.snooze, oto.unsnooze — plus
oto.more, plus anything under the oto.noop. prefix (the URL buttons, which Slack delivers an
interaction for and oto must acknowledge with nothing to do).
⭐
oto.morewas in NO set at all and every press of it counted here. It is the links overflow — the:package: Moremenu holding Timeline, Prometheus, Alertmanager, Rule history and Show all labels — and the renderer has always emitted it (render/slack/root.go, pinned by five goldens and named in SPEC §H.8). Matching neither the writing ids noroto.noop., it fell to the router’sdefaultarm and incremented this counter on the one element of the card that was working exactly as designed. It is now admitted to the closed set, deliberately rather than renamed tooto.noop.more: cards already sitting in Slack carry the literaloto.more, so a rename would turn every one of those menus into a silent no-op — the defect relocated, not fixed — and the overflow is not link-only anyway (see below). If you are reading this runbook because of a historical spike,oto.moreis the first thing to rule out.⭐
oto.moreIS THE ONE ID WHOSE ARM IS INERT FOR SOME OPTIONS AND NOT OTHERS, AND THE VALUE IS WHAT TELLS THEM APART. Its four URL options (Timeline, Prometheus, Alertmanager, Rule history) carry aurl, so Slack navigates them itself: oto acks and does nothing else, and a press of one enqueues no job.Show all labelscarries avalue—labels|<case id>— and is the one option that asks oto to render something, so a press of it enqueues aslack.interactionjob and is answered with an ephemeral message listing the alert’s labels (applyShowLabelsininternal/channels/service/interactions.go). Neither half touches this counter.The reply is an ephemeral, deliberately not a modal (git-bug
60e6e10, owner’s ruling). SPEC §H.8 designates that value forviews.open; a modal needs atrigger_idspent inside Slack’s three-second synchronous window, and this work runs on the queue, so thetrigger_idwould already be dead.response_urlhas no such clock. It was inert for every option until60e6e10— the value was minted, routed, and answered with silence, against §B.8.6’s “buttons are never no-ops” — and that press never incremented this counter either, correctly: it was known-and-routed, and conflating “one option does nothing” with “oto could not route this” would have put a permanent floor under a series whose whole meaning is “oto is broken”.⚠️ A
Show all labelspress that answers “oto cannot list labels from Slack in this deployment yet” is a WIRING gap, not an unknown action. The labels port (channels/service.Labels) is optional and nil-tolerant: an unwired deployment answers the press with that sentence rather than with silence. It is honest and it is not this metric’s subject — zero here is correct while that sentence is what users see. Wire the adapter ininternal/app/container.gobesideCasesandSnoozes; the compile-time proof belongs ininternal/app/assertions.gowith its neighbours, because a nil port fails at no injection site.
Why it exists, and why it is a counter rather than a log line
Section titled “Why it exists, and why it is a counter rather than a log line”An unknown action is answered 200, and must be: Slack disables an app’s event subscriptions when more than 95 % of deliveries fail inside a 60-minute window. So the endpoint deliberately reports success for a press it could not serve, and this counter is the only evidence anywhere that it happened. Without it the path is back to its original defect — an authentic press, a tick shown to the user, and no record at all.
It carries no labels on purpose. The obvious label is action_id, whose values come from a
payload oto does not author; an unknown id is by definition outside oto’s closed set, so labelling
by it hands an unbounded key space to Prometheus. The id is on the WARN log beside the increment,
which is where high-cardinality detail belongs.
What a non-zero value means
Section titled “What a non-zero value means”Exactly one of two things, and both are bugs in oto:
- A card rendered an
action_idthe consumer does not switch on — the renderer and the router disagree. - An
action_idwas renamed, which retroactively breaks every card already sitting in Slack. Cards are durable; a rename is a wire-contract break for every message already posted, forever.
The user-visible effect is worse than an error: the person sees the interaction accepted and believes they acknowledged the alert. They did not.
What to check
Section titled “What to check”- The WARN log beside the increment — it carries
action_id,slack_team_id,slack_channel_id. Theaction_idstring is the whole diagnosis. - Compare it with the constant block in
internal/channels/service/interactions.go(ActionAcknowledge,ActionUnacknowledge,ActionSnooze,ActionUnsnooze,ActionOverflow,ActionNoopPrefix) and with whatinternal/channels/render/slack/actually emits. The block is the closed set; anything the renderer emits that is not in it is this metric’s whole subject. - Did it start at a deploy?
GET /api/v1/version. A rename lands with a release. - Whether the increment came from the HTTP path or the
slack.interactionworker — both record it, because from an operator’s chair it is the same fact.
What to do
Section titled “What to do”- Renamed id: restore the old id and keep serving it. Cards already in Slack cannot be re-rendered, so the router must accept both the old and the new id indefinitely — the rename is not complete when the code ships, it is complete when the last card carrying the old id has aged out.
- Renderer/router mismatch: add the branch, or stop rendering the button. A card carrying an Acknowledge nobody consumes is worse than a card with no button at all.
- Tell the affected people their acknowledgement did not land — oto answered 200 and they have no way to know.