mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
Review caught that the same anti-pattern was still live in the Studio binding: canaryEventProperties destructured only `enabled` and dropped the reason. Its own doc comment promised 'identical shape to the CLI, so a rollout spanning both reads as one flag' — which the CLI-only fix had just made false. This matters beyond symmetry. A CLI-launched Studio adopts the CLI's decisions and shares its bucket seed, so a cohort flip can surface on either surface. Emitting attribution on only one leaves Studio-observed flips unattributable and makes the two flip counts irreconcilable — and Studio is the surface most likely to expose a shared-seed-with-diverging-id pattern, which is the open question the reason exists to answer. Also adds the no_unit_id emission test the CLI side advertised but never asserted, and a Studio pair pinning that a URL override and a cohort roll produce the same assignment with different reasons. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
285 lines
10 KiB
TypeScript
285 lines
10 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
const configState: {
|
|
anonymousId: string;
|
|
bucketSeed: string | undefined;
|
|
telemetryEnabled: boolean;
|
|
} = {
|
|
anonymousId: "db0c1f4a-b95e-4c35-90c6-1a15bd76f717",
|
|
bucketSeed: undefined,
|
|
telemetryEnabled: true,
|
|
};
|
|
const systemState = { is_ci: false };
|
|
// null = no runtime opt-out in force; a string names the source (env var,
|
|
// dev build, ...) exactly as policy.ts reports it.
|
|
const policyState: { runtimeOverride: string | null } = { runtimeOverride: null };
|
|
|
|
vi.mock("./config.js", () => ({
|
|
readConfig: () => ({
|
|
anonymousId: configState.anonymousId,
|
|
bucketSeed: configState.bucketSeed,
|
|
telemetryEnabled: configState.telemetryEnabled,
|
|
}),
|
|
}));
|
|
vi.mock("./system.js", () => ({
|
|
getSystemMeta: () => ({ is_ci: systemState.is_ci }),
|
|
}));
|
|
vi.mock("./policy.js", () => ({
|
|
telemetryRuntimeOverride: () => policyState.runtimeOverride,
|
|
}));
|
|
|
|
// The registry is data; pin a known shape so these tests don't move when a
|
|
// real canary is added or ramped.
|
|
vi.mock("@hyperframes/core/canary-registry", async () => {
|
|
const actual = await vi.importActual<typeof import("@hyperframes/core/canary-registry")>(
|
|
"@hyperframes/core/canary-registry",
|
|
);
|
|
return {
|
|
...actual,
|
|
CANARIES: [
|
|
{
|
|
name: "test-alpha",
|
|
percentage: 100,
|
|
description: "always on",
|
|
owner: "t",
|
|
sunsetAfter: "2099-01-01",
|
|
},
|
|
{
|
|
// A PARTIAL percentage. `exclude` and the unit-id check only apply
|
|
// below 100 — at 100 the guard is about to be deleted, so everyone
|
|
// must already be on it — so exclusion behaviour cannot be expressed
|
|
// against test-alpha.
|
|
name: "test-gamma",
|
|
percentage: 50,
|
|
description: "partial rollout",
|
|
owner: "t",
|
|
sunsetAfter: "2099-01-01",
|
|
},
|
|
{
|
|
name: "test-beta",
|
|
percentage: 0,
|
|
description: "always off",
|
|
owner: "t",
|
|
sunsetAfter: "2099-01-01",
|
|
},
|
|
],
|
|
findCanary: (n: string) =>
|
|
[
|
|
{
|
|
name: "test-alpha",
|
|
percentage: 100,
|
|
description: "",
|
|
owner: "t",
|
|
sunsetAfter: "2099-01-01",
|
|
},
|
|
{
|
|
name: "test-gamma",
|
|
percentage: 50,
|
|
description: "",
|
|
owner: "t",
|
|
sunsetAfter: "2099-01-01",
|
|
},
|
|
{
|
|
name: "test-beta",
|
|
percentage: 0,
|
|
description: "",
|
|
owner: "t",
|
|
sunsetAfter: "2099-01-01",
|
|
},
|
|
].find((c) => c.name === n),
|
|
};
|
|
});
|
|
|
|
const { isCanaryEnabled, resolveCanary, canaryEventProperties, __resetCanaryCacheForTests } =
|
|
await import("./canary.js");
|
|
|
|
beforeEach(() => {
|
|
__resetCanaryCacheForTests();
|
|
configState.anonymousId = "db0c1f4a-b95e-4c35-90c6-1a15bd76f717";
|
|
configState.bucketSeed = undefined;
|
|
configState.telemetryEnabled = true;
|
|
systemState.is_ci = false;
|
|
policyState.runtimeOverride = null;
|
|
delete process.env.HF_CANARY_TEST_ALPHA;
|
|
delete process.env.HF_CANARY_TEST_BETA;
|
|
delete process.env.HF_CANARY_TEST_GAMMA;
|
|
});
|
|
|
|
describe("telemetry opt-out is canary opt-out", () => {
|
|
it("does not enrol when the persisted preference is off", () => {
|
|
configState.telemetryEnabled = false;
|
|
// test-alpha is at 100% — it would be on for everyone otherwise.
|
|
expect(resolveCanary("test-alpha")).toEqual({
|
|
enabled: false,
|
|
reason: "telemetry_opt_out",
|
|
});
|
|
});
|
|
|
|
it.each(["HYPERFRAMES_NO_TELEMETRY", "DO_NOT_TRACK", "dev_mode"])(
|
|
"does not enrol under the %s runtime override, even with the preference on",
|
|
(source) => {
|
|
configState.telemetryEnabled = true;
|
|
policyState.runtimeOverride = source;
|
|
expect(resolveCanary("test-alpha").reason).toBe("telemetry_opt_out");
|
|
},
|
|
);
|
|
|
|
it("never buckets an opted-out install — no cohort is assigned at all", () => {
|
|
configState.telemetryEnabled = false;
|
|
// A bucket number would mean we hashed them into a slice anyway.
|
|
expect(resolveCanary("test-alpha").bucket).toBeUndefined();
|
|
});
|
|
|
|
it("still honours an explicit override — the documented way to test with telemetry off", () => {
|
|
configState.telemetryEnabled = false;
|
|
process.env.HF_CANARY_TEST_BETA = "on";
|
|
expect(resolveCanary("test-beta")).toEqual({ enabled: true, reason: "forced_on" });
|
|
});
|
|
|
|
it("reports every canary as false to PostHog shape when opted out", () => {
|
|
configState.telemetryEnabled = false;
|
|
expect(canaryEventProperties()).toEqual({
|
|
"$feature/canary-test-alpha": "false",
|
|
"$feature/canary-test-gamma": "false",
|
|
"$feature/canary-test-beta": "false",
|
|
canary_reason_test_alpha: "telemetry_opt_out",
|
|
canary_reason_test_gamma: "telemetry_opt_out",
|
|
canary_reason_test_beta: "telemetry_opt_out",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("bucketing unit", () => {
|
|
it("buckets on the bucketSeed when present — the unit that survives config wipes", async () => {
|
|
const { evaluateCanary } = await import("@hyperframes/core/canary");
|
|
configState.bucketSeed = "5f1c9d2e-0000-4000-8000-aaaaaaaaaaaa";
|
|
const viaBinding = resolveCanary("test-gamma").bucket;
|
|
// 50, not 100: at 100 evaluateCanary short-circuits before bucketing and
|
|
// reports no bucket at all, which would make this comparison vacuous.
|
|
const bySeed = evaluateCanary({
|
|
feature: "test-gamma",
|
|
unitId: configState.bucketSeed,
|
|
percentage: 50,
|
|
}).bucket;
|
|
const byId = evaluateCanary({
|
|
feature: "test-gamma",
|
|
unitId: configState.anonymousId,
|
|
percentage: 50,
|
|
}).bucket;
|
|
expect(viaBinding).toBe(bySeed);
|
|
// Only meaningful if the two units actually bucket differently.
|
|
expect(bySeed).not.toBe(byId);
|
|
});
|
|
|
|
it("falls back to the anonymousId when no seed exists (failed legacy backfill)", async () => {
|
|
const { evaluateCanary } = await import("@hyperframes/core/canary");
|
|
const viaBinding = resolveCanary("test-gamma").bucket;
|
|
const byId = evaluateCanary({
|
|
feature: "test-gamma",
|
|
unitId: configState.anonymousId,
|
|
percentage: 50,
|
|
}).bucket;
|
|
expect(viaBinding).toBe(byId);
|
|
// Both undefined would satisfy toBe — assert a bucket was actually computed.
|
|
expect(viaBinding).toEqual(expect.any(Number));
|
|
});
|
|
});
|
|
|
|
describe("CLI canary binding", () => {
|
|
it("reads the percentage from the registry", () => {
|
|
expect(isCanaryEnabled("test-alpha")).toBe(true);
|
|
expect(isCanaryEnabled("test-beta")).toBe(false);
|
|
});
|
|
|
|
it("an unregistered name is off, not a throw — a typo must not break a render", () => {
|
|
expect(isCanaryEnabled("does-not-exist")).toBe(false);
|
|
expect(resolveCanary("does-not-exist").reason).toBe("out_of_cohort");
|
|
});
|
|
|
|
it("HF_CANARY_<FEATURE> overrides the registry in both directions", () => {
|
|
process.env.HF_CANARY_TEST_ALPHA = "off";
|
|
process.env.HF_CANARY_TEST_BETA = "on";
|
|
expect(resolveCanary("test-alpha")).toMatchObject({ enabled: false, reason: "forced_off" });
|
|
expect(resolveCanary("test-beta")).toMatchObject({ enabled: true, reason: "forced_on" });
|
|
});
|
|
|
|
it("excludes CI from percentage enrolment, but an override still reaches it", () => {
|
|
systemState.is_ci = true;
|
|
expect(resolveCanary("test-gamma")).toMatchObject({ enabled: false, reason: "excluded" });
|
|
|
|
__resetCanaryCacheForTests();
|
|
process.env.HF_CANARY_TEST_GAMMA = "on";
|
|
expect(resolveCanary("test-gamma")).toMatchObject({ enabled: true, reason: "forced_on" });
|
|
});
|
|
|
|
it("fails closed when the install has no anonymousId", () => {
|
|
configState.anonymousId = "";
|
|
expect(resolveCanary("test-gamma")).toMatchObject({ enabled: false, reason: "no_unit_id" });
|
|
});
|
|
|
|
it("memoizes so a decision cannot change mid-process", () => {
|
|
expect(isCanaryEnabled("test-beta")).toBe(false);
|
|
// A late env change must NOT flip a render that already started.
|
|
process.env.HF_CANARY_TEST_BETA = "on";
|
|
expect(isCanaryEnabled("test-beta")).toBe(false);
|
|
__resetCanaryCacheForTests();
|
|
expect(isCanaryEnabled("test-beta")).toBe(true);
|
|
});
|
|
|
|
it("emits PostHog flag-shaped properties for every registered canary", () => {
|
|
expect(canaryEventProperties()).toEqual({
|
|
"$feature/canary-test-alpha": "true",
|
|
"$feature/canary-test-gamma": expect.stringMatching(/^(true|false)$/),
|
|
"$feature/canary-test-beta": "false",
|
|
canary_reason_test_alpha: "in_cohort",
|
|
canary_reason_test_gamma: expect.stringMatching(/^(in_cohort|out_of_cohort)$/),
|
|
canary_reason_test_beta: "out_of_cohort",
|
|
});
|
|
|
|
__resetCanaryCacheForTests();
|
|
process.env.HF_CANARY_TEST_ALPHA = "off";
|
|
expect(canaryEventProperties()["$feature/canary-test-alpha"]).toBe("false");
|
|
});
|
|
});
|
|
|
|
// End of the wire: the CLI binding computes the reason and used to drop it.
|
|
describe("reason reaches the event properties", () => {
|
|
it("pairs every canary's assignment with its reason", () => {
|
|
const props = canaryEventProperties();
|
|
expect(props["$feature/canary-test-alpha"]).toBe("true");
|
|
expect(props["canary_reason_test_alpha"]).toBe("in_cohort");
|
|
expect(props["canary_reason_test_beta"]).toBe("out_of_cohort");
|
|
});
|
|
|
|
it("reports an explicit override as forced, not as a cohort roll", () => {
|
|
process.env.HF_CANARY_TEST_BETA = "on";
|
|
const props = canaryEventProperties();
|
|
expect(props["$feature/canary-test-beta"]).toBe("true");
|
|
// Same assignment a real enrolment would produce — the reason is the only
|
|
// thing that distinguishes them, which is the point.
|
|
expect(props["canary_reason_test_beta"]).toBe("forced_on");
|
|
});
|
|
|
|
it("reports CI as excluded rather than out_of_cohort", () => {
|
|
systemState.is_ci = true;
|
|
// Both are enabled:false, but `excluded` was never bucketed. Conflating
|
|
// them is what biased the first fleet accuracy read low.
|
|
expect(canaryEventProperties()["canary_reason_test_gamma"]).toBe("excluded");
|
|
});
|
|
|
|
// The PR body advertises no_unit_id as one of two values that pay for
|
|
// themselves; the resolver exercises it but the emission layer did not.
|
|
it("reports no_unit_id at the emission layer when the install has no id", () => {
|
|
configState.anonymousId = "";
|
|
configState.bucketSeed = undefined;
|
|
expect(canaryEventProperties()["canary_reason_test_gamma"]).toBe("no_unit_id");
|
|
});
|
|
|
|
it("reports telemetry_opt_out when the preference is off", () => {
|
|
configState.telemetryEnabled = false;
|
|
// Emitted for completeness; by construction such an install sends nothing,
|
|
// so this value should never actually be observed in the warehouse.
|
|
expect(canaryEventProperties()["canary_reason_test_alpha"]).toBe("telemetry_opt_out");
|
|
});
|
|
});
|