Hive has no usage analytics. We need anonymous metrics to understand onboarding funnels, activation milestones, and retention patterns to guide product decisions.
| Decision | Choice | Rationale |
|---|---|---|
| Goal | Growth/activation metrics | Understand onboarding funnel, first-use milestones, return patterns |
| SDK | posthog-node (main process only) |
Simpler than dual-process, works offline, single instance, no renderer network access needed |
| Consent | Opt-out (on by default) | Standard for developer tools. Toggle in Settings > Privacy. No toasts or first-launch notices |
| Event count | ~13 core events | Focused on activation funnel, not comprehensive instrumentation |
A singleton TelemetryService in src/main/services/ — follows the exact same pattern as LoggerService and NotificationService. IPC handlers call telemetryService.track() directly where events happen. The renderer forwards UI-only events via window.analyticsOps.track() through IPC.
Why this approach over alternatives:
- vs. IPC middleware/interceptor: IPC channels are implementation details, not user-meaningful events. Can't express state transitions like "first prompt ever" or "onboarding completed" via middleware. Also no existing middleware pattern in the codebase.
- vs. Event bus integration: Hive has no central event bus. Building one just for analytics is over-engineering.
- Centralized service wins on: simplicity, greppability, alignment with existing patterns, flexibility to add/remove events.
┌─────────────────────┐ ┌──────────────────────┐
│ Main Process │ │ Renderer (React) │
│ │ │ │
│ IPC Handlers ───────┼──→ │ window.analyticsOps │
│ │ │ │ .track() ────────────┼──┐
│ ▼ │ │ .setEnabled() ───────┼──┤
│ TelemetryService │◄────┼──────────────────────┼──┘
│ │ │ IPC │ │
│ ▼ │ │ │
│ posthog-node │ │ │
│ (batched HTTP) │ │ │
└─────────────────────┘ └──────────────────────┘
Most events are tracked directly in main process IPC handlers (no IPC round-trip). Only renderer-specific events (e.g., onboarding_completed) use the window.analyticsOps.track() bridge.
Singleton class with these methods:
| Method | Description |
|---|---|
init() |
Called after DB init in app.whenReady(). Loads telemetry_distinct_id and telemetry_enabled from SQLite settings table. Generates crypto.randomUUID() on first launch. Creates PostHog client if enabled. |
track(event, properties?) |
No-op if disabled. Calls posthog.capture() with { distinctId, event, properties: { app_version, platform, ...properties } }. |
identify(properties?) |
Sets user properties on the anonymous profile (e.g., platform, electron_version). |
setEnabled(boolean) |
Toggles telemetry_enabled in SQLite. Creates or destroys PostHog client accordingly. |
isEnabled() |
Returns current enabled state. |
shutdown() |
Flushes all pending events. Called on will-quit before closeDatabase(). |
new PostHog(POSTHOG_API_KEY, {
host: POSTHOG_HOST, // https://us.i.posthog.com or https://eu.i.posthog.com
flushAt: 20, // batch size before flush
flushInterval: 30000 // 30 seconds
})- Generated via
crypto.randomUUID()on first app launch - Stored in SQLite
settingstable with keytelemetry_distinct_id - Persists across app sessions but is per-machine (tied to
~/.hive/hive.db) - Never linked to any PII
- If user deletes DB, a new ID is generated (privacy-preserving)
posthog-nodequeues events in memory and flushes in batches (20 events or 30s)- Failed flushes are retried on next interval
shutdown()inwill-quitmakes a final flush attempt before process exits- No disk persistence needed — Electron's
will-quitevent is reliable enough
- Analytics is on by default (opt-out model)
telemetry_enabledsetting absent from SQLite = enabled (default behavior)- User can disable via Settings > Privacy toggle
- When disabled: no events are captured, PostHog client is destroyed
- When re-enabled: new PostHog client is created, events resume
- Setting persists across app restarts via SQLite
- No toasts, banners, or first-launch notices — just the settings toggle
- No project names, paths, or file contents
- No prompt text or AI responses
- No git branch names, commit messages, or diffs
- No worktree paths or directory structures
- No PII of any kind
- The distinct_id is a random UUID with no link to any identity
| Event | Trigger Location | Properties | Purpose |
|---|---|---|---|
app_launched |
src/main/index.ts (after window creation) |
app_version, platform |
Top of funnel — how many users launch the app |
onboarding_completed |
AgentSetupGuard.tsx via window.analyticsOps.track() |
{ sdk: string } |
Setup completion rate |
| Event | Trigger Location | Properties | Purpose |
|---|---|---|---|
project_added |
src/main/ipc/database-handlers.ts (db:project:create) |
{ language?: string } |
First meaningful action after setup |
worktree_created |
src/main/ipc/worktree-handlers.ts (worktree:create) |
— | Engagement depth |
session_started |
src/main/ipc/opencode-handlers.ts (connect handler) |
{ agent_sdk: string } |
Core activation — user starts coding with AI |
prompt_sent |
src/main/ipc/opencode-handlers.ts (prompt handler) |
{ agent_sdk: string } |
Core usage — user interacts with AI |
| Event | Trigger Location | Properties | Purpose |
|---|---|---|---|
connection_created |
src/main/ipc/connection-handlers.ts |
— | Multi-repo feature usage |
git_commit_made |
src/main/ipc/git-file-handlers.ts (git:commit) |
— | Git integration adoption |
git_push_made |
src/main/ipc/git-file-handlers.ts (git:push) |
— | Git workflow depth |
script_run |
src/main/ipc/script-handlers.ts |
{ type: 'setup' | 'run' | 'archive' } |
Setup automation adoption |
worktree_opened_in_editor |
src/main/ipc/settings-handlers.ts |
— | External editor integration |
| Event | Trigger Location | Properties | Purpose |
|---|---|---|---|
app_session_ended |
src/main/index.ts (will-quit handler) |
{ session_duration_ms: number } |
Session length distribution |
| Event | Trigger Location | Properties | Purpose |
|---|---|---|---|
telemetry_disabled |
telemetry-service.ts (setEnabled(false)) |
— | Track opt-out rate |
Follow the same component pattern as SettingsSecurity.tsx:
- Section header: "Privacy" / "Control how Hive collects anonymous usage data"
- Toggle switch: "Send anonymous usage analytics" — bound to
window.analyticsOps.isEnabled()/window.analyticsOps.setEnabled() - Info box: Brief explanation of what is and isn't collected (feature usage counts, app version, platform — NOT project names, file contents, prompts, git data, or PII)
Add to SECTIONS array in SettingsModal.tsx:
{ id: 'privacy', label: 'Privacy', icon: Eye }Add import and conditional rendering in content area.
// src/preload/index.ts
const analyticsOps = {
track: (event: string, properties?: Record<string, unknown>) =>
ipcRenderer.invoke('telemetry:track', event, properties),
setEnabled: (enabled: boolean) =>
ipcRenderer.invoke('telemetry:setEnabled', enabled),
isEnabled: () =>
ipcRenderer.invoke('telemetry:isEnabled') as Promise<boolean>
}analyticsOps: {
track: (event: string, properties?: Record<string, unknown>) => Promise<void>
setEnabled: (enabled: boolean) => Promise<void>
isEnabled: () => Promise<boolean>
}Register inline in src/main/index.ts or in a new src/main/ipc/telemetry-handlers.ts:
ipcMain.handle('telemetry:track', (_e, event, props) => telemetryService.track(event, props))
ipcMain.handle('telemetry:setEnabled', (_e, enabled) => telemetryService.setEnabled(enabled))
ipcMain.handle('telemetry:isEnabled', () => telemetryService.isEnabled())| File | Purpose |
|---|---|
src/main/services/telemetry-service.ts |
Core PostHog singleton service |
src/renderer/src/components/settings/SettingsPrivacy.tsx |
Privacy settings UI section |
| File | Change |
|---|---|
package.json |
Add posthog-node dependency |
src/main/index.ts |
Init telemetry service, register IPC handlers, track app_launched/app_session_ended, shutdown on quit |
src/preload/index.ts |
Expose window.analyticsOps namespace |
src/preload/index.d.ts |
Add analyticsOps type declarations |
src/renderer/src/components/settings/SettingsModal.tsx |
Add Privacy section to navigation and content |
src/renderer/src/stores/useSettingsStore.ts |
Add telemetryEnabled: boolean to AppSettings (renderer cache) |
src/renderer/src/components/setup/AgentSetupGuard.tsx |
Track onboarding_completed event |
src/main/ipc/database-handlers.ts |
Track project_added event |
src/main/ipc/worktree-handlers.ts |
Track worktree_created event |
src/main/ipc/opencode-handlers.ts |
Track session_started, prompt_sent events |
src/main/ipc/git-file-handlers.ts |
Track git_commit_made, git_push_made events |
src/main/ipc/script-handlers.ts |
Track script_run event |
src/main/ipc/connection-handlers.ts |
Track connection_created event |
src/main/ipc/settings-handlers.ts |
Track worktree_opened_in_editor event |
| Pattern | Reference File |
|---|---|
| Singleton service | src/main/services/logger.ts |
| Settings UI section | src/renderer/src/components/settings/SettingsSecurity.tsx |
| IPC bridge namespace | src/preload/index.ts (any existing namespace) |
| SQLite settings | window.db.setting.get/set — no schema migration needed |
- Build:
pnpm build— no TypeScript errors with new types - Dev mode:
pnpm dev— check main process logs for "Telemetry initialized" with distinct ID - PostHog Live Events: Use the app (add project, create worktree, start session, send prompt) → verify events appear in PostHog dashboard within ~30s
- Opt-out toggle: Settings > Privacy → toggle off → verify no further events in PostHog
- Opt-out persistence: Toggle off → restart app → verify telemetry stays disabled (check logs)
- Lint:
pnpm lint— no lint errors - Tests:
pnpm test— existing tests pass (mockwindow.analyticsOpsin test setup)
These events could be added later for deeper product understanding:
model_changed— model selection patternscommand_palette_used— command palette engagementworktree_archived/worktree_deleted— lifecycle completionsession_mode_changed— build vs plan mode usageterminal_created— embedded terminal adoption- Session recording via
posthog-jsin renderer (separate effort)