mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
P1 — SPA route bypassed the DNS-rebinding guard. Guarding only
/api/telemetry-identity left the catch-all as an open side door: a
rebound origin could fetch `/` and read __HF_CLI_DISTINCT_ID and
__HF_CLI_BUCKET_SEED straight out of the returned HTML. The SPA response
now applies the same isLoopbackHost() check; an untrusted Host still gets
a working Studio, just with no identity, seed or decisions injected.
Route-level regression added.
P1 — a CLI cohort roll could override Studio's own opt-out.
decideStudioCanary() adopted the injected decision before checking
isOptedOut(), so CLI-telemetry-on plus Studio-opted-out still enrolled
Studio. A bare boolean could not express the difference between a
deliberate override and an ordinary cohort roll, so the injected map now
carries provenance ({ enabled, forced }). Forced wins outright — it is
the documented escalation channel and must behave the same on both
surfaces — while a percentage roll now loses to this profile's opt-out.
Full interaction matrix tested.
P1 — the legacy studio:* path sat outside both contracts.
utils/studioTelemetry.ts shipped its own opt-out key and its own send
loop, so the documented hyperframes-studio:telemetryDisabled did not
silence it and its events carried no cohort assignment. It now honours
both keys (the legacy one stays, so nobody already opted out is quietly
re-enabled) and mixes in canaryEventProperties(), making "every
telemetry event carries the assignment" actually true.
P2 — partial salvage could drop a tripped breaker.
salvageInstallState() discarded the whole record when markerAt and
bucketSeed were both unusable, taking deParallelRouterTrialFired with it
and re-enrolling a machine whose router already failed. All three fields
are now independently salvageable.
Docs: canary-rollouts.mdx said "disabling telemetry disables the
reporting, not the enrolment" — exactly backwards since the opt-out gate
landed. Corrected; checked for other copies, none.
Tests: 13 new (4 opt-out precedence, 4 legacy-path opt-out and canary
props, 3 route-level host guard, 2 breaker salvage). Fault injection:
each of the four fixes reverted independently fails its own tests
(2 CLI + 1 Studio + 2 Studio).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
2.8 KiB
TypeScript
75 lines
2.8 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// The `studio:*` path predates telemetry/config.ts and shipped its own
|
|
// opt-out key and its own send loop, so it sat outside both contracts the
|
|
// canary work established: the documented opt-out did not silence it, and its
|
|
// events carried no cohort assignment. These pin both.
|
|
|
|
const DOCUMENTED_OPT_OUT = "hyperframes-studio:telemetryDisabled";
|
|
const LEGACY_OPT_OUT = "hf-studio-telemetry-opt-out";
|
|
|
|
vi.mock("../telemetry/canary", () => ({
|
|
canaryEventProperties: () => ({ "$feature/canary-test-one": "true" }),
|
|
}));
|
|
|
|
describe("studioTelemetry — shared opt-out and canary properties", () => {
|
|
let trackStudioEvent: typeof import("./studioTelemetry").trackStudioEvent;
|
|
let fetchMock: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(async () => {
|
|
localStorage.clear();
|
|
vi.resetModules();
|
|
vi.useFakeTimers();
|
|
fetchMock = vi.fn(() => Promise.resolve({ ok: true } as Response));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
({ trackStudioEvent } = await import("./studioTelemetry"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
/** Drain the queue and return the events the batch would have sent. */
|
|
async function sentEvents(): Promise<Array<Record<string, unknown>>> {
|
|
await vi.runOnlyPendingTimersAsync();
|
|
if (fetchMock.mock.calls.length === 0) return [];
|
|
const body = fetchMock.mock.calls[0]?.[1] as { body?: string } | undefined;
|
|
const parsed = JSON.parse(body?.body ?? "{}") as { batch?: Array<Record<string, unknown>> };
|
|
return parsed.batch ?? [];
|
|
}
|
|
|
|
it("honours the documented opt-out key", async () => {
|
|
// Previously only the legacy key was checked, so a user who opted out the
|
|
// documented way kept emitting every `studio:*` event.
|
|
localStorage.setItem(DOCUMENTED_OPT_OUT, "1");
|
|
trackStudioEvent("thing_happened");
|
|
expect(await sentEvents()).toHaveLength(0);
|
|
});
|
|
|
|
it("still honours the legacy opt-out key", async () => {
|
|
// Anyone already opted out this way must not be quietly re-enabled.
|
|
localStorage.setItem(LEGACY_OPT_OUT, "1");
|
|
trackStudioEvent("thing_happened");
|
|
expect(await sentEvents()).toHaveLength(0);
|
|
});
|
|
|
|
it("attaches canary assignments to every event", async () => {
|
|
trackStudioEvent("thing_happened");
|
|
const events = await sentEvents();
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0]?.["properties"]).toMatchObject({
|
|
"$feature/canary-test-one": "true",
|
|
});
|
|
});
|
|
|
|
it("lets an explicit property win over the canary mixin", async () => {
|
|
trackStudioEvent("thing_happened", { "$feature/canary-test-one": "false" });
|
|
const events = await sentEvents();
|
|
expect(events[0]?.["properties"]).toMatchObject({
|
|
"$feature/canary-test-one": "false",
|
|
});
|
|
});
|
|
});
|