Files
OpenHands/.env.sample
T
HeyItsChloeandopenhands 466164808a feat: add posthog events for install, conversation creation, and automations (#1036)
* feat(telemetry): add posthog events for install, conversation creation, and prebuilt automation

- canvas_install already fires pre-consent via telemetry.ts
- Add conversation_created to NPM library telemetry (trackEvent) in use-create-conversation.ts onSuccess
- Add prebuilt_automation_enabled event in recommended-automations-launcher.tsx when a catalog automation is successfully launched
- Add prebuilt_automation_enabled event in automations-list.tsx and automation-detail.tsx handleToggle when enabling (not disabling)
- All consent-gated events route through telemetry.ts to PostHog prod via POSTHOG_PROD_KEY baked in by build:lib

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: align posthog tracking with existing useTracking hook pattern

- Add trackPrebuiltAutomationEnabled to useTracking hook (typed, named,
  includes commonProperties like all other tracked events)
- Remove duplicate conversation_created trackEvent call in
  use-create-conversation (trackConversationCreated already covers it)
- Replace all trackEvent(telemetry.ts) calls in automation routes and
  recommended-automations-launcher with useTracking hook functions
- Add trackPrebuiltAutomationEnabled to useCallback dep array in launcher

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: implement dual-system PostHog tracking (Option C)

System 1 — telemetry.ts (anonymous, library-level):
- canvas_install and canvas_new_session remain in telemetry.ts
- trackEvent() stays as the public library API (lib/index.ts export)
- Rule: never import trackEvent in app routes/components

System 2 — useTracking hook (identified, app-level):
- Wire posthog_client_key from VITE_POSTHOG_CLIENT_KEY env var in
  OptionService.getConfig() so PostHogProvider mounts when key is set
- Document VITE_POSTHOG_CLIENT_KEY in .env.sample
- Add 6 typed tracking functions to useTracking:
    trackInitialQuerySubmitted, trackUserMessageSent,
    trackDownloadVsCodeButtonClicked, trackSettingsSaved,
    trackMcpConfigUpdated, trackDownloadTrajectoryButtonClicked
- All include commonProperties (current_url, user_email) automatically
- Migrate all 6 raw usePostHog()/posthog.capture() call sites:
    chat-interface.tsx, conversation-card.tsx, settings-form.tsx,
    use-save-settings.ts, use-download-conversation.ts
- Rule: never call posthog.capture() directly from components

Consent unification:
- AnalyticsConsentFormModal now calls setTelemetryConsent() after saving
  user_consents_to_analytics so both systems honour the same decision

Documentation:
- AGENTS.md: tracking architecture section with boundary rules,
  system ownership table, and instructions for adding new events

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: treat null consent as opted-out in useSyncPostHogConsent

PostHog defaults to capturing enabled on init. The previous hook only
called opt_out_capturing() when user_consents_to_analytics was explicitly
false, leaving a window where events fired while settings were loading
(null) or on first visit before the user had made a decision.

Changes:
- null and false both route to opt_out_capturing() — only an explicit
  true opts PostHog in
- Remove hasSyncedRef: the hook now reacts to every settings change,
  which correctly handles consent granted in another tab or after the
  initial load. handleCaptureConsent is idempotent so repeated calls
  are safe.
- Update comment to document the three-state consent model

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add unit tests for tracking and consent changes

New test files:
- use-sync-posthog-consent.test.ts (7 tests)
  Covers the null/false/true consent states and reactive re-sync
  behaviour. Specifically tests the bug fix: null now opts out.

- use-tracking.test.ts (17 tests)
  One test per tracking function. Verifies correct event names,
  property mapping (snake_case / SCREAMING_SNAKE_CASE), and that
  commonProperties (current_url, user_email) are included.
  Also covers git_user_email fallback and null user_email.

- analytics-consent-form-modal.test.tsx (5 tests)
  Verifies setTelemetryConsent is called with 'granted'/'denied'
  matching the form checkbox state on submission.

- use-save-settings.test.ts (4 tests)
  Covers trackMcpConfigUpdated: fires on new config, skips on
  absent config, skips on same object reference, handles zero counts.

- use-download-conversation.test.ts (3 tests)
  Covers trackDownloadTrajectoryButtonClicked: called on download,
  called before the API request, still called even when API rejects.

Updated test files:
- option-service.test.ts
  Two new cases for posthog_client_key: returns key from env var
  when set, returns null when env var is absent.

- chat-interface.test.tsx
  Added vi.mock('#/hooks/use-tracking') to prevent test noise.
  New 'Tracking' describe block: verifies trackInitialQuerySubmitted
  fires on first message and trackUserMessageSent on follow-ups.

- conversation-card.test.tsx
  Added vi.mock('#/hooks/use-tracking') guard.

- settings-form.test.tsx
  Added vi.mock('#/hooks/use-tracking') guard.
  New test: verifies trackSettingsSaved is called with LLM_API_KEY_SET
  and SEARCH_API_KEY_SET on form submission.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(tests): resolve two CI failures in tracking tests

use-tracking.test.ts: COMMON.current_url was hardcoded as
'http://localhost/' but jsdom in CI runs at 'http://localhost:3000/'.
Change COMMON from a module-level const to a let populated in beforeEach
via window.location.href so it always matches the environment.

chat-interface.test.tsx: trackUserMessageSent was never called (0 times)
because totalEvents is derived from uiEvents via useFilteredEvents, not
from the events array. The test was setting events but leaving uiEvents
empty, so totalEvents stayed 0 and the component took the
trackInitialQuerySubmitted branch instead. Seed uiEvents with a minimal
MessageEvent (has llm_message.role + content, which isMessageEvent and
shouldRenderAgentServerEvent both accept) so totalEvents = 1.

Co-authored-by: openhands <openhands@all-hands.dev>

* style: fix prettier formatting in option-service getConfig

?? null was split onto its own line but the full expression fits
within the 80-char printWidth (79 chars), so prettier requires them
joined on one line.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(tests): resolve 4 TypeScript type errors in test files

analytics-consent-form-modal.test.tsx:
  Spread of unknown[] is not valid — TS requires a tuple type or rest
  param. Replace (...args: unknown[]) => mock(...args) with a typed
  single-arg forwarder: (consent: string) => mock(consent).

use-save-settings.test.ts:
  MCPConfig requires shttp_servers (added in a later migration).
  Add shttp_servers: [] to all three fixture objects (full config,
  sharedConfig reference-equality test, and zero-count test).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(tests): type setTelemetryConsentMock to accept a string arg

vi.fn(() => Promise.resolve()) infers zero parameters, so calling
it with a string argument fails TS2554. Add _consent: string to
the implementation so the inferred mock signature matches the call.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(tracking): add explicit consent guard to useTracking

All tracking functions now require posthog to be initialized AND
user_consents_to_analytics === true before calling posthog.capture().

Previously, events were blocked only via PostHog's opt-out mechanism
(set by useSyncPostHogConsent), leaving three gaps:
  1. posthog undefined (no VITE_POSTHOG_CLIENT_KEY) - silent no-op
  2. Desync between useSyncPostHogConsent and useTracking can cause
     events to fire or be dropped depending on hook ordering
  3. No self-documenting enforcement of the documented requirement
     that System 2 requires user_consents_to_analytics === true

The new internal track() helper short-circuits when either condition
is unmet, so each tracking function is self-contained and the consent
contract is explicit in the code that enforces it.

Tests: beforeEach now includes user_consents_to_analytics: true; posthog
mock is made variable so the undefined case can be tested; new 'consent
gate' describe block covers all blocked cases (false, null, undefined
settings, no posthog).

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: add usePostHogIdentify hook for cloud-mode PostHog identity

Calls posthog.identify(userId, { email }) for cloud users who have
granted analytics consent. Required because PostHogProvider is
initialized with person_profiles='identified_only', which silently
drops all events until identify() has been called.

Identity lifecycle:
- consent === true + userId present → posthog.identify()
- consent === false (explicit denial) → posthog.reset()
- userId becomes null after identify (logout) → posthog.reset()
- consent === null / settings loading → no-op

userId sourced from useCloudCurrentUserId() (cloud mode only).
Local mode is skipped — no stable server-issued user ID available.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-03 13:44:17 -07:00

41 lines
3.1 KiB
Bash

# OpenHands Agent Server target
# These defaults assume you are manually pointing the frontend at a backend on
# 127.0.0.1:8000. The recommended local workflow is `npm run dev`, which starts
# an isolated local backend for this checkout. Use `npm run dev:frontend` only
# when you intentionally want to point at a separately managed backend.
VITE_BACKEND_HOST="127.0.0.1:8000" # Host:port used by the Vite dev proxy
VITE_BACKEND_BASE_URL="http://127.0.0.1:8000" # Base URL used by browser-side direct requests
# VITE_SESSION_API_KEY="" # Set to the same value as backend SESSION_API_KEY or OH_SESSION_API_KEYS_0 when auth is enabled
# VITE_WORKING_DIR="/workspace/project/agent-canvas" # Base dir for per-conversation working_dirs. Each conversation's working_dir is <VITE_WORKING_DIR>/<id_hex>. Defaults to <OH_CANVAS_SAFE_STATE_DIR>/workspaces, which is the sibling of the agent server's <state_dir>/conversations/ persistence dir — both share the same <id_hex> per conversation.
# VITE_WORKER_URLS="" # Optional comma-separated worker URLs for the Browser tab
# VITE_ENABLE_BROWSER_TOOLS="true" # Set to false to omit BrowserToolSet from new conversations
# VITE_LOAD_PUBLIC_SKILLS="false" # Set to false to disable loading public skills from https://github.com/OpenHands/extensions (on by default)
# Frontend dev server
VITE_FRONTEND_PORT="3001" # Port to run the frontend application
VITE_USE_TLS="false" # Use HTTPS/WSS for proxied backend connections
VITE_INSECURE_SKIP_VERIFY="false" # Skip TLS certificate verification for proxied backend requests
# App-level PostHog project key — enables useTracking (identified, behaviour analytics).
# Events route to https://us.i.posthog.com under this project. Omit to disable the
# PostHogProvider entirely (useTracking calls are silently dropped).
# VITE_POSTHOG_CLIENT_KEY="phc_..."
# Deployment environment — controls which PostHog project key is compiled in.
# Set to "production" or "staging" only in real deployment build pipelines.
# Leave unset locally so developers never accidentally send telemetry.
# VITE_APP_ENV="production"
# Mocking / test helpers
VITE_MOCK_API="false" # Enable/disable API mocking with MSW
# `npm run dev` (scripts/dev-safe.mjs) — isolated local backend via uvx
# OH_CANVAS_SAFE_BACKEND_PORT="18000" # Port the spawned agent-server listens on
# OH_CANVAS_SAFE_VSCODE_PORT="18001" # Port forwarded to the embedded VS Code (defaults to backend port + 1)
# OH_CANVAS_SAFE_STATE_DIR="$HOME/.openhands/agent-canvas" # Where conversations and bash events are stored (tmux sockets live under /tmp)
# Agent server version selection (uvx) — listed in order of precedence (highest first)
# OH_AGENT_SERVER_LOCAL_PATH="" # Absolute path to a local software-agent-sdk checkout. Runs the local checkout via uvx with editable openhands-sdk/tools/workspace installs so source edits are picked up on restart. Highest precedence.
# OH_AGENT_SERVER_GIT_REF="" # Git commit SHA or branch name (e.g., "main", "abc1234"). Takes precedence over version.
# OH_AGENT_SERVER_VERSION="" # Specific PyPI version (e.g., "1.18.0"). Uses latest release if unset.