feat(studio): browser canary binding + leaf subpath imports

Adds the Studio (browser) binding so a canary can span the CLI and the editor,
and fixes a bundling mistake the studio test suite caught.

## The binding

Same public API as the CLI — `isCanaryEnabled("name")` — so a call site reads
identically whether it runs in Node or the browser. Three inputs differ:

- UNIT ID: `resolveStudioDistinctId()`, which already adopts
  `window.__HF_CLI_DISTINCT_ID` when the CLI launched Studio. A CLI-launched
  Studio therefore lands in the SAME cohort as the CLI: a rollout spanning
  render and editor is coherent for that user instead of enrolling their
  terminal but not their editor. A test pins that the id is passed through
  unmodified — prefixing or re-hashing it would silently break that parity.

- OVERRIDE: no `process.env` in a page, so `?hf_canary_<name>=on` mirrored
  into sessionStorage. Session scope is deliberate. A URL is the right carrier
  (shareable — "support: open this link"), but persisting a URL-borne override
  to localStorage would let one click silently pin a browser into a cohort
  forever, long after anyone remembers why. Closing the tab is the reset;
  `=reset` clears it explicitly.

- EXCLUSION: `navigator.webdriver` stands in for the CLI's `is_ci`. Automated
  browsers mint a fresh localStorage id per run, so they would hop cohorts
  between runs — noise in the signal, nothing learned about real users. An
  override still reaches them, which is how you test a canary under Playwright.

Studio's `trackEvent` now attaches `canaries` to every event, mirroring the CLI.

## The bundling fix

Importing the `@hyperframes/core` barrel into studio browser code broke two
unrelated hook test files with an esbuild TextEncoder invariant violation. The
barrel re-exports the whole core surface (parsers, lint, studio-server), so it
drags a Node-oriented dependency graph into a browser bundle — the test
failure was the symptom, the bundle bloat was the bug.

`@hyperframes/core` now exposes `./canary` and `./canary-registry`, declared in
packages/core/package-subpaths.json (the generated source of truth for exports —
hand-editing package.json is reverted by the sync script) and marked
`environments: [browser, bun, node]`. Both the studio AND cli bindings import
the leaf modules; the CLI gets the same benefit for a different reason, since
this resolves on the startup path — the reason the producer is lazily loaded.

Verified: the two hook files pass again; 269 studio files / 2982 tests, 98
core / 1433, 166 cli / 2194 green, `bun run lint` clean including the subpath
check. Fault-injection confirms both design decisions are pinned — swapping
session for local storage fails the scope test, prefixing the unit id fails the
CLI/Studio cohort-parity test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-30 15:14:35 -07:00
co-authored by Claude Opus 5
parent df1521a0b6
commit 71ee156dac
7 changed files with 372 additions and 11 deletions
@@ -0,0 +1,174 @@
// @vitest-environment happy-dom
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { evaluateCanary } from "@hyperframes/core/canary";
// Pin the registry: real entries move as rollouts ramp, and these tests are
// about the BINDING (does the browser supply the right three inputs?), not
// about whichever canaries happen to be live today.
vi.mock("@hyperframes/core/canary-registry", async () => {
const actual = await vi.importActual<typeof import("@hyperframes/core/canary-registry")>(
"@hyperframes/core/canary-registry",
);
const defs = [
{
name: "on-everywhere",
percentage: 100,
description: "",
owner: "t",
sunsetAfter: "2099-01-01",
},
{
name: "off-everywhere",
percentage: 0,
description: "",
owner: "t",
sunsetAfter: "2099-01-01",
},
];
return { ...actual, CANARIES: defs, findCanary: (n: string) => defs.find((d) => d.name === n) };
});
const {
isCanaryEnabled,
resolveCanary,
activeCanaryNames,
canaryParamName,
__resetStudioCanaryCacheForTests,
} = await import("./canary");
const { resolveStudioDistinctId, __resetStudioDistinctIdForTests } = await import("./distinctId");
function setSearch(search: string): void {
window.history.replaceState({}, "", `/${search}`);
}
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
setSearch("");
delete window.__HF_CLI_DISTINCT_ID;
Object.defineProperty(navigator, "webdriver", { value: false, configurable: true });
__resetStudioCanaryCacheForTests();
__resetStudioDistinctIdForTests();
});
afterEach(() => {
setSearch("");
__resetStudioCanaryCacheForTests();
__resetStudioDistinctIdForTests();
});
describe("studio canary binding", () => {
it("reads the percentage from the shared registry", () => {
expect(isCanaryEnabled("on-everywhere")).toBe(true);
expect(isCanaryEnabled("off-everywhere")).toBe(false);
});
it("an unregistered name is off, not a throw — a typo must not break the editor", () => {
expect(isCanaryEnabled("nope")).toBe(false);
expect(resolveCanary("nope").reason).toBe("out_of_cohort");
});
it("derives the query param from the canary name", () => {
expect(canaryParamName("de-parallel-router")).toBe("hf_canary_de_parallel_router");
});
});
describe("URL override", () => {
it("turns a canary on and off from the query string", () => {
setSearch("?hf_canary_off_everywhere=on");
expect(resolveCanary("off-everywhere")).toMatchObject({ enabled: true, reason: "forced_on" });
__resetStudioCanaryCacheForTests();
setSearch("?hf_canary_on_everywhere=off");
expect(resolveCanary("on-everywhere")).toMatchObject({ enabled: false, reason: "forced_off" });
});
it("survives losing the query string, so in-app navigation keeps the override", () => {
setSearch("?hf_canary_off_everywhere=on");
expect(isCanaryEnabled("off-everywhere")).toBe(true);
// Navigate away from the param — a real SPA drops it constantly.
__resetStudioCanaryCacheForTests();
setSearch("");
expect(isCanaryEnabled("off-everywhere")).toBe(true);
});
it("is session-scoped, not persisted to localStorage", () => {
// A URL-borne override must not silently pin a browser into a cohort
// forever; closing the tab is the reset.
setSearch("?hf_canary_off_everywhere=on");
expect(isCanaryEnabled("off-everywhere")).toBe(true);
expect(JSON.stringify(localStorage).includes("canary")).toBe(false);
expect(sessionStorage.length).toBeGreaterThan(0);
});
it("=reset clears a stored override", () => {
setSearch("?hf_canary_off_everywhere=on");
expect(isCanaryEnabled("off-everywhere")).toBe(true);
__resetStudioCanaryCacheForTests();
setSearch("?hf_canary_off_everywhere=reset");
expect(isCanaryEnabled("off-everywhere")).toBe(false);
__resetStudioCanaryCacheForTests();
setSearch("");
expect(isCanaryEnabled("off-everywhere")).toBe(false);
});
});
describe("automated browsers", () => {
it("are excluded from percentage enrolment", () => {
Object.defineProperty(navigator, "webdriver", { value: true, configurable: true });
expect(resolveCanary("on-everywhere")).toMatchObject({ enabled: false, reason: "excluded" });
});
it("still honour an explicit override, so a canary can be tested under automation", () => {
Object.defineProperty(navigator, "webdriver", { value: true, configurable: true });
setSearch("?hf_canary_on_everywhere=on");
expect(resolveCanary("on-everywhere")).toMatchObject({ enabled: true, reason: "forced_on" });
});
});
describe("cohort identity", () => {
it("buckets on the Studio distinct id unmodified — so a CLI-launched Studio shares the CLI's cohort", () => {
// distinctId.ts adopts window.__HF_CLI_DISTINCT_ID when the CLI launched
// Studio. This asserts the binding passes that id through untouched: if it
// prefixed or re-hashed it, the editor would land in a different cohort
// than the terminal for the same user, and a rollout spanning both would
// be incoherent.
const cliId = "db0c1f4a-b95e-4c35-90c6-1a15bd76f717";
window.__HF_CLI_DISTINCT_ID = cliId;
__resetStudioDistinctIdForTests();
__resetStudioCanaryCacheForTests();
expect(resolveStudioDistinctId()).toBe(cliId);
// 50% so the answer is id-dependent rather than trivially true.
const viaBinding = resolveCanary("on-everywhere").bucket;
const direct = evaluateCanary({
feature: "on-everywhere",
unitId: cliId,
percentage: 100,
}).bucket;
expect(viaBinding).toBe(direct);
});
it("memoizes so a decision cannot change mid-session", () => {
expect(isCanaryEnabled("off-everywhere")).toBe(false);
// A late override must NOT flip a component that already rendered.
setSearch("?hf_canary_off_everywhere=on");
expect(isCanaryEnabled("off-everywhere")).toBe(false);
__resetStudioCanaryCacheForTests();
expect(isCanaryEnabled("off-everywhere")).toBe(true);
});
});
describe("telemetry", () => {
it("reports enrolled canaries, undefined when none", () => {
expect(activeCanaryNames()).toBe("on-everywhere");
__resetStudioCanaryCacheForTests();
setSearch("?hf_canary_on_everywhere=off");
expect(activeCanaryNames()).toBeUndefined();
});
});