---
title: UI Usage Tracking — see which controls and integrations you actually use
---

# UI Usage Tracking — see which controls and integrations you actually use

## What it is

> **Library page** — describes what users see and how to use it, then how it works under the hood. Self-contained so an outside AI (with no repo access) can read this and answer "what is UI Usage tracking and how does it work?"

Omniscio quietly records, **on your own device**, which named controls you click (buttons, links, toggles, tabs, menu items) and which sidebar integrations you open (Gmail, GitHub, SMS, Stats, KMS, and the rest). It then shows you a plain "used vs. never used" picture so you can tell what you actually rely on versus what you've never touched — the point being to decide later what to remove or auto-hide so the app stays uncluttered.

The **"used vs never used" report is built on your own device** — it needs no server. One honest caveat: when **Error Reporting** (Settings → System) is on, Omniscio also folds anonymous per-control **click counts** into its daily diagnostics digest — the same anonymized fleet health summary that crashes and errors ride. Those are counts only (a catalogued control's internal name + how many times it was clicked, filtered to Omniscio's known control list and keyed to a random install id) — never message content, file names, or anything that identifies you. Turn Error Reporting off and nothing about your usage leaves the machine. (See "The on/off toggle, and how it relates to telemetry" and "Stage 2" below.)

## Where to find it

1. Open Omniscio.
2. Open **Settings** (gear icon in the toolbar, or the **Settings** entry in the Omniscio sidebar group).
3. Go to the **Diagnostics** section.
4. Two things live there for this feature:
   - A toggle, **"Track which UI controls you use"** (on by default). The report it builds is local; when Error Reporting is on, anonymous click counts also ride the daily diagnostics digest (see "The on/off toggle" below).
   - A **"UI Usage"** card below it that shows the report.

## How it behaves

### What the card shows

The **UI Usage** card has a small row of time-window buttons — **Last 24h / 7 days / 30 days / All time** (it opens on 30 days) — and a "Tracking since &lt;date&gt;" note so you know how far back the data goes. Under that, two groups:

- **Controls** — the named buttons / links / toggles / tabs / menu items in the app.
- **Integrations** — the sidebar integrations (the built-in virtual projects like Gmail, GitHub, SMS, Stats, Inbox, Settings, etc.).

Each group is split into two lists:

- **Used (N)** — the controls/integrations you've used in the selected window, **most-used first**, with a click count next to each.
- **Never used (N)** — the ones you've never used (in the catalogue of trackable things), listed alphabetically.

Switching the time window re-runs the report, so "Never used" under **Last 24h** means "you didn't touch it today", while "Never used" under **All time** means "you've never touched it since tracking began".

Under each **Used** list, a small **mobile / desktop split** shows how much of that usage came from a phone versus a computer (a phone icon and a monitor icon, each with a count) — so you can tell, for instance, that a control you rely on is mostly used on mobile. The split appears only once there's device-stamped usage to show; older data recorded before this was added simply isn't attributed to either device.

#### The "Never used" list looks huge on day one — that's expected, not a bug

When you first turn the app on, **everything is unused until you click it**, so the "Never used" lists start out long and the "Used" lists start out short. This is normal. As you use Omniscio over days and weeks, items move from "Never used" into "Used" and the lists become a meaningful map of your real habits. A long "Never used" list early on is not a sign that tracking is broken — it just hasn't seen you use those controls yet.

#### Off is left out of "Never used" — for both integrations and controls

If you've **turned an integration off** (so it isn't even showing in your sidebar), it is **not** listed under the integrations "Never used" list. The reasoning: _off is not the same as unused._ The "could be opened" universe is the set of integrations currently **visible** to you, so a feature you deliberately disabled isn't held against you as "never used".

**Controls follow the same rule.** A control that simply cannot appear in your copy of Omniscio is left out of the controls "Never used" list too — because you never _could_ have used it. Three kinds are filtered out: **dev-only** controls (they exist only in a developer build, never in the shipped app), **platform-specific** controls (e.g. the Windows-only AutoHotkey manager, hidden on macOS/Linux), and controls behind an **in-development feature you haven't turned on**. As soon as such a control _can_ render for you — you're on the right platform, or you enable the feature — it re-enters the "Never used" list until you use it. So the list stays a map of things you could use but haven't, not a pile of things that were never reachable.

The device you used something on is also recorded, so the card can show a **mobile vs. desktop split** (see below).

### Flow — what you tend to do right after a control

Below the UI Usage card sits a small companion, the **Flow** card. Pick a starting control (e.g. "Go to inbox") and it shows the controls you most often use **right after** it, within the same app session — a first look at your real navigation habits ("after I open search, I usually attach a file next"). Like everything above, it is **100% local** and windowed by the same 24h / 7d / 30d / All time buttons. It's a rough picture, not an exact replay.

To make this possible, each recorded event now also carries two pieces of **local-only** sequence context: a **run id** (a fresh random id minted once per app launch — deliberately NOT the stable install id, so it can never link you across launches) and a **step number** (a simple counter within that launch). Together they let Omniscio reconstruct the order of your clicks _within a single run_, on your own machine. These two values **never leave the device** — the anonymous fleet rollup only ever sees per-control counts, never sequences (a build guard enforces that), and any future upload of flow data is a separate, reserved privacy decision.

### The on/off toggle, and why it's separate from telemetry

The **"Track which UI controls you use"** toggle in Settings → Diagnostics controls this feature. It is **on by default**, and only an explicit "off" turns it off; flip it off and Omniscio stops recording new clicks and integration opens (the card then tells you tracking is off and to turn it on to start collecting).

This toggle is **deliberately separate from the telemetry / "share data" consent (Error Reporting).** It is a **local feature switch** — "do I want Omniscio to keep this diary of what I use?" — and it gates whether clicks are recorded at all. Whether the recorded **counts** then leave your machine is a _separate_ decision, gated by Error Reporting: with it **on**, anonymous per-control click counts ride the daily diagnostics digest; with it **off**, nothing about your usage is sent. So this switch controls the local diary; Error Reporting controls egress.

### Privacy — what is and isn't stored

Only two things are ever written for each event:

1. **Which** control or integration it was — the control's internal anchor name (e.g. the catalogue key for the "New Session" button) or the integration's id — and
2. **When** — a timestamp.

That's it. **No message content, no file names, no folder paths, no contact names, no personal data** is ever recorded. Per-instance things are never stored either: clicking a specific session row, for example, is ignored — only catalogued, named controls count, so a one-off id like a session id never enters the database (see "How it works"). Everything lives in your local Omniscio database; the only thing that can leave it is the anonymous per-control **count** (the control's internal name + a number, never the timestamp and never anything identifying), and only when Error Reporting is on — see "The on/off toggle" above.

There is also an **anonymous install id** — a random UUID generated and saved on your machine the first time it's needed. In Stage 1 **nothing reads it and nothing sends it**; it exists only so a future Stage 2 could count distinct installs without identifying anyone. It is deliberately a **separate** random value from the id Omniscio already uses for in-app support chat, so future analytics can never be tied back to your identity by reusing an existing id. If the file holding it is ever lost, a fresh one is minted (counted as a new install), which is fine for anonymous aggregate counting.

### Stage 2 — central rollup (partly superseded by fleet telemetry)

Since this feature was written, the anonymous click **counts** already leave the machine a different way: they ride Omniscio's **fleet telemetry** daily diagnostics digest when Error Reporting is on (filtered to Omniscio's known control list, keyed to a random _fleet_ install id), and the maintainer sees them as aggregate product analytics. So a cross-user rollup of clicks exists today — through fleet telemetry, not through this feature's own pipe.

What is **not** built is the dedicated path once envisioned here: a ui-usage-specific opt-in upload (catalogue id + kind + count + timestamp + this feature's own install id) surfaced as an "all users" view _inside this Diagnostics card_. This feature's own install id (see "Privacy" above) is still dormant, and that "all users" tab does not exist. Either way, `usageTrackingEnabled` gates only local recording; the egress that DOES happen rides the Error Reporting (telemetry) consent and must never weaken the local guarantees above.

## For agents

### How it works (for agents with repo access)

The feature splits cleanly: the **renderer** captures and interprets; the **main process** is "dumb" storage that only inserts batches and counts them by time window. All the meaning — what's trackable, what's visible, what counts as "never used" — lives in the renderer.

**Capturing control clicks.** A single delegated click listener is installed once at the App root, in the **capture phase** (so it still fires even if a child stops propagation). On each click it walks up from the click target to the nearest element carrying a `data-ui-anchor` attribute and resolves that anchor's name. It records the click **only if** the anchor is in a static allow-list of trackable controls. That allow-list, `TRACKED_CONTROL_ANCHORS`, is the app's `data-ui-anchor` registry (`STATIC_UI_ANCHORS`) filtered down to **interactive** kinds (button / link / toggle / tab / menuitem) and **non-internal** entries. Templated / blind-spot anchors that carry a runtime value (e.g. `session-row:<id>`) are not keys in that set, so they're naturally ignored — no normalization is needed and no per-instance id ever reaches the database. Crucially, **this same allow-list is also the "never used" universe** for controls, so "what we track" and "what we show as never-used" can never drift apart.

**Capturing integration opens and closes.** Integration "opens" are _not_ tracked from a sidebar click — they're tracked at the `setActiveProject` chokepoint (the single place the active project changes). On a real transition, the newly-active project's `folderPath` is mapped through the `INTEGRATION_REGISTRY` (folder-path sentinel → integration id) and, if it resolves to a built-in integration, the open is recorded. Tracking at this chokepoint means opens via hotkey or the command palette count too, not just sidebar clicks. One case is intentionally suppressed: the **startup-restore re-open** of the last project (`source === 'restore'`) is skipped, so an integration you happened to leave open isn't phantom-counted on every launch. The **close** is recorded symmetrically: the same chokepoint, on leaving an integration, records an `integration_close` event for the one being left, carrying the **dwell** (how long it was open, computed in the renderer and clamped to a sane `[0, 24h]`). Closes are kept paired with opens — a restore-suppressed open produces no close, and turning tracking off resets the open state — so you never get orphan closes.

**Stamping the device.** Every event (control click, integration open, integration close) is stamped with the current **platform** — `mobile` on a narrow, touch device, else `desktop` — at the moment it's buffered. This is the only new dimension on capture, and it stays local.

**Buffering and flushing.** Events accumulate in an in-renderer buffer and flush **fire-and-forget** to the main process over the `ui-usage:record` IPC channel — on a periodic timer (every ~10 s), and when the page is hidden / unloaded. The flush is loss-resilient: the buffer is swapped out _before_ the await (so clicks during the network round-trip aren't lost), and a failed flush re-queues the batch (bounded) so a transient error doesn't silently drop usage.

**Storage and counting (main).** Records land in a dedicated local SQLite table, `ui_usage_events(id, kind, target, platform, duration_ms, created_at, run_id, seq)`, where `kind` is `control` / `integration` / `integration_close`, `target` is the anchor key or integration id, `platform` is `mobile` / `desktop` / null, `duration_ms` is the dwell on a close (null otherwise), and `run_id`/`seq` (both nullable) are the local sequence context described under "Flow" above — a per-app-run id + a monotonic ordinal stamped renderer-side at buffer time (a batch's timestamp can't order events within it, so `seq` is the intra-run ordering key). Every added column is nullable, so rows recorded before it existed carry null. Timestamps are stamped JS-side (never `datetime('now')`). The main process exposes `ui-usage:summary` (per-`(kind, target, platform)` counts within a rolling 24h / 7d / 30d / all window plus the "tracking since" date — the per-platform grouping is what lets the card show the device split) and `ui-usage:followers` (the local Flow query: what was used within the next few events after a chosen control, in the same run, via an indexed self-join on `(run_id, seq)`). The counts query never selects `run_id`/`seq`, and the digest read stays grouped by `(kind, target)` only (no platform), so neither the sequence context nor the device split can reach the anonymous fleet digest; its egress allow-list also drops any kind other than `control`/`integration`, so a local-only `integration_close` can never ride it. That's the full extent of the backend's knowledge — it never knows about registries, visibility, or "never used".

**The vocabulary crosswalk.** Omniscio tracks actions in three separate id vocabularies — control anchors (this feature), keyboard-shortcut action ids, and telemetry feature ids — that historically never referenced each other. A small checked-in map, `FLOW_CROSSWALK`, links a logical action across the vocabularies where a genuine match exists (e.g. "Go to inbox" = the `app-inbox-button` anchor + the `goToInbox` shortcut + the `inbox` feature). A build guard fails on any dangling id, so the map stays honest against all three registries. The Flow card uses it to label results with friendly names.

**Computing "never used" (renderer).** The Diagnostics card asks for the windowed counts, then the report builder diffs them against the universes the renderer can see: the control catalogue and the **currently-visible** integrations (the same two sidebar visibility filters the projects sidebar uses, mapped to registry id + display name). The control catalogue starts from `TRACKED_CONTROL_ANCHORS` (labelled from each anchor's registry description) but is **filtered to controls that can actually render in this install** — a control's optional `visibility` metadata (`{ devOnly, platforms, unreleasedFeature }`) is evaluated with the very predicates the app renders by (`isDevBuildRuntime()`, the current OS, `isUnreleasedFeatureVisibleInRenderer()`), and a control that can't appear now is dropped. Anything left in a universe with a count of 0 is "never used"; the rest is "used", sorted by count, and each used entry also carries its mobile/desktop breakdown. Because both universes exclude what can't be reached, a turned-off feature or an unreachable control is omitted rather than flagged "never used".

**Tagging a control's visibility.** The `visibility` metadata lives in the app's `data-ui-anchor` registry. Because a whole feature's controls usually share one gate, each co-located `*.ui-anchors.ts` file wraps its anchors in a `gatedBy({ … }, { … })` helper that stamps the gate onto all of them at once — so a new control added to that file inherits the gate automatically and can't silently drift back into the "never used" list. Mixed files set a per-anchor `visibility` that merges over the file gate.

**The setting.** `usageTrackingEnabled` is an `AppSettings` field, default `true`. Capture short-circuits when it's explicitly `false`. It is independent of the telemetry/send-consent setting by design.

### Files (for agents with repo access)

- Table / migrations — [src/main/db/migrations/20260605180334-ui-usage-events-table.ts](../../src/main/db/migrations/20260605180334-ui-usage-events-table.ts) (creates `ui_usage_events`) + [.../20260717030737-add-run-id-and-seq-...](../../src/main/db/migrations/20260717030737-add-run-id-and-seq-to-ui-usage-events-for-local-sequence.ts) (`run_id`/`seq`) + [.../20260717032035-add-platform-and-duration-ms-...](../../src/main/db/migrations/20260717032035-add-platform-and-duration-ms-to-ui-usage-events.ts) (`platform`/`duration_ms`).
- Queries (insert + windowed counts w/ optional platform split + "tracking since" + local Flow `getUiUsageFollowers`) — [src/main/db/queries-ui-usage.ts](../../src/main/db/queries-ui-usage.ts).
- IPC handlers (`ui-usage:record`, `ui-usage:summary`, `ui-usage:followers`) — [src/main/ipc/handlers-ui-usage.ts](../../src/main/ipc/handlers-ui-usage.ts).
- Fleet-digest egress allow-list (fails closed on non-`control`/`integration` kinds) — [src/main/services/telemetry/telemetry-digest-data.ts](../../src/main/services/telemetry/telemetry-digest-data.ts).
- Vocabulary crosswalk (control-anchor ↔ shortcut ↔ feature) — [src/shared/ui-flow-crosswalk.ts](../../src/shared/ui-flow-crosswalk.ts); contract [.claude/memory/contracts/ui-flow-crosswalk-contract.md](../../.claude/memory/contracts/ui-flow-crosswalk-contract.md).
- Flow card UI (Settings → Diagnostics → Health) — [src/renderer/src/features/settings/sections/diagnostics/UiFlowCard.tsx](../../src/renderer/src/features/settings/sections/diagnostics/UiFlowCard.tsx).
- Anonymous install id (Stage-2 forward-compat, inert in Stage 1) — [src/main/services/install/install-id.ts](../../src/main/services/install/install-id.ts).
- Trackable-control universe / capture allow-list — [src/renderer/src/lib/ui-usage-anchors.ts](../../src/renderer/src/lib/ui-usage-anchors.ts).
- Control render-gate metadata (`visibility`) + the `gatedBy()` helper — [src/shared/ui-anchor-types.ts](../../src/shared/ui-anchor-types.ts) + [src/shared/ui-anchor-visibility.ts](../../src/shared/ui-anchor-visibility.ts).
- Renderer capture (buffer + platform stamp + control-click + project open/close + flush) — [src/renderer/src/lib/ui-usage-tracker.ts](../../src/renderer/src/lib/ui-usage-tracker.ts).
- Root click listener / flush hook — [src/renderer/src/hooks/useUiUsageTracker.ts](../../src/renderer/src/hooks/useUiUsageTracker.ts).
- "Used vs never used" report builder — [src/renderer/src/lib/ui-usage-report.ts](../../src/renderer/src/lib/ui-usage-report.ts).
- Diagnostics card UI — [src/renderer/src/features/settings/sections/diagnostics/UiUsageCard.tsx](../../src/renderer/src/features/settings/sections/diagnostics/UiUsageCard.tsx).
- Full invariants + the tests that lock them — [.claude/memory/contracts/ui-usage-tracking-contract.md](../../.claude/memory/contracts/ui-usage-tracking-contract.md).

## Related

Two neighbouring surfaces are worth reading next:

- [Stats](stats.md) — the broader "how you use Omniscio" virtual project (cost, tokens, feature-event counts, trends). UI Usage tracking is narrower and local-only: it's about _which named controls/integrations get clicked_, surfaced in Settings → Diagnostics, not in Stats.
- [Feedback channel opt-out](feedback-channel-opt-out.md) — the neighboring Settings → Diagnostics toggles for silencing outbound feedback/telemetry email (a different, send-related consent).
