mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(telemetry): unify CLI and Studio PostHog identity (Layer 1) (#1829)
* feat(telemetry): unify CLI and Studio PostHog identity (Layer 1) Seed the CLI's anonymous distinct_id into Studio at launch so a developer's CLI and their Studio browser session resolve to the same PostHog person. Also unifies Studio's two previously-independent anonymous ids into one source of truth. Uses only the existing anonymous machine id (no new PII). - cli: inject window.__HF_CLI_DISTINCT_ID into the served index.html <head> (mirrors the existing __HF_STUDIO_ENV__ injection) + add a fallback GET /api/telemetry-identity endpoint. Only seeds when CLI telemetry is enabled; empty/no-op otherwise. - studio: new telemetry/distinctId.ts single source of truth; adopts the CLI-seeded id when present, else falls back to the existing per-browser localStorage id. Both Studio clients (studio:* and studio_*/render) now share this one id. * fix(telemetry): keep Studio distinct_id resolver fail-silent on getItem resolveStudioDistinctId read localStorage.getItem() outside a try/catch while every other external access in the module is guarded. In a storage-restricted context where the localStorage reference resolves but getItem throws, the resolver threw — breaking the module's fail-silent contract (telemetry must never break Studio). Guard the reads and treat a throw as "no id". Also drop an unnecessary `as` cast in the test per the repo CLAUDE.md convention (the optional global is already declared). * refactor(telemetry): address review feedback on identity unification - dedup safeLocalStorage/safeSessionStorage into utils/safeStorage.ts, used by both telemetry/config.ts and telemetry/distinctId.ts (Miga #6) - replace redundant `??=` with `=` in the no-storage branch; cachedId is guaranteed null there (Miga #2) - extract buildStudioHeadScripts() so the "identity script before env script" head-injection ordering is a pure, tested invariant (Miga #5) - add tests: head-script ordering + telemetry-off passthrough, and a Studio memoization test proving an adopted CLI id survives a later window.__HF_CLI_DISTINCT_ID reassignment (Rames) - clarify the XSS-escaping comment (both < and / escaped so no </script> sequence can form) (Miga #1)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI → Studio telemetry identity (Layer 1).
|
||||
//
|
||||
// The CLI owns both the Studio launch and the local server, so it seeds the
|
||||
// browser with its own anonymous `config.anonymousId`. Studio adopts it as its
|
||||
// distinct_id (see packages/studio/src/telemetry/distinctId.ts), so the CLI's
|
||||
// `cli_command*` events and the browser's `studio:*` / `studio_*` / render
|
||||
// events are attributed to one PostHog person.
|
||||
//
|
||||
// This uses ONLY the existing anonymous machine id (a random UUID, no PII), so
|
||||
// the "no personal info" telemetry disclosure stays valid. When CLI telemetry
|
||||
// is disabled (opt-out / dev / CI / DO_NOT_TRACK) nothing is seeded and Studio
|
||||
// behaves exactly as if opened standalone.
|
||||
//
|
||||
// Kept out of studioServer.ts so it can be unit-tested without pulling in the
|
||||
// server's heavy render dependencies (@hyperframes/producer, engine, …).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { readConfig } from "../telemetry/config.js";
|
||||
import { shouldTrack as telemetryShouldTrack } from "../telemetry/client.js";
|
||||
|
||||
/**
|
||||
* The CLI's anonymous distinct id to hand to Studio, or null when CLI telemetry
|
||||
* is disabled or no id is available. Fail-silent — telemetry must never break
|
||||
* the preview server.
|
||||
*/
|
||||
export function resolveCliTelemetryDistinctId(): string | null {
|
||||
try {
|
||||
if (!telemetryShouldTrack()) return null;
|
||||
const id = readConfig().anonymousId;
|
||||
return typeof id === "string" && id.length > 0 ? id : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `<script>` tag to inject into the served index.html `<head>`, publishing the
|
||||
* CLI distinct id as `window.__HF_CLI_DISTINCT_ID` before the studio bundle
|
||||
* runs. Preferred over a URL param so the id never leaks into `$current_url` /
|
||||
* `url_hash` telemetry or browser history. Empty string when there's nothing to
|
||||
* seed (telemetry off / no id).
|
||||
*/
|
||||
export function buildCliIdentityScript(): string {
|
||||
const cliId = resolveCliTelemetryDistinctId();
|
||||
if (!cliId) return "";
|
||||
// The id is a randomUUID() so this is belt-and-suspenders, but JSON.stringify
|
||||
// does not escape "<" or "/". Escaping both means no "</script>" (or "</…")
|
||||
// sequence can form in the emitted value, so it can never terminate the
|
||||
// inline <script> or open a new tag.
|
||||
const encoded = JSON.stringify(cliId).replace(/</g, "\\u003c").replace(/\//g, "\\/");
|
||||
return `<script>window.__HF_CLI_DISTINCT_ID=${encoded};</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the scripts injected into the served Studio `index.html` `<head>`.
|
||||
* The CLI identity script MUST come first so `window.__HF_CLI_DISTINCT_ID` is
|
||||
* set before the (deferred) Studio bundle runs telemetry init and reads it;
|
||||
* `envScript` is the existing `window.__HF_STUDIO_ENV__` injection. Keeping the
|
||||
* ordering in one pure, tested function guards against a future `<head>` inject
|
||||
* silently landing ahead of the identity script and reintroducing a boot race.
|
||||
*/
|
||||
export function buildStudioHeadScripts(envScript: string): string {
|
||||
return `${buildCliIdentityScript()}${envScript}`;
|
||||
}
|
||||
Reference in New Issue
Block a user