test+fix(telemetry): address PR review — dev-mode gate, session-storage dedupe, payload tests

Addresses review comments on #982:

- studio shouldTrack(): adds VITE_HYPERFRAMES_NO_TELEMETRY (mirrors CLI's
  HYPERFRAMES_NO_TELEMETRY) and import.meta.env.DEV gates so dev / CI
  studio builds don't pollute production telemetry. shouldTrack() is now
  exported for testability.
- App.tsx session dedupe: moves the once-per-session check from a useRef
  (which resets on HMR / remount) to sessionStorage via new
  hasFiredSessionStart / markSessionStartFired helpers in config.ts.
- studioRenderTelemetry.ts: documents why `workers` is intentionally
  omitted from emitStudioRenderError (studio renders don't accept a
  user-supplied worker count, so early failures genuinely don't know one).
- client.ts flush(): documents fire-and-forget no-retry design so future
  hands don't accidentally add retry logic that double-counts.

Tests:
- studioRenderTelemetry.test.ts (8 tests): perfPayload mapping for every
  RenderPerfSummary field, undefined-perf path, missing-extract path,
  zero-elapsed edge case, error event shape.
- studio/telemetry/events.test.ts (4 tests): pin event names
  (studio_session_start, studio_render_start) and payload shape.
- studio/telemetry/client.test.ts (9 tests): shouldTrack() returns false
  for non-phc_ key, opt-out, doNotTrack, build-time env, vite dev mode;
  memoization.
This commit is contained in:
James
2026-05-20 14:53:38 -04:00
committed by James Russo
parent 3cc4c82f9e
commit da38de1b12
7 changed files with 393 additions and 8 deletions
@@ -0,0 +1,100 @@
// @vitest-environment happy-dom
import { describe, expect, it, vi, beforeEach } from "vitest";
// `shouldTrack()` reads `POSTHOG_API_KEY` from module-level const that's
// evaluated at module load time, so changing `import.meta.env` after import
// has no effect on the key. Each test resets module cache and re-imports.
const OPT_OUT_KEY = "hyperframes-studio:telemetryDisabled";
function setKey(value: string | undefined): void {
if (value === undefined) {
delete (import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_POSTHOG_KEY;
} else {
(import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_POSTHOG_KEY = value;
}
}
function setNoTelemetry(value: string | undefined): void {
if (value === undefined) {
delete (import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_NO_TELEMETRY;
} else {
(import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_NO_TELEMETRY = value;
}
}
function setDev(value: boolean): void {
(import.meta.env as { DEV: boolean }).DEV = value;
}
async function loadShouldTrack(): Promise<() => boolean> {
vi.resetModules();
const mod = await import("./client");
return mod.shouldTrack;
}
describe("studio client shouldTrack", () => {
beforeEach(() => {
setDev(false);
setKey("phc_test_key");
setNoTelemetry(undefined);
localStorage.clear();
vi.unstubAllGlobals();
});
it("returns true when key is configured, not in dev mode, and no opt-outs", async () => {
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(true);
});
it("returns false when API key does not start with phc_", async () => {
setKey("not_a_real_key");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when API key is empty string", async () => {
setKey("");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when user has opted out via localStorage", async () => {
localStorage.setItem(OPT_OUT_KEY, "1");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when navigator.doNotTrack is '1'", async () => {
vi.stubGlobal("navigator", { ...navigator, doNotTrack: "1" });
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when VITE_HYPERFRAMES_NO_TELEMETRY=1 at build time", async () => {
setNoTelemetry("1");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when VITE_HYPERFRAMES_NO_TELEMETRY='true'", async () => {
setNoTelemetry("true");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false in vite dev mode", async () => {
setDev(true);
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("memoizes its decision after the first call", async () => {
const shouldTrack = await loadShouldTrack();
const first = shouldTrack();
// Flip an underlying input — memoized return must not change.
localStorage.setItem(OPT_OUT_KEY, "1");
expect(shouldTrack()).toBe(first);
});
});