matterbox

matterbox / Docs / Telemetry

Telemetry

matterbox can report anonymous usage data to help work out which parts of it are useful, which are ignored, and which are quietly broken. It is off unless you turned it on, and this page is the complete list of what it sends.

# ~/.config/matterbox/config.yaml
telemetry:
  enabled: false          # nothing is sent unless this is explicitly true
  anonymous_id: ""        # random id events are grouped by; delete it for a fresh one

Setting enabled: false stops it for good, and so does removing the telemetry section entirely. Deleting anonymous_id makes your future events unlinkable to your past ones.

That config key is the only gate. The PostHog project key is compiled into the binary, release builds included — but it does nothing on its own: with the key present and consent absent, nothing is sent.

Where events go is a separate question from whether they are sent, and it has two knobs. Neither grants consent:

Knob When What it does
make POSTHOG_KEY=phc_…build timeCompiles a different project key in. Pass it empty — make POSTHOG_KEY= — to build a binary with no key at all, which can report nowhere.
MATTERBOX_POSTHOG_KEYrun timeOverrides the compiled-in key for one run, for pointing a build at your own project.
MATTERBOX_POSTHOG_HOSTrun timeOverrides the ingest host (default https://eu.i.posthog.com).

To see where all of that landed on your own machine, ask the binary: matterbox --version prints a telemetry: line reporting whether it would send anything — and if not, whether that is because nobody has answered the question, because the answer was no, or because the build carries no key.

The update check, which is not this

One other thing matterbox does over the network on its own, and it is worth naming here because this is the page people read to find out. Once a day it fetches https://matterbox.work/latest.json to see whether a newer release exists. That is not telemetry and is not behind this consent: the request carries no version, no platform and no identifier, the file answers everyone the same way, and the comparison happens on your machine — so there is nothing in it to count installs with, and it reveals no more than opening the website does. It is on by default, and update_check.enabled: false turns it off. See config.md.

The two do meet in one place. Whether people upgrade at all is a real question, and version_upgraded below is what answers it — with consent, like everything else on this page.

The setup wizard, which asks before it can send

The wizard asks the telemetry question on its last screen, so everything before it happens with the answer unknown. Its own funnel — which step you reached, how many times a step was shown, whether a sign-in was rejected — is therefore held in memory and only sent if you answer yes. Answer no, or quit before the question, and the buffer is discarded: it never reaches the network, and nothing on disk records that it existed.

The same holds for the subcommands. Telemetry is started once a verb has finished — so matterbox decode never reads a config it has no use for — which means anything the verb wanted to report is held until then and discarded if consent is absent.

What is never sent

None of the following has any representation in the event catalogue below — not as a property, not in an id, not in an error message:

Where free text could appear, and what happens to it

Two things carry prose, and both are the same thing: scrubbed error text, on the operation_failed and panic_recovered events and on the error reports below. Error strings are where private data leaks in practice — open /home/ana/.config/matterbox/messages.db: permission denied is a username, and post "shall we ship it?" rejected is a message — so every such value is put through a scrubber first, which replaces:

Pattern Becomes
URLs, including ws://<url>
Email addresses<email>
Absolute, ~-relative and Windows paths<path>
26-character Mattermost ids (post, channel, user, team, file)<id>
@mentions<mention>
Anything in single, double or back quotes<quoted>
Runs of 7+ digits<num>
Alphanumeric runs of 32+ characters<token>

What survives is the part that says what went wrong (permission denied), capped at 200 characters. If nothing but placeholders is left, the value is replaced with <redacted> — the event still records that something failed.

Stack frames on panic_recovered are filtered to functions inside the matterbox module — internal/ui.(*Model).renderMessages — with no arguments, no file paths, and no frames from the standard library or dependencies.

Error reports

Crashes and failures are also sent to PostHog’s error-tracking view, as $exception records rather than as events. They are the same consent and the same anonymous id — telemetry off means none of this happens — and they exist because an event can say that something broke while only a stack can say where.

Not every failure becomes one. A panic always does. A failed operation does only when its class says the fault is ours — a parse error, a bad config, an internal error — and never when the class says it was the world: network, server, rate_limited, auth, permission, not_found. Those are counted by operation_failed and go no further, because a dropped connection is a number rather than a bug, and thousands of them would bury the real ones.

A report carries:

Part What it holds
TitleThe failure site (store.migrate) or panic in <function>. Both from the fixed lists on this page.
DescriptionScrubbed error or panic text, exactly as described above.
where, classThe same closed sets the events use, validated the same way.
handledWhether matterbox carried on afterwards.
os, arch, version, build_tagsThe machine and the build, as on app_started.
Stackmatterbox frames only: internal/ui/view.go:1602 internal/ui.(*Model).View.

The stack is the part worth being precise about, because it is the part that could leak. The PostHog SDK builds one for you, and matterbox does not use it: the SDK records each frame’s file path as the compiler saw it, which on a matterbox built from source is an absolute path through your home directory, and it attaches raw instruction addresses and an identifier for your executable besides.

So frames are rebuilt from scratch, and the rule is the opposite one: a frame is dropped unless it can be proved to belong to matterbox, by reducing its recorded path against this build’s own module root. A frame from the standard library, a dependency or a vendored tree does not reduce, so it is dropped rather than filtered — anything unrecognised fails closed. What survives is a file and line of a public repository. Nothing identifies the machine, and because no executable identifier is sent, PostHog’s source-context feature stays dark: that is the trade, and it is deliberate.

One session may send at most 25 reports, and at most 3 of any one issue, so a failure inside a retry loop cannot flood.

What PostHog collects on its own account

Being complete means naming the data we do not choose to send.

Events are delivered to PostHog Cloud EU over HTTPS, so PostHog’s servers see the connecting IP address in transit, as any service you talk to does. It goes no further: the project is configured to discard client IPs, so the address is never stored with an event — transformations such as bot detection may use it, then it is thrown away. Your IP is not something matterbox puts in an event either. Location lookup is off regardless: every event carries $geoip_disable, which is the Go SDK’s default and is not overridden here, so no country, region or city is derived from that address. PostHog does stamp each event with a receive time.

The SDK also stamps four properties of its own on every event, before matterbox sees the payload and with no setting to turn them off:

Property Example Note
$osLinuxDuplicates the catalogue’s os.
$os_version44The distribution’s release, which the catalogue does not ask for.
$os_distroFedora Linux Asahi RemixThe distribution’s name, which the catalogue does not ask for.
$go_versiongo1.26.6-X:nodwarf5The untrimmed toolchain string.

The last one is worth calling out, because it partly undoes a decision made here: the catalogue’s own go_version is trimmed to go1.26.6 precisely because the full string can carry local toolchain flags that describe someone’s build environment. The SDK sends the untrimmed one alongside it. This page would rather say so than claim a completeness it does not have.

How the guarantees above are enforced

Not by convention. Every event and property is declared in internal/telemetry/catalogue_events.go, and the sending path checks each value against its declaration before queuing it:

So a call site that accidentally passed a message body would not leak it: the value would fail validation and never be sent. In tests the same violations panic instead, so a mistake fails CI rather than shipping. This page is generated from those declarations, which is why it can claim to be complete.

Error reports do not go through the catalogue — they are not events — so they get the same treatment by a different route: every label on one is checked against the same closed sets, the message goes through the same scrubber, and every stack frame has to prove it is matterbox’s own code. The section above describes what that leaves. The properties the SDK adds by itself are outside both mechanisms, and are named in full below.

Events

29 events, 29 of them sent by the current build.

Two events carry most of the volume. usage_snapshot is a periodic tally of everything too frequent to send individually — keystrokes, clicks, commands — flushed every 5 minutes and once at exit. Everything else is sent as it happens, because for those the ordering is part of the answer.

app_started

The TUI launched and finished its first layout.

Why: Establishes the denominator for every other number, and describes the environment matterbox actually runs in: terminal, graphics support, window size, and which optional features are configured on. Most “this feature is unused” findings turn out to be “this feature is unavailable”, and this is the event that tells the two apart.

Property Type Meaning
versionbuild stringmatterbox version / git describe output of the build.
build_tagsbuild stringOptional build tags compiled in (e.g. video), comma separated.
go_versionbuild stringGo toolchain the binary was built with.
osenumOperating system. One of linux, darwin, windows, freebsd, openbsd, netbsd, other.
archenumCPU architecture. One of amd64, arm64, arm, 386, other.
terminalenumTerminal emulator, recognised from a fixed list of $TERM_PROGRAM / $TERM values. An unrecognised terminal reports other, never the raw variable.
image_protocolenumTerminal graphics protocol available for image, emoji and video rendering.
colsenumTerminal width, bucketed. Decides whether the three-pane layout fits.
rowsenumTerminal height, bucketed.
features_onenum listWhich optional features are enabled in config — adoption of a feature can only be read against how many people have it switched on.
mouse_enabledboolWhether mouse support is on.
nav_modifierenumConfigured modifier for arrow-key sidebar navigation. One of ctrl, alt, shift, super, meta, hyper, none.
overridden_actionsenum listWhich actions have a custom keybinding. A default people keep rebinding is a default that is wrong.
teamsenumNumber of teams the account is in, bucketed.
channelsenumNumber of channels in the sidebar, bucketed. Sidebar and switcher design depends on this and we are guessing at it today.
first_runboolWhether this launch immediately followed the setup wizard.
startup_msenumTime from process start to the first rendered frame, bucketed.
cache_warmboolWhether the local message cache had content to render before the network answered.

app_stopped

The TUI exited. Sent with a final usage_snapshot on the way out.

Why: Session length and what a session contained is the retention picture: whether people keep matterbox open all day (in which case background behaviour and notifications matter most) or dip in and out (in which case startup time and the unread feed matter most).

Property Type Meaning
sessionenumHow long the session lasted, bucketed.
reasonenumHow the session ended. One of quit, signal, error, unknown.
actionsenumTotal keyboard actions in the session, bucketed.
messages_sentenumMessages sent in the session, bucketed.
channels_openedenumDistinct conversations opened, bucketed.

usage_snapshot

A periodic tally of everything counted rather than reported individually: keyboard actions, mouse targets, palette and slash commands, feature use, and friction signals. Flushed every few minutes and once more at exit.

Why: Answers “what is used and what is dead” across the entire surface — all 79 keybindings, every clickable region, every command — without sending an event per keystroke. A key nobody presses shows up as a permanent absence from these maps, which is exactly the finding that lets a keymap this large be pruned. The trade is deliberate: counts are cheap and complete, but they carry no ordering, so anything where sequence matters is a discrete event above instead.

Property Type Meaning
windowenumHow long this tally covers, bucketed.
finalboolWhether this is the last snapshot of a session (sent alongside app_stopped).
actionscounter mapKeyboard action id → how many times it fired.
actions_usedenum listThe same action ids as a flat list, so PostHog can break down “was this ever used” without a HogQL query over the map.
mousecounter mapClick target → how many times it was clicked.
palettecounter mapCommand-palette entry id → how many times it ran.
slashcounter mapBuilt-in slash command → how many times it ran. Server and plugin commands count as server.
featurescounter mapFeature id → how many times it was used.
frictioncounter mapFriction signal → how many times it occurred. See the friction events below for what each one means.
surfacescounter mapPane → how many actions happened while it was focused, i.e. where the time goes.

version_upgraded

The first launch of a build whose version differs from the last one seen.

Why: Tells us whether people update at all, and how long a release takes to reach them — without which “this bug is fixed” and “nobody is running the fix” look identical in the data.

Property Type Meaning
frombuild stringPreviously recorded version.
tobuild stringVersion now running.

setup_step

The setup wizard displayed a step.

Why: The wizard is the whole first impression, and a fresh install that fails here never becomes a user. Step-by-step events make it a funnel, so the step people abandon is visible instead of inferred.

Property Type Meaning
stepenumWhich wizard step. One of server, login, advanced, telemetry.
attemptcountHow many times this step has been shown this run — a step shown three times is a step being fought with.

setup_finished

The setup wizard completed or was abandoned.

Why: Closes the activation funnel: what fraction of fresh installs reach a working login, how long it takes, and where the rest stop.

Property Type Meaning
outcomeenumWhether setup reached a working login. One of completed, abandoned.
last_stepenumThe step it ended on. Always the telemetry question for a wizard that was answered — which is the only kind that can report anything at all — so this is here for a future step order rather than for today’s. One of server, login, advanced, telemetry.
durationenumHow long setup took, bucketed.
auth_methodenumHow the login was obtained. One of password, oauth, token, none.
telemetry_opt_inboolThe answer to the telemetry question. Recorded only when the answer was yes — a no sends nothing at all, so this property is always true and exists to make the opt-in rate legible next to install counts.

login_failed

A login attempt was rejected.

Why: Login failures are invisible to us today and are the most likely reason a new install is abandoned. The class of failure separates “our OAuth flow is broken” from “they typed the wrong password”.

Property Type Meaning
methodenumWhich login route. One of password, oauth, token.
classenumFailure class.
mfaboolWhether the server asked for MFA.

channel_opened

A conversation was opened and rendered.

Why: The central navigation question: how people get to a conversation, and whether it appears fast. via is the payoff — it ranks the sidebar against the switcher against the keyboard jumps against outside entry points, and a route nobody uses is a feature to fix or delete. The timing properties are the only field measurement of the warm-cache render path.

Property Type Meaning
viaenumHow the conversation was reached.
channel_typeenumKind of conversation. Never its name or id.
was_unreadboolWhether it had unread messages.
cacheenumWhether the local cache could render it before the network answered. One of warm, cold, partial.
render_msenumTime to the first rendered frame of the conversation, bucketed.
postsenumPosts rendered, bucketed.

message_sent

A message was sent successfully.

Why: The core action of the product, and the properties describe how people compose rather than what they write: whether replies are threaded, whether attachments and code blocks are common, how long messages are. That decides where composer effort belongs. No part of the text is sent — only its shape.

Property Type Meaning
surfaceenumWhere it was composed. One of composer, thread, feed_reply, cli, control_socket.
is_replyboolWhether it is a thread reply.
is_nested_replyboolWhether it carries a matterbox nested-reply parent.
lengthenumMessage length in characters, bucketed. The text itself is never sent.
linesenumNumber of lines, bucketed.
attachmentsenumAttachment count, bucketed.
mentionsenumCount of @mentions, bucketed. The names are not sent.
has_code_blockboolWhether it contains a fenced code block.
has_linkboolWhether it contains a URL. The URL is not sent.
has_emojiboolWhether it contains an emoji shortcode.
has_effectboolWhether it carries a matterbox text effect.
send_msenumRound trip to the server, bucketed.

message_acted

A message was edited, deleted, reacted to, copied, collapsed or saved.

Why: These are the actions the transcript exists to support, and several of them (edit history, collapse, code copy) were expensive to build with no evidence anyone uses them. age matters for edit and delete: editing something from three days ago is a different feature from fixing a typo ten seconds later.

Property Type Meaning
actionenumWhat was done.
ownboolWhether it was the user’s own message.
ageenumHow old the message was, bucketed.
outcomeenumResult.
reaction_slotenumFor a reaction: which slot of the configured quick-reaction bar it came from, or that it was searched for. The emoji itself is not sent — the slot is what tells us whether the default bar holds the right five.
viaenumHow the action was invoked. One of key, mouse, palette, picker, cli.

thread_opened

A thread pane was opened on a message.

Why: Threading is the feature most likely to be either central or ignored, and we do not know which. Reply depth and the nested-reply flag also say whether the matterbox-only nested reply tree is worth its complexity.

Property Type Meaning
viaenumHow the thread was opened. One of key, mouse, feed, permalink, reply, unknown.
repliesenumReplies in the thread, bucketed.
nestedboolWhether the thread contains matterbox nested replies.
depthenumDeepest nesting level, bucketed.

search_run

A search was executed.

Why: There are four search backends (server FTS, local FTS, semantic, and the agentic AI search) and no evidence about which earns its keep. Result count and latency per mode say which one finds things and which one is slow; the query itself is never sent, only how many words it had.

Property Type Meaning
modeenumWhich backend ran. One of server, local_fts, semantic, hybrid, ai.
scopeenumSearch scope. One of channel, all, feed.
fromenumHow it was started. One of key, palette, slash, cli, unknown.
termsenumNumber of words in the query, bucketed. The query text is never sent.
had_operatorsboolWhether the query used search operators (from:, in:, quotes).
resultsenumHits returned, bucketed.
latency_msenumTime to results, bucketed.
outcomeenumResult.

search_result_opened

A search hit was opened.

Why: The other half of search quality: a search that returns fifty results nobody opens has failed, and rank says whether the ranking is any good. Together with search_run this is a funnel — searched, opened, or gave up.

Property Type Meaning
modeenumWhich backend produced the hit. One of server, local_fts, semantic, hybrid, ai.
rankenum1-based position of the opened hit, bucketed. Says whether the top result is the right one.
dwellenumTime between results appearing and the hit being opened, bucketed.

feed_used

An action was taken in the unread feed.

Why: The feed is matterbox’s own idea rather than a Mattermost concept, so whether it is the main way people triage — or an unused tab — is worth knowing before more is built on it.

Property Type Meaning
actionenumWhat was done. One of opened, mark_read, mark_all_read, reply, open_channel, toggle_muted, refresh.
itemsenumUnread items in the feed at the time, bucketed.
viaenumHow it was invoked. One of key, mouse, palette, unknown.

feature_used

A named feature was used, with the outcome and how long it took where that applies. The counted equivalent lives in usage_snapshot’s features map; this event exists for the features whose outcome matters, not just their count.

Why: Direct answer to “what did we build that nobody uses, and what fails for the people who do try it”. Adoption alone can be read from the snapshot; a feature that is used but errors half the time needs the outcome dimension to be visible at all.

Property Type Meaning
featureenumWhich feature.
outcomeenumResult.
latency_msenumHow long it took, bucketed, where the feature is slow enough to matter.
sizeenumA feature-specific magnitude, bucketed: posts summarised, rows returned, suggestions offered, messages indexed.
viaenumHow it was invoked. Separates “nobody wants this” from “nobody can find this”. One of key, mouse, palette, slash, cli, auto, unknown.
error_classenumFailure class when the outcome is an error.

forge_action

A Jira, GitLab or GitHub action was performed from the reference panel.

Why: The forge integrations are the largest optional subsystem in the app. Whether anyone changes a Jira status or approves a merge request from matterbox — rather than just reading the panel — decides whether the write paths were worth building and which ones to extend.

Property Type Meaning
providerenumWhich provider. Never the instance URL, project or issue key. One of jira, gitlab, github.
actionenumWhat was done.
outcomeenumResult.
latency_msenumAPI round trip, bucketed.
error_classenumFailure class when it failed.

attachment_added

A file was attached to a message.

Why: Three separate paths exist (paste, drag-and-drop, picker) and we do not know whether any of them is discoverable. Size and kind say whether the upload path needs progress feedback.

Property Type Meaning
viaenumHow the file was attached. One of paste, drop, picker, cli.
kindenumCoarse file kind, from the extension. The filename is never sent. One of image, video, audio, pdf, text, archive, other.
sizeenumFile size, bucketed.
countenumAttachments now pending, bucketed.
outcomeenumResult.

media_rendered

An image, animated emoji or video frame was drawn in the terminal.

Why: Terminal graphics are the most fragile thing matterbox does and the most likely to be silently broken on a given terminal. Pairing the protocol with the outcome shows which terminals media actually works on rather than which ones we hope it works on.

Property Type Meaning
kindenumWhat was rendered. One of image_preview, inline_image, emoji_image, video, thumbnail.
protocolenumGraphics protocol used.
outcomeenumResult.
decode_msenumDecode plus transmit time, bucketed.
error_classenumFailure class when it failed.

unhandled_key

A keypress in a reading pane matched no binding and was not text input.

Why: The best available proxy for a broken mental model: the person expected that key to do something here and it did nothing. Aggregated by key and surface it names the exact bindings people expect but do not have — which is a far better source of keymap changes than our own intuition. bound_elsewhere separates “invented a key” from “knows the key, wrong pane”, and the latter is usually a bug in where we scoped the binding.

Property Type Meaning
keyenumThe keystroke, from a fixed list of non-text keys (modified keys, function keys, navigation). Plain typed characters are never reported, so nothing here can spell out content.
surfaceenumWhich pane had focus.
bound_elsewhereboolWhether that key is bound to something in a different pane.

friction

One of the counted friction signals crossed the threshold that makes it worth reporting on its own: a help lookup after a dead key, an escape cascade, a mashed action, an abandoned picker, a discarded draft.

Why: Turns the friction counters into something with context. The counter says how often people get stuck; this says where, and in what, so the fix has an address. This is the “what needs better UX” half of the brief.

Property Type Meaning
signalenumWhich signal.
surfaceenumWhere it happened.
actionenumWhich action was involved, for the action-specific signals.
countcountHow many repetitions triggered it (escapes in the cascade, presses in the mash).
dwellenumHow long the person spent before giving up, bucketed.
sizeenumFor a discarded draft, how much was typed before it was thrown away. The text is never sent.

slow_frame

A render took long enough to be perceptible.

Why: Render cost is the recurring performance problem in this codebase and it has only ever been measured locally, on one machine, against one cache. This is the field version: which pane, at which terminal size, with how much history loaded. It is rate-limited so a slow session cannot flood.

Property Type Meaning
surfaceenumFocused pane at the time.
msenumFrame time, bucketed.
postsenumPosts loaded in the transcript, bucketed.
colsenumTerminal width, bucketed.
causeenumWhat the frame was doing. One of render, resize, image, animation, unknown.

ws_disconnected

The Mattermost websocket dropped.

Why: Silent disconnects are the worst failure this client has: the UI looks fine and messages stop arriving. Knowing how often it happens in the field, and after how long a healthy connection, is the only way to tell a flaky network from a bug in our reconnect logic. cause is what draws that line: a ping timeout means the socket stopped producing without erroring, which is a half-open link or a reader of ours that stalled — the latter is ours to fix, and classifies identically to a plain network drop without this.

Property Type Meaning
classenumWhy it dropped.
connectedenumHow long it had been connected, bucketed.
cleanboolWhether it was a clean close.
causeenumHow the socket ended, which the class can’t tell apart. One of ping_timeout, read_error, closed.

ws_reconnected

The websocket came back.

Why: Completes the disconnect picture: how long people spend disconnected, how many attempts it takes, and whether the catch-up resync works — a reconnect that recovers the socket but not the missed messages is still a failure.

Property Type Meaning
attemptsenumReconnect attempts needed, bucketed.
downtimeenumTime disconnected, bucketed.
resyncenumWhether missed messages were recovered. One of none, partial, full, failed.

operation_failed

An operation that the user was waiting on failed.

Why: The general reliability signal, grouped by which subsystem broke rather than by error string. where is a short label written by hand at the call site; detail is scrubbed error text, kept because the shape of a failure is often the only clue and dropped to a placeholder when nothing in it was safe.

Property Type Meaning
whereenumWhich operation, from a fixed list of hand-written labels.
classenumFailure class.
statuscountHTTP status code, for API failures. 0 when there wasn’t one.
retriedboolWhether it was retried.
user_visibleboolWhether the user saw an error, as opposed to it being handled silently.
detailscrubbed textScrubbed error text: paths, URLs, ids, quoted strings, mentions and tokens are replaced with placeholders before it leaves the machine. See the privacy section.

panic_recovered

A panic was caught rather than taking the process down.

Why: A crash a user works around is a crash we never hear about. The matterbox stack frames are enough to find the bug, and they are code from a public repository rather than anything about the person running it.

Property Type Meaning
whereenumWhich subsystem recovered it.
framesstack framesmatterbox stack frames, innermost first. Only functions in the matterbox module are kept — no arguments, no file paths, no dependency or standard-library frames.
detailscrubbed textScrubbed panic value.

cli_command

A matterbox <verb> subcommand ran to completion.

Why: There are twenty-odd subcommands and no idea which are used. Some exist only to be called from scripts and notification handlers, where nobody would ever report a problem — so a verb that always fails could have been broken for months. This is the cheapest high-value event in the catalogue.

Property Type Meaning
commandenumWhich subcommand.
outcomeenumResult.
duration_msenumWall-clock duration, bucketed.
ttyboolWhether stdout was a terminal — i.e. a person ran it, rather than a script.
error_classenumFailure class when it failed.

daemon_started

sent only by the matterbox listen daemon

The matterbox listen daemon started.

Why: The daemon runs unattended for weeks; its configuration is invisible to us and its rules engine is the most complex config surface in the product. How many rules people write, and which delivery channels they wire up, decides where that subsystem goes next.

Property Type Meaning
versionbuild stringBuild running.
rulescountNumber of configured rules.
channels_onenum listWhich delivery and rule capabilities are configured. One of desktop, telegram, two_way, digest, exec, summarize.

rule_fired

sent only by the matterbox listen daemon

A listen rule matched and its actions ran.

Why: Says which rule kinds are worth their complexity — and, through the outcome, whether the exec and notify actions people rely on are actually succeeding on an unattended machine. Rule names are user-written and are never sent; only the action types are.

Property Type Meaning
actionenumAction type that ran. Never the rule’s name or its command line.
outcomeenumResult.
triggerenumWhat triggered the rule. One of message, mention, dm, reaction, schedule, other.

notification_actioned

sent only by the matterbox listen daemon

A delivered notification was acted on.

Why: The desktop notification buttons and inline reply took real work and sit outside the app where nothing can be observed. This says whether anyone presses them, and whether the action then succeeds.

Property Type Meaning
channelenumWhere the notification was delivered. One of desktop, telegram.
actionenumWhat the user did with it. One of read, react, reply, open, dismissed, expired.
outcomeenumResult of carrying it out.

Value sets

The closed sets the properties above draw from. A property can hold nothing outside its set — that is checked at runtime, not just documented.

Count buckets

Used wherever a number describes user content — posts, results, mentions, replies.

0, 1, 1000+, 101-1000, 2-5, 21-100, 6-20

Length buckets

Characters of text. Used for message and query length; the text itself is never sent.

0, 1-40, 161-500, 2000+, 41-160, 501-2000

Duration buckets (ms)

Latencies and frame times.

1-10ms, 1-5s, 10-50ms, 200ms-1s, 30s+, 5-30s, 50-200ms, <1ms

Duration buckets (s)

Human-scale spans: session length, how long a picker stayed open, message age.

1-4h, 10-60m, 2-10m, 30s-2m, 4h+, 5-30s, <5s

Terminal width buckets

The layout’s own breakpoints, not round numbers.

120-159, 160-199, 200-279, 280+, 80-119, <80

Terminal height buckets

100+, 24-39, 40-59, 60-99, <24

File size buckets

1-10MB, 10-100MB, 100MB+, 64KB-1MB, <64KB

Result rank buckets

Position of an opened search hit, so “is the top result right” is answerable.

1, 11-50, 2-3, 4-10, 50+

Contexts

The layers of the key-handling ladder — the panes, modes and modals that can own a keystroke. Taken from the keyContexts table in internal/ui/contexts.go, which is the app’s own tested model of which bindings are live where.

focus:attachments, focus:feed, focus:info, focus:info-media, focus:input, focus:messages, focus:ref, focus:search, focus:sql, focus:sqlresults, focus:teams, focus:thread, global:command-picker, global:nav, global:reading, global:switcher-chord, global:team-jump, modal:channel-form, modal:code-picker, modal:confirm, modal:delete-confirm, modal:game, modal:history, modal:image-preview, modal:jira-comment, modal:jira-picker, modal:jira-points, modal:kaomoji-picker, modal:key-debug, modal:keys-sheet, modal:open-picker, modal:poll-dialog, modal:reaction-picker, modal:saved-posts, modal:stl-view, modal:summary, modal:switcher, modal:template-picker, modal:text-popup, mode:filter, unknown, welcome

Channel types

The kind of conversation. Never its name or id.

dm, group_dm, private, public, unknown

Open routes

How a conversation was reached.

cli, dm_jump, feed, filter, nav_key, notification, palette, permalink, restore, search_hit, sidebar_key, sidebar_mouse, switcher, team_jump, unknown, unread_jump

Outcomes

cancelled, denied, empty, error, ok, timeout, unavailable

Error classes

Grouped by what a user would have to do about them.

auth, config, disk, internal, network, not_found, parse, permission, rate_limited, server, unknown, unsupported

Image protocols

Terminal graphics support.

iterm, kitty, none, sixel

Reportable keys

The only keystrokes unhandled_key may report: non-text keys, so a stream of them cannot reconstruct anything typed.

alt+0, alt+1, alt+2, alt+3, alt+4, alt+5, alt+6, alt+7, alt+8, alt+9, alt+a, alt+b, alt+backspace, alt+c, alt+d, alt+down, alt+e, alt+enter, alt+f, alt+g, alt+h, alt+i, alt+j, alt+k, alt+l, alt+left, alt+m, alt+n, alt+o, alt+p, alt+q, alt+r, alt+right, alt+s, alt+t, alt+u, alt+up, alt+v, alt+w, alt+x, alt+y, alt+z, backspace, ctrl+0, ctrl+1, ctrl+2, ctrl+3, ctrl+4, ctrl+5, ctrl+6, ctrl+7, ctrl+8, ctrl+9, ctrl+a, ctrl+alt+a, ctrl+alt+b, ctrl+alt+c, ctrl+alt+d, ctrl+alt+e, ctrl+alt+f, ctrl+alt+g, ctrl+alt+h, ctrl+alt+i, ctrl+alt+j, ctrl+alt+k, ctrl+alt+l, ctrl+alt+m, ctrl+alt+n, ctrl+alt+o, ctrl+alt+p, ctrl+alt+q, ctrl+alt+r, ctrl+alt+s, ctrl+alt+t, ctrl+alt+u, ctrl+alt+v, ctrl+alt+w, ctrl+alt+x, ctrl+alt+y, ctrl+alt+z, ctrl+b, ctrl+backspace, ctrl+c, ctrl+d, ctrl+down, ctrl+e, ctrl+end, ctrl+f, ctrl+g, ctrl+h, ctrl+home, ctrl+i, ctrl+j, ctrl+k, ctrl+l, ctrl+left, ctrl+m, ctrl+n, ctrl+o, ctrl+p, ctrl+q, ctrl+r, ctrl+right, ctrl+s, ctrl+t, ctrl+u, ctrl+up, ctrl+v, ctrl+w, ctrl+x, ctrl+y, ctrl+z, delete, down, end, enter, esc, f1, f10, f11, f12, f2, f3, f4, f5, f6, f7, f8, f9, home, insert, left, other, pgdown, pgup, right, shift+down, shift+enter, shift+left, shift+right, shift+tab, shift+up, space, tab, up

Action ids

Every rebindable action in the keymap. The usage snapshot counts each one, which is how a binding nobody uses becomes visible.

apply_open, attachment_remove, bottom, cancel_edit, channel_info, channel_next, channel_prev, clear_filter, clear_input, close_thread, collapse_message, command_picker, compose, confirm_no, confirm_yes, copy_code_block, copy_markdown, copy_selection, cut_selection, delete_post, down, download_attachment, edit_history, edit_post, feed_mark_all_read, feed_reply, feed_toggle_muted, filter, focus_next, focus_prev, goto_dm, goto_feed, goto_parent, goto_team, help, input_down, input_up, jira_assignee, jira_comment, jira_points, jira_priority, jira_reply, jira_status, leave_input, left, load_team, mark_read, move_team_left, move_team_right, newline, next_match, open_attachment, open_channel, open_reference, open_thread, page_down, page_up, paste, prev_match, prev_own_message, preview_image, quit, react, redo, ref_approve, ref_jobs, ref_merge, refresh, reply_in_thread, right, search_all, search_here, select_down, select_left, select_right, select_up, send, sheet_remove, switcher, team_next, team_prev, top, undo, up

Mouse targets

The clickable regions of the UI.

channel, composer, feed, feed_blobs, feed_mark_all, info, jump_bottom, message, nothing, reference, search, sql, tab, thread, toast

Command palette ids

The “>” palette entries, by stable id — several display names interpolate a channel name, which is never sent.

channel_mute, channel_unmute, copy_channel_link, copy_message_link, create_channel, debug_copy_channel_id, debug_copy_message_id, debug_key_inspector, feed_hide_muted, feed_mark_all_read, feed_show_muted, gorillas, gorillas_hotseat, image_click, index_channel, join_channel, keys, kurve, kurve_hotseat, mark_unread_post, message_stats, rejoin_game, saved_messages, sidebar_all_channels, sidebar_unread_channels, start_group_dm, status_away, status_custom_clear, status_custom_set, status_dnd, status_offline, status_online, summarize, typing_animation, view_profile

Slash commands

Built-in “/” commands. Server and plugin commands count as server without their trigger word, since those names are organisation-specific.

bad, copy, dm, glow, help, kaomoji, me, ok, pulse, rainbow, scroll, search, server, shimmer, shrug, spoiler, tmpl, underline, unknown, warn, whisper

Feature ids

The features whose adoption is counted.

ai_search, attachments_drop, attachments_paste, channel_create, channel_edit, channel_join, cheatsheet, code_copy, collapse, custom_status, digest, download, drafts, embed_index, emoji_images, feed, games, github, gitlab, grammar_check, group_dm, image_preview, jira, kaomoji, key_inspector, markdown_table, message_stats, mouse_selection, nested_reply, permalink, polls, reactions, rules, saved_messages, semantic_search, sql_tab, summary, templates, text_effects, video_preview

Friction signals

The counted signs that something was not understood. See the friction event for what each means.

action_repeated, composer_discarded, delete_cancelled, esc_cascade, help_after_unhandled, picker_abandoned, resize_storm, scroll_wall, search_abandoned, search_empty, send_failed, slow_frame, undo_after_edit, unhandled_key, unhandled_key_bound_elsewhere

Failure sites

Hand-written labels naming where something failed. Constants in the source, never derived from data.

api.channels, api.files, api.other, api.posts, api.prefs, api.reactions, api.search, api.status, api.teams, api.users, auth.login, auth.oauth, auth.refresh, auth.token, cli.other, clipboard, config.load, config.save, config.schema, control_socket, embed.index, embed.server, github.api, gitlab.api, jira.api, listen.other, listen.rule, llm.request, llm.tools, media.decode, media.download, media.transmit, media.upload, notify, opener, render, store.fts, store.migrate, store.open, store.purge, store.query, store.vector, store.write, telegram, ui.other, ws.auth, ws.connect, ws.read, ws.resync

CLI commands

channels, decode, digest, embed, github, keys, listen, login, mark-read, open, react, read, register-handler, reply, rules, search, send, unread, upgrade, url-handler, welcome, whoami

Checking for yourself

Don’t take this page’s word for it. Point a build at a PostHog project you own, using the run-time knobs from the table above, and read exactly what arrives:

$ export MATTERBOX_POSTHOG_KEY=phc_your_own_key
$ export MATTERBOX_POSTHOG_HOST=https://eu.i.posthog.com
$ make run

Consent still applies — telemetry.enabled must be true. Grepping the source for telemetry. shows every call site; each one resolves to a typed emitter in internal/telemetry/emit.go, and every emitter maps to an event on this page.