mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
* 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)
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
// CLI → Studio telemetry identity seeding (Layer 1). Verifies the server only
|
|
// hands the browser a distinct id when CLI telemetry is enabled, and passes
|
|
// through the anonymous machine id (no PII) otherwise.
|
|
|
|
const shouldTrack = vi.fn();
|
|
const readConfig = vi.fn();
|
|
|
|
vi.mock("../telemetry/client.js", () => ({
|
|
shouldTrack: (...args: unknown[]) => shouldTrack(...args),
|
|
}));
|
|
vi.mock("../telemetry/config.js", () => ({
|
|
readConfig: (...args: unknown[]) => readConfig(...args),
|
|
}));
|
|
|
|
const { resolveCliTelemetryDistinctId, buildCliIdentityScript, buildStudioHeadScripts } =
|
|
await import("./telemetryIdentity.js");
|
|
|
|
describe("resolveCliTelemetryDistinctId", () => {
|
|
beforeEach(() => {
|
|
shouldTrack.mockReset();
|
|
readConfig.mockReset();
|
|
});
|
|
|
|
it("returns the CLI anonymousId when telemetry is enabled", () => {
|
|
shouldTrack.mockReturnValue(true);
|
|
readConfig.mockReturnValue({ anonymousId: "machine-uuid" });
|
|
expect(resolveCliTelemetryDistinctId()).toBe("machine-uuid");
|
|
});
|
|
|
|
it("returns null when telemetry is disabled (opt-out / dev / CI)", () => {
|
|
shouldTrack.mockReturnValue(false);
|
|
readConfig.mockReturnValue({ anonymousId: "machine-uuid" });
|
|
expect(resolveCliTelemetryDistinctId()).toBeNull();
|
|
// Must not even read config when suppressed.
|
|
expect(readConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns null when there is no anonymousId", () => {
|
|
shouldTrack.mockReturnValue(true);
|
|
readConfig.mockReturnValue({ anonymousId: "" });
|
|
expect(resolveCliTelemetryDistinctId()).toBeNull();
|
|
});
|
|
|
|
it("never throws — returns null if config reading fails", () => {
|
|
shouldTrack.mockReturnValue(true);
|
|
readConfig.mockImplementation(() => {
|
|
throw new Error("disk error");
|
|
});
|
|
expect(resolveCliTelemetryDistinctId()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("buildCliIdentityScript", () => {
|
|
beforeEach(() => {
|
|
shouldTrack.mockReset();
|
|
readConfig.mockReset();
|
|
});
|
|
|
|
it("emits a script that sets window.__HF_CLI_DISTINCT_ID when telemetry is on", () => {
|
|
shouldTrack.mockReturnValue(true);
|
|
readConfig.mockReturnValue({ anonymousId: "machine-uuid" });
|
|
expect(buildCliIdentityScript()).toBe(
|
|
'<script>window.__HF_CLI_DISTINCT_ID="machine-uuid";</script>',
|
|
);
|
|
});
|
|
|
|
it("emits an empty string when telemetry is disabled (nothing to seed)", () => {
|
|
shouldTrack.mockReturnValue(false);
|
|
expect(buildCliIdentityScript()).toBe("");
|
|
});
|
|
|
|
it("JSON-encodes the id so it can't break out of the script literal", () => {
|
|
shouldTrack.mockReturnValue(true);
|
|
readConfig.mockReturnValue({ anonymousId: "</script><script>alert(1)" });
|
|
const script = buildCliIdentityScript();
|
|
// The raw closing tag must be escaped by JSON.stringify, not emitted literally.
|
|
expect(script).not.toContain("</script><script>alert(1)");
|
|
expect(script).toContain("window.__HF_CLI_DISTINCT_ID=");
|
|
});
|
|
});
|
|
|
|
describe("buildStudioHeadScripts", () => {
|
|
beforeEach(() => {
|
|
shouldTrack.mockReset();
|
|
readConfig.mockReset();
|
|
});
|
|
|
|
const ENV_SCRIPT = "<script>window.__HF_STUDIO_ENV__={};</script>";
|
|
|
|
it("places the CLI identity script before the env script so the global is set first", () => {
|
|
shouldTrack.mockReturnValue(true);
|
|
readConfig.mockReturnValue({ anonymousId: "machine-uuid" });
|
|
const head = buildStudioHeadScripts(ENV_SCRIPT);
|
|
expect(head.indexOf("__HF_CLI_DISTINCT_ID")).toBeGreaterThanOrEqual(0);
|
|
expect(head.indexOf("__HF_CLI_DISTINCT_ID")).toBeLessThan(head.indexOf("__HF_STUDIO_ENV__"));
|
|
});
|
|
|
|
it("returns just the env script when identity is suppressed (telemetry off)", () => {
|
|
shouldTrack.mockReturnValue(false);
|
|
expect(buildStudioHeadScripts(ENV_SCRIPT)).toBe(ENV_SCRIPT);
|
|
});
|
|
});
|