Files
hyperframes/packages/cli/src/telemetry/policy.test.ts
T
Vance IngallsandClaude Opus 5 3f69a2c635 fix(cli,core,studio): close 15 review findings + 2 R5 blockers
R5 blockers
- Negative install-state latch was cached for the process lifetime, but
  only `true` is monotonic across processes. A long-lived preview server
  held a stale `false` and could re-enrol after another process tripped
  the breaker. Only the positive is cached now; `false` re-reads.
- The real breaker writer used writeConfig(), which collapses
  {ok:true, mirrored:false} to success, so a run that mirrored nothing
  reported done with the latch only on the erasable store. It consumes
  writeConfigWithResult and retries until both stores carry it.

Bucketing integrity
- Storage-restricted Studio profiles all bucketed on the literal
  "anonymous": computed against the shipped hash, 100% of them were
  enrolled in calibration-50 rather than 50%, and they merged into one
  PostHog person. Per-session random id instead — persists nothing.
- bucketSeed had read/write authority backwards: install-state is
  write-once authoritative, but readConfig took config.json's blindly, so
  the stores could hold different seeds until a re-mint flipped every
  cohort. Merged on read, like the latch.
- An unwritable ~/.hyperframes with no config.json re-minted per call,
  re-rolling the seed on every command, and the "cohorts will not be
  stable" warning was unreachable on that path.
- A corrupt PRE-MOVE state file was never deleted, so a machine reset
  with `rm -rf ~/.hyperframes` reported predecessorFound/stateFileCorrupt
  forever — poisoning the exact metric this work exists to produce.

Opt-out honoring
- CLI canary decisions memoized per process, so `hyperframes telemetry
  disable` during a running preview server was ignored for hours while
  the server kept serving pre-opt-out decisions. The memo is keyed on the
  telemetry posture.
- shouldTrack() memoized, contradicting policy.ts's documented "not
  memoized" contract that policy.test.ts asserts.
- The Studio override path resolved the bucket unit eagerly as an
  argument, minting and PERSISTING a tracking id for an opted-out profile
  — a value evaluateCanary discards unread.
- Storage reads could throw out of telemetry into a post-commit catch
  block, reporting an already-committed edit as failed.
- readConfig printed an unsilenceable stderr warning on every invocation
  for installs that opted out of telemetry entirely.

Host split
- isLoopbackHost rejected 0.0.0.0, so the documented
  HYPERFRAMES_PREVIEW_HOST LAN mode silently lost CLI→Studio identity
  stitching and split one user across two PostHog persons. Identity is
  now allowed when the operator explicitly opted into LAN binding.
- Corrected the comment claiming the guard refuses spoofed Hosts: a
  non-browser client sets Host freely. It is a browser DNS-rebinding
  mitigation, not access control, and now says so.

Semantics and test hygiene
- percentage:100 did not mean everyone — exclude and no_unit_id sat above
  the fast path, so the registry's "delete the entry at 100" step was an
  unstaged flip for CI and seedless installs.
- CLI cohort adoption returned before evaluateCanary, dropping Studio's
  own webdriver exclusion.
- overdueCanaries() was asserted against wall-clock time, so the whole
  core suite would go red on 2026-09-15 for every unrelated PR; and `>`
  against midnight made a canary overdue ON its sunset date.
- Statistical assertions ran on unseeded randomUUID() populations tight
  enough to fail ~1 run in 200. Seeded.

Also: broke a config -> policy -> transport -> config import cycle by
moving POSTHOG_API_KEY to a leaf module.

Tests: 2347 CLI (bundle absent), 3153 Studio, 1450 core. Fault injection
covers the latch, seed authority, LAN identity, webdriver exclusion and
the anonymous-bucket fix. Two pre-existing tests asserted behaviour these
findings identify as wrong (shouldTrack memoization, 100%-excludes-CI)
and were rewritten with the reasoning stated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 01:27:37 -07:00

83 lines
3.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
async function loadPolicy(options?: { devMode?: boolean; apiKey?: string }) {
vi.resetModules();
vi.doMock("../utils/env.js", () => ({
isDevMode: () => options?.devMode ?? false,
}));
// The key moved to a leaf module to break a config -> policy -> transport
// -> config import cycle; policy.ts reads it from there now.
vi.doMock("./posthogKey.js", () => ({
POSTHOG_API_KEY: options?.apiKey ?? "phc_test",
}));
return import("./policy.js");
}
describe("telemetry policy", () => {
afterEach(() => {
vi.doUnmock("../utils/env.js");
vi.doUnmock("./transport.js");
vi.resetModules();
delete process.env["HYPERFRAMES_NO_TELEMETRY"];
delete process.env["DO_NOT_TRACK"];
});
it.each(["1", "true", "TRUE", " yes ", "on"])(
"treats %j as an explicit environment opt-out",
async (value) => {
process.env["HYPERFRAMES_NO_TELEMETRY"] = value;
const { effectiveTelemetryStatus } = await loadPolicy();
expect(effectiveTelemetryStatus(true).source).toBe("HYPERFRAMES_NO_TELEMETRY");
},
);
it.each(["", "0", "false", "no", "off", "anything"])(
"does not treat %j as an affirmative opt-out value",
async (value) => {
process.env["HYPERFRAMES_NO_TELEMETRY"] = value;
const { effectiveTelemetryStatus } = await loadPolicy();
expect(effectiveTelemetryStatus(true)).toEqual({ enabled: true, source: "config" });
},
);
it("reports HYPERFRAMES_NO_TELEMETRY as the effective source", async () => {
process.env["HYPERFRAMES_NO_TELEMETRY"] = "true";
const { effectiveTelemetryStatus } = await loadPolicy();
expect(effectiveTelemetryStatus(true)).toEqual({
enabled: false,
source: "HYPERFRAMES_NO_TELEMETRY",
});
});
it("reports DO_NOT_TRACK as the effective source", async () => {
process.env["DO_NOT_TRACK"] = "yes";
const { effectiveTelemetryStatus } = await loadPolicy();
expect(effectiveTelemetryStatus(true)).toEqual({
enabled: false,
source: "DO_NOT_TRACK",
});
});
it("reports dev mode instead of claiming telemetry is active", async () => {
const { effectiveTelemetryStatus } = await loadPolicy({ devMode: true });
expect(effectiveTelemetryStatus(true)).toEqual({
enabled: false,
source: "dev_mode",
});
});
it("reports a telemetry-disabled build instead of claiming telemetry is active", async () => {
const { effectiveTelemetryStatus } = await loadPolicy({ apiKey: "disabled" });
expect(effectiveTelemetryStatus(true)).toEqual({
enabled: false,
source: "telemetry_disabled_build",
});
});
it("falls back to the persisted preference when no runtime override applies", async () => {
const { effectiveTelemetryStatus } = await loadPolicy();
expect(effectiveTelemetryStatus(false)).toEqual({ enabled: false, source: "config" });
expect(effectiveTelemetryStatus(true)).toEqual({ enabled: true, source: "config" });
});
});