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
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { canaryBucket, evaluateCanary, parseCanaryOverride, type CanaryInput } from "./canary.js";
|
||||
import { CANARIES, canaryEnvVar, findCanary, overdueCanaries } from "./canaryRegistry.js";
|
||||
import { CANARY_FEATURE_PREFIX, canaryFeatureKey, canaryFeatureProperties } from "./canary.js";
|
||||
|
||||
const base = (over: Partial<CanaryInput> = {}): CanaryInput => ({
|
||||
feature: "test-feature",
|
||||
@@ -249,3 +250,36 @@ describe("registry", () => {
|
||||
expect(overdueCanaries()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PostHog flag-shaped properties", () => {
|
||||
it("namespaces keys so a canary can never alias a real PostHog flag", () => {
|
||||
// A real flag namespace already exists in this project, owned by the web
|
||||
// app (e.g. `enable-chat-tab`). Without the `canary-` infix a canary named
|
||||
// after a real flag would fight it for the same property.
|
||||
expect(canaryFeatureKey("de-parallel-router")).toBe("$feature/canary-de-parallel-router");
|
||||
expect(CANARY_FEATURE_PREFIX.startsWith("$feature/")).toBe(true);
|
||||
});
|
||||
|
||||
it("emits every canary, not just enrolled ones", () => {
|
||||
// Absent vs "false" are different facts: absent = this build predates the
|
||||
// canary, "false" = this build has it and this install is control.
|
||||
// Collapsing them makes a ramp unreadable.
|
||||
const props = canaryFeatureProperties([
|
||||
{ name: "a", enabled: true },
|
||||
{ name: "b", enabled: false },
|
||||
]);
|
||||
expect(props).toEqual({
|
||||
"$feature/canary-a": "true",
|
||||
"$feature/canary-b": "false",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses string values, matching how PostHog records boolean flags", () => {
|
||||
const props = canaryFeatureProperties([{ name: "a", enabled: true }]);
|
||||
expect(typeof props["$feature/canary-a"]).toBe("string");
|
||||
});
|
||||
|
||||
it("is empty when nothing is registered", () => {
|
||||
expect(canaryFeatureProperties([])).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user