Telemetry
July 30, 2026 · View on GitHub
AO uses anonymous telemetry to understand reliability and product usage. The Electron renderer sends sanitized PostHog events directly, and the Go daemon can persist allowlisted events locally and fan them out to PostHog when remote telemetry is enabled.
For cost-control runbooks, including the v2 PostHog event namespace and legacy ingestion drop rules, see posthog-cost-controls.md.
What is collected
- App activation events:
ao.app.active/ao.v2.app.activefrom the renderer and meaningful user-context CLI commands, each capped to one event per six-hour UTC slot, or four per day per install/channel - Renderer load and daily route-surface usage, grouped by coarse surface names
- Project/task/session UI actions, with project identifiers SHA-256 hashed
- Renderer exceptions, reduced to error name and coarse context
- Daemon operational events: CLI invocation, session spawn/failure, waiting-input transitions, HTTP 5xx, and daemon panics
- AO version context (
app_version/ao_version), platform, and build mode
PostHog session recording is disabled by default. If a time-boxed investigation enables it, network request names are masked before recording.
Privacy
Before any renderer event or recording is transmitted:
- Absolute file paths (
/home/...,/Users/...,C:\...) are replaced with[redacted-local-path] - Local URLs (
file://,app://renderer,localhost,127.0.0.1,[::1]) are replaced with[redacted-local-url] - Project IDs are one-way hashed and never sent in plain text
Daemon events use a remote payload allowlist before PostHog export. Project and session IDs are hashed, and raw location/IP fields are not accepted from AO payloads. Geographic reporting should use PostHog's GeoIP enrichment only.
Three burst-prone daemon events — ao.http.5xx, ao.daemon.panic,
ao.cli.usage_errors — are aggregated before export: every occurrence in a
rolling one-minute window is folded into a single rollup event carrying
count, window_start, and window_end, instead of exporting one PostHog
event per occurrence. A storm of 10,000 errors and one of 6 both cost the same
one event, and the true magnitude is still visible via count rather than
being silently capped away. Only the most recent occurrence's other
properties (path, fingerprint, etc.) are kept on the rollup — if a burst hits
several different endpoints or fingerprints in the same window, the ones
overwritten by later occurrences aren't visible on that rollup. Local SQLite
storage is unaffected: it receives every raw occurrence, unaggregated, for
full-fidelity debugging regardless of what PostHog sees.
Everything reaching PostHog remotely is still bounded per event name: a 5-per-minute burst cap plus a 200-per-day hard ceiling for ordinary events, or a 1,500-per-day ceiling for the three aggregated names above (since their per-occurrence cost is already collapsed by aggregation, the daily cap there is a structural backstop rather than the primary limit). The renderer applies the same 5-per-minute / 200-per-day shape to its own event and exception capture path, without the aggregation step.
All events are sent as PostHog anonymous events ($process_person_profile: false; the renderer never calls identify()). The renderer keeps PostHog SDK
persistence in memory, disables person profiles, and explicitly bootstraps the
AO install ID as anonymous. This prevents legacy PostHog state from restoring
an identified user or replacing the stable AO device ID after an upgrade. The
install ID still deduplicates unique-user counts, but no person profiles are
created — person properties and person-property cohorts are intentionally
unavailable. AO's heartbeat and route reservations continue to use their own
sanitized localStorage keys independently of PostHog SDK persistence.
ao.cli.invoked is capped at once per actor type and command path per UTC day
per install. Routine successful internal/read-only commands (ao status,
ao session ls, ao session get, ao project ls, ao project get,
ao orchestrator ls, ao hooks, and ao pty-host) are excluded outright.
Commands that never reflect product activity — the supervisor-driven
ao daemon/ao start, the self-documenting ao completion/ao help, and
the internal ao agent-process runtime process — are also excluded outright.
CLI invocations are classified by actor:
actor_type=user: a user-context CLI command. These can refresh CLI-channelao.app.active.actor_type=agent: commands run inside an AO-managed agent session (AO_SESSION_IDis set). These are useful command-adoption signal but do not refreshao.app.active, because agents can keep running after the human has stopped actively using AO. Routine internal paths such asao hooksare dropped on success.actor_type=system: supervisor/runtime background processes. These are not sent as CLI usage.
The per-command daily cap keeps invocation frequency off PostHog, and the CLI reservation state is persisted under the AO data dir so a daemon restart does not re-emit every polling command for the same day.
Routine successful internal/read-only commands are not reliability signal by
themselves and should not be reintroduced as success telemetry. For commands
such as ao status, ao session ls, ao session get, ao project ls,
ao project get, ao orchestrator ls, ao hooks, and ao pty-host, track
only meaningful user-impacting failures through a separate, rate-limited event
such as ao.v2.cli.failed. That event should carry safe enum-like fields such
as command_path, actor_type, error_category, and stable error_code; it
must not include raw error messages, stack traces, local paths, project names,
repository URLs, prompts, terminal output, tokens, or request payloads.
ao.renderer.route_viewed is capped at once per coarse surface per UTC day per
renderer install. This preserves surface adoption and retention signal while
dropping repeated navigation churn inside the same surface.
Product Metrics Model
AO currently has a stable install ID, not a signed-in account user ID. That means today's DAU/MAU can accurately represent active installs, but not unique people across multiple machines. True user-level new/churn/journey metrics require an explicit stable user identity from a login, license, or workspace account system. That identity should be sent as a first-party AO user ID (or a one-way hash of it) only when the user has authenticated or explicitly enabled account-level telemetry; it should not be inferred from machine fingerprints, paths, git remotes, emails in repo config, or other local data.
The minimum signals for accurate usage analytics are:
ao.app.active/ao.v2.app.active: up to one event per six-hour UTC slot per install/account when a human uses the desktop app or runs a meaningful user-context CLI command. This powers DAU, WAU, MAU, retention, and churn while keeping arbitrary rolling windows from undercounting long-running usage. Renderer active events are sent immediately; a slot is released for retry when the SDK rejects or throws while capturing the event.ao.projects.createdandao.onboarding.first_project_added: activation funnel from install to first project.ao.session.spawned,ao.session.spawn_failed, andao.onboarding.first_session_spawned: activation funnel from project to first running agent, plus spawn reliability.ao.cli.invoked/ao.v2.cli.invokedwithactor_type=user|agent: command adoption by actor for meaningful non-internal commands, capped by command/install/day. Agent-context command usage is product signal, but should be analyzed separately from active-user counts.ao.session.waiting_input_entered/exited: whether agents are making progress or waiting on the human, with dwell time.- Renderer and daemon error/crash events: reliability and support signal.
Signals that should not drive active-user metrics:
- Internal runtime hosts such as
ao pty-host. - Supervisor startup/control commands such as
ao daemonandao start. - Agent hook callbacks and other CLI commands run with
AO_SESSION_ID, except as separate agent-activity or command-adoption metrics. - Raw polling frequency for read-only state commands.
Volume Investigation: 2026-07-21
Read-only HogQL queries against PostHog project 475752 over the trailing
30-day window found 3,203,364 total events. The dominant event names were:
| Event | Count | Installs | Events/install |
|---|---|---|---|
ao.cli.invoked | 1,508,888 | 870 | 1,734.35 |
ao.app.active | 1,411,807 | 1,434 | 984.52 |
ao.renderer.route_viewed | 114,940 | 1,388 | 82.81 |
ao.renderer.api_error | 18,634 | 662 | 28.15 |
ao.session.waiting_input_entered | 17,583 | 377 | 46.64 |
$exception | 16,563 | 681 | 24.32 |
ao.cli.usage_errors | 15,349 | 215 | 71.39 |
ao.session.waiting_input_exited | 15,343 | 339 | 45.26 |
$set | 13,211 | 1,137 | 11.62 |
ao.session.spawned | 11,439 | 887 | 12.90 |
The top two events were almost entirely CLI-sourced and moved together:
ao.cli.invoked had 1,508,888 events and CLI-channel ao.app.active had
1,403,170 events. The largest command paths were polling/hook paths:
| Command path | ao.cli.invoked count | Install-days | Projected events saved by persistent daily cap |
|---|---|---|---|
ao hooks | 589,338 | 1,624 | 587,714 |
ao session ls | 270,977 | 764 | 270,213 |
ao orchestrator ls | 236,877 | 177 | 236,700 |
ao status | 220,436 | 524 | 219,912 |
ao session get | 75,946 | 603 | 75,343 |
ao project ls | 40,435 | 462 | 39,973 |
ao project get | 31,048 | 356 | 30,692 |
ao send | 19,104 | 536 | 18,568 |
Using ao.session.spawned as the AO-session denominator, the 30-day window had
11,439 spawned sessions, 131.91 ao.cli.invoked events per spawned session,
and 10.05 ao.renderer.route_viewed events per spawned session. Looking only
at renderer/PostHog browser sessions, there were 211,532 renderer SDK events
across 6,988 PostHog sessions, or 30.27 events per PostHog session. Route
views were the largest renderer contributor at 17.67 events per PostHog
session.
Projected 30-day reduction from the implemented changes, using the observed install-day cardinalities:
- Persisting the CLI command daily cap:
ao.cli.invokeddrops from 1,508,888 to about 8,416 events, saving about 1,500,472 events. - Persisting the CLI active six-hour slot cap: CLI-channel
ao.app.activedrops from 1,403,170 to at most about 7,508 events, saving at least about 1,395,662 events. - Daily renderer route-surface capping:
ao.renderer.route_vieweddrops from 114,940 to about 8,483 events, saving about 106,457 events.
Total projected event-volume savings from those three changes are still roughly 3.0M events per trailing 30 days before adoption effects.
Anonymous-vs-identified check: all events had a person_id in HogQL, but the
event-level profile-processing property showed renderer exceptions as the
remaining identified-risk path: 16,534 of 16,563 $exception events carried
$process_person_profile=true, while only 29 carried false. Renderer
captures now force $process_person_profile=false on the event properties, and
Web Vitals capture is disabled because the 7,017 $web_vitals events in the
window were diagnostic noise rather than activation, feature usage, or
crash/error signal.
Install ID
On first run, a random install identifier is generated and stored at
~/.ao/data/telemetry_install_id (or $AO_DATA_DIR/telemetry_install_id). The
renderer and daemon both use this ID as the PostHog distinct ID so activity is
deduplicated across app launches and CLI invocations. It is not linked to any
personal account. In the renderer it is also the PostHog device ID, and the SDK
is explicitly kept in anonymous mode.
Configuration
Renderer PostHog key and host are baked in at build time. To point a build at another PostHog project, set these environment variables before building:
VITE_AO_POSTHOG_KEY=phc_yourkey
VITE_AO_POSTHOG_HOST=https://your-posthog-host.com
Daemon event capture is off by default when the daemon is launched directly. The Electron supervisor starts the daemon with these defaults unless the environment already provides explicit values:
AO_TELEMETRY_EVENTS=on
AO_TELEMETRY_REMOTE=posthog
AO_TELEMETRY_POSTHOG_KEY=phc_yourkey
AO_TELEMETRY_POSTHOG_HOST=https://us.i.posthog.com
Local daemon telemetry is retained in SQLite for 30 days.
PostHog Retention And Geography Dashboard
Use ao.v2.app.active as the current active-user event for DAU, weekly
retention, and country-level active-user maps. During migration, union it with
legacy ao.app.active where channel=renderer or where channel=cli and
actor_type=user. AO emits active-user telemetry from:
channel=rendererwhen the desktop app initializes and at most once per UTC six-hour slot while the app stays openchannel=cliwhen the CLI reports a meaningful user-typed command invocation to the local daemon, at most once per UTC six-hour slot per install
Recommended PostHog setup:
- Enable PostHog GeoIP enrichment for the project.
- Create an "AO Active Users" dashboard.
- Add a Trends insight:
- Event:
ao.v2.app.active - Aggregation: unique users
- Chart type: world map
- Breakdown: GeoIP country code, for example
$geoip_country_code
- Event:
- Add a Retention insight:
- Start event:
ao.v2.app.active - Return event:
ao.v2.app.active - Interval: weekly
- Range: last 12 weeks
- Start event:
- Add optional filters or breakdowns for
channel=rendererandchannel=cliwhen comparing desktop app and CLI activity.
PostHog references:
- GeoIP enrichment: https://posthog.com/docs/cdp/geoip-enrichment
- Trends insights: https://posthog.com/docs/product-analytics/trends
- Retention insights: https://posthog.com/docs/product-analytics/retention