mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
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>
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
// `shouldTrack()` reads module-level constants evaluated at module load time,
|
|
// so changing env after import has no effect. Each test resets module cache.
|
|
|
|
const OPT_OUT_KEY = "hyperframes-studio:telemetryDisabled";
|
|
|
|
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);
|
|
setNoTelemetry(undefined);
|
|
localStorage.clear();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("returns true when not in dev mode and no opt-outs", async () => {
|
|
const shouldTrack = await loadShouldTrack();
|
|
expect(shouldTrack()).toBe(true);
|
|
});
|
|
|
|
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.each(["true", "TRUE", " yes ", "on"])(
|
|
"returns false when VITE_HYPERFRAMES_NO_TELEMETRY=%j",
|
|
async (value) => {
|
|
setNoTelemetry(value);
|
|
const shouldTrack = await loadShouldTrack();
|
|
expect(shouldTrack()).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("does not opt out for an explicit false value", async () => {
|
|
setNoTelemetry("false");
|
|
const shouldTrack = await loadShouldTrack();
|
|
expect(shouldTrack()).toBe(true);
|
|
});
|
|
|
|
it("returns false in vite dev mode", async () => {
|
|
setDev(true);
|
|
const shouldTrack = await loadShouldTrack();
|
|
expect(shouldTrack()).toBe(false);
|
|
});
|
|
|
|
// Previously asserted the opposite. That memoization WAS the bug: policy.ts
|
|
// is explicit that transports re-ask, and policy.test.ts asserts a
|
|
// mid-session opt-out takes effect at once — but this transport cached on
|
|
// first call, so a user who opted out in DevTools after one event kept
|
|
// sending `studio_*` and render events while `studio:*` correctly stopped.
|
|
it("re-reads the policy, so a mid-session opt-out takes effect immediately", async () => {
|
|
const shouldTrack = await loadShouldTrack();
|
|
expect(shouldTrack()).toBe(true);
|
|
localStorage.setItem(OPT_OUT_KEY, "1");
|
|
expect(shouldTrack()).toBe(false);
|
|
});
|
|
});
|