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.active from 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-channel ao.app.active.
  • actor_type=agent: commands run inside an AO-managed agent session (AO_SESSION_ID is set). These are useful command-adoption signal but do not refresh ao.app.active, because agents can keep running after the human has stopped actively using AO. Routine internal paths such as ao hooks are 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.created and ao.onboarding.first_project_added: activation funnel from install to first project.
  • ao.session.spawned, ao.session.spawn_failed, and ao.onboarding.first_session_spawned: activation funnel from project to first running agent, plus spawn reliability.
  • ao.cli.invoked / ao.v2.cli.invoked with actor_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 daemon and ao 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:

EventCountInstallsEvents/install
ao.cli.invoked1,508,8888701,734.35
ao.app.active1,411,8071,434984.52
ao.renderer.route_viewed114,9401,38882.81
ao.renderer.api_error18,63466228.15
ao.session.waiting_input_entered17,58337746.64
$exception16,56368124.32
ao.cli.usage_errors15,34921571.39
ao.session.waiting_input_exited15,34333945.26
$set13,2111,13711.62
ao.session.spawned11,43988712.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 pathao.cli.invoked countInstall-daysProjected events saved by persistent daily cap
ao hooks589,3381,624587,714
ao session ls270,977764270,213
ao orchestrator ls236,877177236,700
ao status220,436524219,912
ao session get75,94660375,343
ao project ls40,43546239,973
ao project get31,04835630,692
ao send19,10453618,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.invoked drops 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.active drops 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_viewed drops 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=renderer when the desktop app initializes and at most once per UTC six-hour slot while the app stays open
  • channel=cli when 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:

  1. Enable PostHog GeoIP enrichment for the project.
  2. Create an "AO Active Users" dashboard.
  3. 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
  4. Add a Retention insight:
    • Start event: ao.v2.app.active
    • Return event: ao.v2.app.active
    • Interval: weekly
    • Range: last 12 weeks
  5. Add optional filters or breakdowns for channel=renderer and channel=cli when comparing desktop app and CLI activity.

PostHog references: