mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(core): emit canary assignments as PostHog flag properties
Replaces the single `canaries: "a,b"` telemetry property with PostHog's own
flag shape, one property per registered canary:
$feature/canary-de-parallel-router: "true" | "false"
PostHog treats `$feature/<key>` as a first-class flag property, so breakdowns,
funnels split by cohort and the experiment surfaces work on a canary with
nothing configured server-side. The decision still happens locally: the render
path forbids render-time network calls, behaviour must not depend on analytics
being reachable, and neither the CLI nor Studio ships posthog-js (both
hand-roll a batch POST, so there is no SDK to evaluate a real flag with).
Decide locally, analyse natively.
Two decisions worth recording:
- BOTH ARMS ARE EMITTED. A non-enrolled install reports "false" rather than
omitting the property. Absent means "this build predates the canary", which
is a different fact from "this install is control" — collapsing them makes a
ramp unreadable, because you cannot separate a control group from an old
version.
- KEYS ARE NAMESPACED with a `canary-` infix. A real PostHog flag namespace
already exists in this project, owned by the web app (`enable-chat-tab`, set
by posthog-js from `$lib=web` events). Namespacing guarantees a canary key
can never alias a real flag key and have the two fight over one property.
Values are the strings "true"/"false" to match how PostHog records boolean
flag values, so the property is directly comparable to a real flag.
98 core / 1437, 166 cli / 2194, 269 studio / 2982 green; tsc clean across all
three packages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
71ee156dac
commit
a1682e1228
@@ -32,7 +32,7 @@ vi.mock("@hyperframes/core/canary-registry", async () => {
|
||||
const {
|
||||
isCanaryEnabled,
|
||||
resolveCanary,
|
||||
activeCanaryNames,
|
||||
canaryEventProperties,
|
||||
canaryParamName,
|
||||
__resetStudioCanaryCacheForTests,
|
||||
} = await import("./canary");
|
||||
@@ -164,11 +164,14 @@ describe("cohort identity", () => {
|
||||
});
|
||||
|
||||
describe("telemetry", () => {
|
||||
it("reports enrolled canaries, undefined when none", () => {
|
||||
expect(activeCanaryNames()).toBe("on-everywhere");
|
||||
it("emits the same PostHog flag-shaped properties as the CLI", () => {
|
||||
expect(canaryEventProperties()).toEqual({
|
||||
"$feature/canary-on-everywhere": "true",
|
||||
"$feature/canary-off-everywhere": "false",
|
||||
});
|
||||
|
||||
__resetStudioCanaryCacheForTests();
|
||||
setSearch("?hf_canary_on_everywhere=off");
|
||||
expect(activeCanaryNames()).toBeUndefined();
|
||||
expect(canaryEventProperties()["$feature/canary-on-everywhere"]).toBe("false");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,12 @@
|
||||
// browser bundle, and the barrel re-exports the whole core surface (parsers,
|
||||
// lint, studio-server); pulling that in here drags a Node-oriented dependency
|
||||
// graph into the bundle. These two modules are pure and leaf.
|
||||
import { evaluateCanary, parseCanaryOverride, type CanaryDecision } from "@hyperframes/core/canary";
|
||||
import {
|
||||
canaryFeatureProperties,
|
||||
evaluateCanary,
|
||||
parseCanaryOverride,
|
||||
type CanaryDecision,
|
||||
} from "@hyperframes/core/canary";
|
||||
import { CANARIES, findCanary } from "@hyperframes/core/canary-registry";
|
||||
import { resolveStudioDistinctId } from "./distinctId";
|
||||
import { safeSessionStorage } from "../utils/safeStorage";
|
||||
@@ -142,11 +147,12 @@ export function isCanaryEnabled(name: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Comma-joined names of the canaries this install is enrolled in, or undefined
|
||||
* when none — attached to every Studio event so any metric can be split by
|
||||
* cohort, exactly as the CLI does.
|
||||
* Canary assignments as PostHog flag properties (`$feature/canary-<name>`),
|
||||
* attached to every Studio event so any metric can be split by cohort —
|
||||
* identical shape to the CLI, so a rollout spanning both reads as one flag.
|
||||
*/
|
||||
export function activeCanaryNames(): string | undefined {
|
||||
const active = CANARIES.filter((c) => resolveCanary(c.name).enabled).map((c) => c.name);
|
||||
return active.length > 0 ? active.join(",") : undefined;
|
||||
export function canaryEventProperties(): Record<string, string> {
|
||||
return canaryFeatureProperties(
|
||||
CANARIES.map((c) => ({ name: c.name, enabled: resolveCanary(c.name).enabled })),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { getAnonymousId, hasShownNotice, isOptedOut, markNoticeShown } from "./config";
|
||||
import { getBrowserSystemMeta } from "./system";
|
||||
import { activeCanaryNames } from "./canary";
|
||||
import { canaryEventProperties } from "./canary";
|
||||
|
||||
// Write-only PostHog project key, safe to embed in client code.
|
||||
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
||||
@@ -74,10 +74,10 @@ export function trackEvent(event: string, properties: EventProperties = {}): voi
|
||||
const sys = getBrowserSystemMeta();
|
||||
eventQueue.push({
|
||||
event,
|
||||
// `canaries` mirrors the CLI: the cohorts this install is enrolled in, on
|
||||
// EVERY event so any metric can be split by cohort. Resolved after the
|
||||
// shouldTrack guard, so opted-out users never pay for it.
|
||||
properties: { ...properties, ...sys, canaries: activeCanaryNames() },
|
||||
// Canary assignments as `$feature/canary-<name>`, mirroring the CLI so a
|
||||
// rollout spanning both surfaces reads as one flag in PostHog. Resolved
|
||||
// after the shouldTrack guard, so opted-out users never pay for it.
|
||||
properties: { ...properties, ...sys, ...canaryEventProperties() },
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user