Transparency
Telemetry events
Every event Argus sends to PostHog, with payload shapes. Anonymous distinct id only — opt out anytime in App settings.
Catalog of every event Argus sends to PostHog. Telemetry has three levels, picked during the one-time consent step on first launch and changeable any time under Settings → Privacy:
| Mode | What gets sent |
|---|---|
off |
Nothing. The privacy-maximum mode — even heartbeat $pageview is suppressed, so PostHog never sees this install at all. |
presence |
Exactly one event per app launch: a heartbeat $pageview with a fixed argus:/// path. No baseProps, no errors, no feedback, no other $pageviews. |
full |
The whole catalogue below — lifecycle, sessions, merges, agents, org tasks, errors, feedback. Same as before. |
The default for new installs is off until the user makes an explicit
choice in onboarding. Existing installs that opted out under the old
on/off toggle keep behaving as off; existing opt-ins map to full.
Events come from two channels: main (the Argus app process,
covering lifecycle, sessions, merges, agents, org tasks) and
renderer (the UI, covering renderer lifecycle and feedback). Both
funnel into the same PostHog project. In presence mode the renderer
SDK is initialised but emits nothing — the heartbeat is sent from main
only.
What's attached to every event
The "every event" rules below describe what full mode sends. Presence
mode is different — see the dedicated Presence section: it
attaches only app_version and the unavoidable GeoIP / SDK bookkeeping,
nothing else.
When you opt in to full mode, every event PostHog receives carries the catalogue below.
Some of these come from our explicit baseProps(); some are added
automatically by PostHog's SDKs; some are enriched server-side at PostHog
from the request IP. We list all three so the picture is complete.
Identifiers — anonymous, but stable across launches:
distinct_id/$device_id— random UUID generated locally and stored at~/.argus/telemetry-id(main) orlocalStorage(renderer). Wipe the file or clear browser storage to rotate it. We never callposthog.identify(...)— there is no person profile, no email, and no account linked to it ($is_identified: false,$process_person_profile: false).
Argus props (from our baseProps()):
- Main:
app_version,is_packaged,platform,arch,os_release,electron_version,node_version. - Renderer:
app_version,platform,process: "renderer".
PostHog SDK auto-properties (renderer only — posthog-js):
$browser,$browser_version,$browser_language,$raw_user_agent$os,$os_version(derived from the user agent)$device_type(e.g.Desktop)$screen_width/$screen_heightand$viewport_width/$viewport_height$timezone,$timezone_offset$current_url,$pathname,$host— the in-app URL of the renderer window (always underlocalhost:1420or the bundled file URL; never external sites)$referrer,$referring_domain— almost always$directbecause the renderer is local$session_id,$window_id,$session_entry_*— rotating per PostHog SDK session (30-min idle timeout); used by PostHog to group events into sessions on its end$insert_id,$sent_at,$time,$lib,$lib_version— SDK bookkeeping
These come from PostHog's posthog-js and are not configured by us. If
you want to know what each one means, the
PostHog autocapture docs
list them in full.
GeoIP enrichment (server-side at PostHog):
We have Discard client IP data turned on for our PostHog project. This means PostHog does not store the request IP alongside the event. However, the IP is still used at ingestion time — before it is discarded — to derive coarse geographic properties. Each event in PostHog therefore includes:
$geoip_country_code,$geoip_country_name,$geoip_continent_code,$geoip_continent_name$geoip_subdivision_1_code,$geoip_subdivision_1_name(state / province)$geoip_city_name$geoip_postal_code$geoip_latitude,$geoip_longitude,$geoip_accuracy_radius$geoip_time_zone
The accuracy is roughly city-level. The raw IP is not retained. PostHog only supports discarding the IP after enrichment — they don't offer an option to skip GeoIP enrichment entirely. If even city-level geography is unacceptable to you, leave telemetry off — that is the supported privacy mode.
What is not sent, even when telemetry is on:
- Session replay / DOM recordings — disabled in code
(
$has_recording: false,$recording_status: "disabled"confirm this on every event). - DOM autocapture — disabled in code, so we never send element selectors or click targets.
- Person profiles — we never call
posthog.identify(...), so PostHog doesn't link the distinct id to a person record ($process_person_profile: false). - Prompts, file contents, agent output, file paths, project names, repo names, branch names — anything from your work. The event catalogue below lists exactly which props each event carries; nothing else is sent.
- File-system paths inside error messages and stack traces — these are
scrubbed of
/Users/<name>,/home/<name>, andC:\Users\<name>before sending.
When telemetry is hard-disabled, regardless of the toggle:
- Development builds. Our own development sessions never appear in PostHog.
Presence
Sent in both presence and full modes. This is the single source of
DAU truth — PostHog defines a Daily Active User as a distinct_id with
at least one $pageview event in the day, so the heartbeat has to be
a $pageview (not a custom event) for presence-only users to count.
| Event | Source | When | Extra props |
|---|---|---|---|
$pageview |
main | Once per process lifetime, the first time the active mode flips from off to either non-off mode |
$current_url: "argus:///", $pathname: "/", app_version, source: "heartbeat" |
The heartbeat deliberately bypasses baseProps() — no platform,
arch, OS release, Electron or Node version is attached. The only
property beyond the $pageview magic keys is app_version, kept so we
can answer "are old builds still in the wild?" without learning
anything else about the device. The source: "heartbeat" property lets
analytics distinguish the main-process heartbeat from the
renderer-route $pageview events that full users also emit.
Lifecycle
Sent only in full mode.
| Event | Source | When | Extra props |
|---|---|---|---|
app_launched |
main | Telemetry transitions off → on (first opt-in, or re-enable after opt-out) | — |
app_quit |
main | App is quitting | — |
renderer_started |
renderer | Each renderer (main window or popout) finishes booting | popout: "main" | string |
$pageview |
renderer | The renderer URL changes (initial mount + every navigation) | $pathname, $current_url — both sanitised, see ↓ |
telemetry_enabled |
main | User opts in | — |
telemetry_disabled |
main | User opts out (sent before the off-flip + flush) | — |
$pageview sanitisation
PostHog magic event used by Web Analytics, DAU, paths and funnels. Argus sends the route template, never the raw URL — every dynamic segment that could carry a project / session / organisation / PR / device identifier is replaced with its placeholder name before the event leaves the renderer:
| Real path | Sent as |
|---|---|
/sessions/abc-123/editor |
/sessions/:sessionId/editor |
/organizations/houwert |
/organizations/:organizationId |
/pull-requests/foo/bar/42 |
/pull-requests/:owner/:repo/:number |
/agents, /devices, /runtimes, etc. |
unchanged (no dynamic segments) |
Both $pathname and $current_url carry the template. $current_url is
prefixed with argus:// so PostHog recognises it as a URL but the host
is a fixed literal, not the user's machine or any external origin.
Sessions
| Event | Source | When | Extra props |
|---|---|---|---|
session_created |
main | A new session is created | has_explicit_branch, has_description, use_existing_branch, has_explicit_base_branch |
session_deleted |
main | A session is deleted | kind (worktree | org_host | project_root), delete_branch, is_org_task |
Merges
session_merge_* covers the merge flow. phase distinguishes the
initial attempt from a continue after the user resolved conflicts.
| Event | Source | When | Extra props |
|---|---|---|---|
session_merged |
main | Merge completed cleanly | phase: "attempt" | "continue" |
session_merge_conflict |
main | Merge stopped on conflicts | phase, conflicting_files (count, not paths) |
session_merge_aborted |
main | An in-progress merge was aborted | kind: "rebase" | "merge" |
Agents
| Event | Source | When | Extra props |
|---|---|---|---|
agent_started |
main | A new agent is started | has_model, has_resume_id, has_parent, permission_mode, session_kind |
agent_restored |
main | Conversation restored at a prior turn | keep_turn_number, is_fresh_start |
agent_exited |
main | The agent process exited | exit_code, success, turns_completed, tool_uses, duration_ms |
agent_stopped |
main | The agent was stopped | — |
agent_interrupted |
main | The agent was interrupted | — |
Org tasks
| Event | Source | When | Extra props |
|---|---|---|---|
org_task_started |
main | An organisation task started | scope_size |
Onboarding
| Event | Source | When | Extra props |
|---|---|---|---|
onboarding_completed |
main | First-launch onboarding finished | — |
Feedback
| Event | Source | When | Extra props |
|---|---|---|---|
user_feedback |
renderer | Feedback message delivered (carries the message body) | message, $set_once.has_submitted_feedback: true |
Errors
Two channels:
$exception— fires on truly unhandled crashes (process gone, uncaught exception or rejection). Includes the error name, scrubbed error message, scrubbed stack trace, and asourceindicating which channel triggered it. Crash-process events additionally include the exit reason and exit code.app_error— caught-and-logged errors. Props:scope(e.g.agent,session:merge) and a scrubbed, truncatedmessage.
Error messages and stack traces are scrubbed of user home directory paths before sending.