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 oneSetting 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 time | Compiles 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_KEY | run time | Overrides the compiled-in key for one run, for pointing a build at your own project. |
MATTERBOX_POSTHOG_HOST | run time | Overrides 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:
- Message text. No message body, draft, edit or search
query. Where the size of something matters it is bucketed
(
501-2000characters), never quoted. - Names. No username, display name, nickname or email — yours or anyone else’s. Mentions are counted, never named.
- Channels and teams. No channel or team name, id, purpose or member list. The kind of conversation (public, private, DM, group DM) is sent, because “people live in DMs” changes what the sidebar should do — and it names nobody.
- Your server. No Mattermost URL, hostname or instance name, so events cannot be tied to an employer.
- Files and paths from your machine. No filename you
opened, no directory, no home directory. File kind and bucketed
size only. The one kind of path that is sent is a source path from
matterbox’s own public repository —
internal/ui/view.go:1602, in a crash report — which is a line of code, not a thing on your disk. See the error-report section. - Credentials. No token, password or session id.
- Keystrokes as text. No single-character keystroke is
ever reported. The one event about keys (
unhandled_key) draws from a fixed list containing only multi-character keystrokes —ctrl+w,f5,pgup,alt+enter— and fires only when no text input has focus. An unhandled bare letter is reported asother: that a key did nothing, and where, but not which one. A stream of these cannot spell anything.
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 |
|---|---|
| Title | The failure site (store.migrate) or panic in <function>. Both from the fixed lists on this page. |
| Description | Scrubbed error or panic text, exactly as described above. |
where, class | The same closed sets the events use, validated the same way. |
handled | Whether matterbox carried on afterwards. |
os, arch, version, build_tags | The machine and the build, as on app_started. |
| Stack | matterbox 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 |
|---|---|---|
$os | Linux | Duplicates the catalogue’s os. |
$os_version | 44 | The distribution’s release, which the catalogue does not ask for. |
$os_distro | Fedora Linux Asahi Remix | The distribution’s name, which the catalogue does not ask for. |
$go_version | go1.26.6-X:nodwarf5 | The 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:
- An event name not in the catalogue is dropped.
- A property not declared on that event is dropped.
- An enum value outside its declared set is dropped — and almost every property is an enum, including every bucketed number, so the full set of values a property can ever hold is finite and listed on this page.
- Counter map keys outside their whitelist are dropped.
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 |
|---|---|---|
version | build string | matterbox version / git describe output of the build. |
build_tags | build string | Optional build tags compiled in (e.g. video), comma separated. |
go_version | build string | Go toolchain the binary was built with. |
os | enum | Operating system. One of linux, darwin, windows, freebsd, openbsd, netbsd, other. |
arch | enum | CPU architecture. One of amd64, arm64, arm, 386, other. |
terminal | enum | Terminal emulator, recognised from a fixed list of $TERM_PROGRAM / $TERM values. An unrecognised terminal reports other, never the raw variable. |
image_protocol | enum | Terminal graphics protocol available for image, emoji and video rendering. |
cols | enum | Terminal width, bucketed. Decides whether the three-pane layout fits. |
rows | enum | Terminal height, bucketed. |
features_on | enum list | Which optional features are enabled in config — adoption of a feature can only be read against how many people have it switched on. |
mouse_enabled | bool | Whether mouse support is on. |
nav_modifier | enum | Configured modifier for arrow-key sidebar navigation. One of ctrl, alt, shift, super, meta, hyper, none. |
overridden_actions | enum list | Which actions have a custom keybinding. A default people keep rebinding is a default that is wrong. |
teams | enum | Number of teams the account is in, bucketed. |
channels | enum | Number of channels in the sidebar, bucketed. Sidebar and switcher design depends on this and we are guessing at it today. |
first_run | bool | Whether this launch immediately followed the setup wizard. |
startup_ms | enum | Time from process start to the first rendered frame, bucketed. |
cache_warm | bool | Whether 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 |
|---|---|---|
session | enum | How long the session lasted, bucketed. |
reason | enum | How the session ended. One of quit, signal, error, unknown. |
actions | enum | Total keyboard actions in the session, bucketed. |
messages_sent | enum | Messages sent in the session, bucketed. |
channels_opened | enum | Distinct 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 |
|---|---|---|
window | enum | How long this tally covers, bucketed. |
final | bool | Whether this is the last snapshot of a session (sent alongside app_stopped). |
actions | counter map | Keyboard action id → how many times it fired. |
actions_used | enum list | The same action ids as a flat list, so PostHog can break down “was this ever used” without a HogQL query over the map. |
mouse | counter map | Click target → how many times it was clicked. |
palette | counter map | Command-palette entry id → how many times it ran. |
slash | counter map | Built-in slash command → how many times it ran. Server and plugin commands count as server. |
features | counter map | Feature id → how many times it was used. |
friction | counter map | Friction signal → how many times it occurred. See the friction events below for what each one means. |
surfaces | counter map | Pane → 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 |
|---|---|---|
from | build string | Previously recorded version. |
to | build string | Version 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 |
|---|---|---|
step | enum | Which wizard step. One of server, login, advanced, telemetry. |
attempt | count | How 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 |
|---|---|---|
outcome | enum | Whether setup reached a working login. One of completed, abandoned. |
last_step | enum | The 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. |
duration | enum | How long setup took, bucketed. |
auth_method | enum | How the login was obtained. One of password, oauth, token, none. |
telemetry_opt_in | bool | The 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 |
|---|---|---|
method | enum | Which login route. One of password, oauth, token. |
class | enum | Failure class. |
mfa | bool | Whether 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 |
|---|---|---|
via | enum | How the conversation was reached. |
channel_type | enum | Kind of conversation. Never its name or id. |
was_unread | bool | Whether it had unread messages. |
cache | enum | Whether the local cache could render it before the network answered. One of warm, cold, partial. |
render_ms | enum | Time to the first rendered frame of the conversation, bucketed. |
posts | enum | Posts 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 |
|---|---|---|
surface | enum | Where it was composed. One of composer, thread, feed_reply, cli, control_socket. |
is_reply | bool | Whether it is a thread reply. |
is_nested_reply | bool | Whether it carries a matterbox nested-reply parent. |
length | enum | Message length in characters, bucketed. The text itself is never sent. |
lines | enum | Number of lines, bucketed. |
attachments | enum | Attachment count, bucketed. |
mentions | enum | Count of @mentions, bucketed. The names are not sent. |
has_code_block | bool | Whether it contains a fenced code block. |
has_link | bool | Whether it contains a URL. The URL is not sent. |
has_emoji | bool | Whether it contains an emoji shortcode. |
has_effect | bool | Whether it carries a matterbox text effect. |
send_ms | enum | Round 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 |
|---|---|---|
action | enum | What was done. |
own | bool | Whether it was the user’s own message. |
age | enum | How old the message was, bucketed. |
outcome | enum | Result. |
reaction_slot | enum | For 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. |
via | enum | How 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 |
|---|---|---|
via | enum | How the thread was opened. One of key, mouse, feed, permalink, reply, unknown. |
replies | enum | Replies in the thread, bucketed. |
nested | bool | Whether the thread contains matterbox nested replies. |
depth | enum | Deepest 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 |
|---|---|---|
mode | enum | Which backend ran. One of server, local_fts, semantic, hybrid, ai. |
scope | enum | Search scope. One of channel, all, feed. |
from | enum | How it was started. One of key, palette, slash, cli, unknown. |
terms | enum | Number of words in the query, bucketed. The query text is never sent. |
had_operators | bool | Whether the query used search operators (from:, in:, quotes). |
results | enum | Hits returned, bucketed. |
latency_ms | enum | Time to results, bucketed. |
outcome | enum | Result. |
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 |
|---|---|---|
mode | enum | Which backend produced the hit. One of server, local_fts, semantic, hybrid, ai. |
rank | enum | 1-based position of the opened hit, bucketed. Says whether the top result is the right one. |
dwell | enum | Time 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 |
|---|---|---|
action | enum | What was done. One of opened, mark_read, mark_all_read, reply, open_channel, toggle_muted, refresh. |
items | enum | Unread items in the feed at the time, bucketed. |
via | enum | How 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 |
|---|---|---|
feature | enum | Which feature. |
outcome | enum | Result. |
latency_ms | enum | How long it took, bucketed, where the feature is slow enough to matter. |
size | enum | A feature-specific magnitude, bucketed: posts summarised, rows returned, suggestions offered, messages indexed. |
via | enum | How it was invoked. Separates “nobody wants this” from “nobody can find this”. One of key, mouse, palette, slash, cli, auto, unknown. |
error_class | enum | Failure 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 |
|---|---|---|
provider | enum | Which provider. Never the instance URL, project or issue key. One of jira, gitlab, github. |
action | enum | What was done. |
outcome | enum | Result. |
latency_ms | enum | API round trip, bucketed. |
error_class | enum | Failure 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 |
|---|---|---|
via | enum | How the file was attached. One of paste, drop, picker, cli. |
kind | enum | Coarse file kind, from the extension. The filename is never sent. One of image, video, audio, pdf, text, archive, other. |
size | enum | File size, bucketed. |
count | enum | Attachments now pending, bucketed. |
outcome | enum | Result. |
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 |
|---|---|---|
kind | enum | What was rendered. One of image_preview, inline_image, emoji_image, video, thumbnail. |
protocol | enum | Graphics protocol used. |
outcome | enum | Result. |
decode_ms | enum | Decode plus transmit time, bucketed. |
error_class | enum | Failure 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 |
|---|---|---|
key | enum | The 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. |
surface | enum | Which pane had focus. |
bound_elsewhere | bool | Whether 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 |
|---|---|---|
signal | enum | Which signal. |
surface | enum | Where it happened. |
action | enum | Which action was involved, for the action-specific signals. |
count | count | How many repetitions triggered it (escapes in the cascade, presses in the mash). |
dwell | enum | How long the person spent before giving up, bucketed. |
size | enum | For 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 |
|---|---|---|
surface | enum | Focused pane at the time. |
ms | enum | Frame time, bucketed. |
posts | enum | Posts loaded in the transcript, bucketed. |
cols | enum | Terminal width, bucketed. |
cause | enum | What 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 |
|---|---|---|
class | enum | Why it dropped. |
connected | enum | How long it had been connected, bucketed. |
clean | bool | Whether it was a clean close. |
cause | enum | How 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 |
|---|---|---|
attempts | enum | Reconnect attempts needed, bucketed. |
downtime | enum | Time disconnected, bucketed. |
resync | enum | Whether 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 |
|---|---|---|
where | enum | Which operation, from a fixed list of hand-written labels. |
class | enum | Failure class. |
status | count | HTTP status code, for API failures. 0 when there wasn’t one. |
retried | bool | Whether it was retried. |
user_visible | bool | Whether the user saw an error, as opposed to it being handled silently. |
detail | scrubbed text | Scrubbed 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 |
|---|---|---|
where | enum | Which subsystem recovered it. |
frames | stack frames | matterbox stack frames, innermost first. Only functions in the matterbox module are kept — no arguments, no file paths, no dependency or standard-library frames. |
detail | scrubbed text | Scrubbed 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 |
|---|---|---|
command | enum | Which subcommand. |
outcome | enum | Result. |
duration_ms | enum | Wall-clock duration, bucketed. |
tty | bool | Whether stdout was a terminal — i.e. a person ran it, rather than a script. |
error_class | enum | Failure 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 |
|---|---|---|
version | build string | Build running. |
rules | count | Number of configured rules. |
channels_on | enum list | Which 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 |
|---|---|---|
action | enum | Action type that ran. Never the rule’s name or its command line. |
outcome | enum | Result. |
trigger | enum | What 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 |
|---|---|---|
channel | enum | Where the notification was delivered. One of desktop, telegram. |
action | enum | What the user did with it. One of read, react, reply, open, dismissed, expired. |
outcome | enum | Result 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 runConsent 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.