* feat(cli): associate signed-in HeyGen account with telemetry
Sign-in telemetry currently attributes everything to the anonymous
install id, so the sign-in funnel can be counted but a completed sign-in
can't be tied to the account it produced. This associates the two.
- On a completed sign-in, emit a PostHog `$identify` alias whose
`$anon_distinct_id` is the install's anonymousId, so events recorded
before sign-in stitch to the same person, and tag `auth_login_completed`
with the account identity (the pre-plumbed `distinctId`).
- `/v3/users/me` exposes no opaque user_id, so the identity key is the
account email, falling back to username (single `identityKey` helper).
- Both no-op under the `telemetry disable` opt-out and only fire after
the user chooses to sign in.
Privacy disclosure updated in lockstep, since this is the first PII the
CLI attaches: the first-run telemetry notice and the telemetry section
of docs/packages/cli.mdx now state that signing in links your account
email to your usage.
Tests: identifyUser payload + no-op, completion attribution incl.
username fallback and no-identity-on-reject/empty. Verified end-to-end
against the built CLI: pre-auth events anonymous, $identify carries
$anon_distinct_id, completion carries the account email.
* docs(cli): disclose the username identity fallback
Review gating item: identityKey is `email ?? username`, but the
first-run notice and cli.mdx said only "email", so an emailless
account's username would reach PostHog undisclosed. `/v3/users/me`
treats email as optional (pickString), so the fallback is live code,
not dead — disclose it rather than assert an unverifiable email
guarantee. Both surfaces now say "email, or username if the account
has no email".
Also soften the identityKey comment: it implied username is "less
identifying", but HeyGen usernames are often email-shaped, so the note
now states username is a fallback, not a privacy win.
* feat(cli/telemetry): surface unrecognized agents in the agent_runtime=null bucket
agent_runtime is a closed allowlist: an agent we have no rule for collapses
to null with no trace of what it was, so ~18% of CLI users are unattributable
and new agents stay invisible until reverse-engineered by hand.
Add detectAgentHints(), a self-populating residual signal computed only for
the null bucket (gated off classified events):
- agent_hint: value of AGENT / AI_AGENT (the emerging self-identification
convention; Crush and Goose set AGENT=<name>) — names agents the allowlist
misses.
- term_program: raw TERM_PROGRAM (editor name) — catches the IDE-terminal
class the same way the cursor/windsurf rules do.
- agent_env_hints: sorted, comma-joined "agent-ish" env-var KEY names present
but matched by no vendor rule — a fingerprint that clusters by agent.
Privacy stays consistent with the existing "never read secret-shaped values"
stance: agent_env_hints emits key names only; the three value-reads are vars
whose sole purpose is non-secret identification, each passed through a strict
short-slug allowlist so anything long/spaced/secret-shaped is dropped.
Breaking down agent_hint / agent_env_hints filtered to agent_runtime IS NULL
AND is_tty=false gives a ranked leaderboard of new agents to promote into
VENDOR_RULES.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli/telemetry): guard agent_hint/term_program against short credential-shaped values
Review feedback (Magi, #1978): the short-slug allowlist in sanitizeHint() still
accepted short credential-shaped values (AGENT=sk-ant-api03,
AGENT=AKIAIOSFODNN7EXAMPLE, AGENT=github_pat_abc), so the "never emit a secret"
claim wasn't actually enforced — only overlong values were dropped.
Add a credential-shape guard on top of the slug allowlist:
- known token/credential prefixes (sk-, ghp_, github_pat_, akia, ya29, ...)
- any unbroken alphanumeric run >= 16 chars (key bodies, hex, base64-ish),
while agent names segment on _/-/. and keep each run short.
Replace the single overlong-value test with the short credential shapes from the
review (parametrized) plus a positive case (gemini_managed_agent) proving real
multi-segment names still pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Studio-triggered renders emit render_complete / render_error from the CLI
preview-server process, which stamps every event with the install's
anonymousId (client.ts drainQueueToPayload). The browser, meanwhile, fires
studio_session_start / studio_render_start under its own getAnonymousId(). So
the render outcome and the render start never share a person_id — verified in
data: of 15,125 users who started a studio render in 30d, ZERO have any
render_complete under any source, and the 898 studio-tagged completers are
disjoint server UUIDs. The studio render funnel — the product's core value
moment and strongest retention signal — is therefore unmeasurable.
Thread the browser's telemetry id through to the render-outcome events:
- client.ts: trackEvent takes an optional distinctId; drainQueueToPayload uses
`event.distinctId ?? config.anonymousId`. CLI renders unchanged.
- events.ts: trackRenderComplete/trackRenderError forward an optional distinctId.
- studioRenderTelemetry.ts: emitStudioRender* pass opts.distinctId through.
- core studio-api (types.ts + routes/render.ts): the render route reads
`telemetryDistinctId` from the request body (validated string) and passes it
to the adapter's startRender, which already forwards opts to the emitters.
- studio (useRenderQueue.ts): include getAnonymousId() as telemetryDistinctId
in the render POST — the same id studio_* events already use.
Result: studio render_complete/error now carry the browser user's id and join
studio_session_start / studio_render_start. Older clients that don't send the
field fall back to anonymousId (no regression). No new tracking surface — it's
the existing anonymous studio id.
Tests: per-event override forwarding (events), studio render distinctId
threading + older-client fallback (studioRenderTelemetry), and route body →
adapter forwarding incl. non-string rejection (core render route).
Add two new properties to every CLI telemetry event so we can tell
managed-sandbox traffic (Codex Cloud, Claude Code Web, etc.) apart from
real developer laptops without geolocation guesswork:
- sandbox_runtime: 'gvisor' | 'firecracker' | 'docker' | 'kvm' | 'wsl' | null
gVisor detected via kernel string ('4.19.0-gvisor' or legacy Sentry
'4.4.0') + /proc/version. Firecracker via /dev/vsock + DMI sys_vendor.
Docker reuses the existing /.dockerenv + cgroup probe.
- agent_runtime: claude_code | codex | cursor | copilot_agent | jules
| replit | devin | aider | gemini_cli | hermes | openclaw | null
Detected by the EXISTENCE of well-known vendor env vars only — values
are never read. Hermes rule keys on HERMES_QUIET=1 (set unconditionally
at hermes-agent/cli.py:50). openclaw rule keys on OPENCLAW_STATE_DIR
or OPENCLAW_CONFIG_PATH (set explicitly in the spawned child env at
openclaw/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts).
Drive-by cleanups required by fallow because system.ts and client.ts
fall into the audit scope of this PR:
- Extract detectWSL into platform.ts to break the system.ts ↔ agent_runtime.ts cycle.
- Refactor detectCI / getCIName into a single CI_PROVIDERS table.
- Dedupe flush / flushSync via a shared drainQueueToPayload helper.
Privacy posture unchanged: HYPERFRAMES_NO_TELEMETRY=1 still opts out;
disclosure in docs/packages/cli.mdx updated to enumerate the new fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI=true early-exit in shouldTrack() was hiding most modern usage
(coding agents in Codespaces, CI pipelines, agent sandboxes). Remove it.
Each event still carries is_ci/is_docker/is_tty from system.ts, so CI vs
laptop traffic can be separated in PostHog without being dropped at
ingestion.
HeyGen's own CI is suppressed via HYPERFRAMES_NO_TELEMETRY=1 added to
each workflow that exercises the CLI.
* feat(cli): add system metrics to telemetry and expand doctor command
Enrich render telemetry with device/environment metadata (CPU, memory,
OS, Docker/CI/WSL detection) following patterns from Next.js and
Turborepo. Add speed_ratio (render time / composition duration),
per-frame capture timing, and resource usage to render events.
Expand the doctor command with CPU, memory, disk, /dev/shm, and
environment checks to help debug rendering issues on user machines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): invert speed_ratio to match experiment-framework convention
composition_duration / render_time — higher is better, >1 means faster
than realtime. Matches magic_edit.render.speed_ratio in experiment-framework.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): wire errorMessage into render error telemetry
Address review feedback — the errorMessage field was declared in the
trackRenderError interface but never populated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): add render telemetry to embedded studio server
Track render_complete and render_error from the studio's render API
endpoint (hyperframes dev). Uses dynamic imports so telemetry is
resolved at call time within the CLI package — no telemetry coupling
added to @hyperframes/studio or @hyperframes/producer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- Adds `process.exit(0)` after render completes in the CLI `render` command
- Fixes the process hanging indefinitely after `npx hyperframes render --output video.mp4`
- Root cause: Node.js `fetch()` keep-alive pool (from update checks and telemetry) keeps TCP connections open, preventing the event loop from draining
- Telemetry is preserved — the `exit` handler in `cli.ts` calls `flushSync()` which spawns a detached child process
## Test plan
- [x] Run `npx hyperframes render --output test.mp4` and verify the process exits after completion
- [x] Verify telemetry events are still sent (check PostHog)
- flushSync: use detached spawn + unref() instead of execFileSync,
so process.exit() paths don't block up to 5s on slow networks
- showTelemetryNotice: persist notice flag BEFORE printing/tracking,
so users are never tracked without having seen the disclosure
- Config dir: set mode 0o700 on ~/.hyperframes/ directory (was umask default)
- $ip: null comment: clarify this is belt-and-suspenders with server-side discard
- shouldTrack: update comment — phc_ prefix check is a safety net, not dead code
- env.ts: add comment explaining try/catch fail-safe defaults to production
- init.ts: consistently call trackInitTemplate after scaffoldProject in both paths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run oxfmt on cli.ts and client.ts
- Replace literal placeholder comparison with phc_ prefix check
(TS2367: comparing two different string literals has no overlap)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PostHog's $ip: null property tells the server to not associate the
request IP with the event. Combined with the "Discard client IP data"
project setting for server-side enforcement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract shared isDevMode() to utils/env.ts (was duplicated in dev.ts and client.ts)
- Use ui/colors.ts instead of raw ANSI escapes in telemetry notice (respects NO_COLOR)
- Derive known commands from subCommands object instead of maintaining duplicate set
- Skip telemetry on --help/--version and unknown commands
- Gate incrementCommandCount() behind shouldTrack() (no disk writes in CI)
- Add flushSync() for process.exit() paths (beforeExit doesn't fire on explicit exit)
- Remove dead trackBrowserInstall(success) param (failure path never called it)
- Remove redundant isEnabled/anonymousId caching in client.ts (config.ts cache suffices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add anonymous usage telemetry to help improve the CLI. Uses PostHog's
HTTP batch API directly (zero new dependencies) with a 5-second timeout
and fail-silent behavior — telemetry never breaks the CLI.
What's collected: command names, render performance (duration, fps,
quality), template choices, OS/arch/Node version/CLI version.
What's NOT collected: file paths, project names, video content, or
any personally identifiable information.
Telemetry is:
- Disabled in dev mode (running via tsx)
- Disabled in CI (CI=true) or via HYPERFRAMES_NO_TELEMETRY=1
- Disabled when API key is placeholder (safe to merge before key is set)
- Controllable via `hyperframes telemetry [enable|disable|status]`
- Disclosed on first run with clear opt-out instructions
Config stored at ~/.hyperframes/config.json (0600 permissions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>