Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ it can never ship a flag that silently resolves to `undefined`.
> **Every `example-*` key in this document is illustrative, not a real flag.**
> Grep for one and you will find only this file. The registry holds exactly the
> flags that gate live behaviour — today four rollout gates, all plain booleans
> (`insights-visual` also runs a 50/50 experiment):
> (`retention-policies` and `insights-visual` default on and are kill switches;
> `insights-visual` ran a 50/50 experiment first):
> `retention-policies`, `lore-explorer-instruments`,
> `lore-explorer-duplicate-clusters` and `insights-visual`. It has also held entries since removed: the
> rollout gate for the `/insights` route (deleted once that page became
Expand Down Expand Up @@ -260,7 +261,8 @@ nothing is the correct, expected symptom of that gap, not a broken query.
Per-user splits need no storage: a signed-in user is bucketed by their Supabase
user id (see [Targeting key resolution](#targeting-key-resolution)), so they see
the same arm on every device for as long as the weights stand still.
`insights-visual` runs this way and is the worked example.
`insights-visual` ran this way (50/50, before it was turned on for every user)
and is the worked example.

### 1. Declare the arms, dark

Expand Down
23 changes: 4 additions & 19 deletions packages/feature-flags/generated/flags.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,35 +57,20 @@
},
{
"key": "insights-visual",
"description": "Insights — the visual first version of `/insights`: ONE page-wide window, the Learn → Recall → Promote → Prune loop strip, the Pulse timeline (lessons written and records read on a shared time axis, chosen vs bulk), the recall flow (client → tool → scope type), and runs drawn as a weekday × hour rhythm plus swimlanes — with the existing 'earning its place' grid, runs list and operational health kept below. Gates the whole page body through the `InsightsView` resolver; with it off `/insights` renders exactly as before. Reads only routes that already ship (`/memories/activity`, `/read-activity`, `/usage`, `/usage/runs`, `/utility`), so there is no backend prerequisite and flipping it is a pure UI decision. RUNNING as a 50/50 experiment: each signed-in user is assigned an arm by a hash of their user id (the same arm on every device), a developer override still wins, and setting `enabled: false` sends everyone back to `off`. `on` is listed first so ramping its weight up never moves a user from `on` back to `off`; lowering it or reordering the arms reassigns users mid-experiment. Exposure is measured by `feature_flag.exposed`, emitted by the `InsightsView` resolver — see docs/feature-flags.md § Running an experiment.",
"description": "Insights — the visual first version of `/insights`: ONE page-wide window, the Learn → Recall → Promote → Prune loop strip, the Pulse timeline (lessons written and records read on a shared time axis, chosen vs bulk), the recall flow (client → tool → scope type), and runs drawn as a weekday × hour rhythm plus swimlanes — with the existing 'earning its place' grid, runs list and operational health kept below. Gates the whole page body through the `InsightsView` resolver; with it off `/insights` renders exactly as before. Reads only routes that already ship (`/memories/activity`, `/read-activity`, `/usage`, `/usage/runs`, `/utility`), so there is no backend prerequisite and flipping it is a pure UI decision. DEFAULTS ON for every user: it ran as a 50/50 experiment (users bucketed by a hash of their user id, exposure measured by `feature_flag.exposed` from the `InsightsView` resolver — see docs/feature-flags.md § Running an experiment), and that split has ended. The flag is now a kill switch for turning the visual page back OFF, not a rollout gate; a developer override still wins. Retiring it is the copy-and-suffix removal: promote `InsightsView.on.tsx`, delete `InsightsView.off.tsx` and the `useFeatureFlagExposure` call, drop this entry.",
"type": "boolean",
"variants": {
"off": false,
"on": true
},
"defaultVariant": "off",
"experiment": {
"enabled": true,
"variants": [
{
"key": "on",
"description": "The visual page",
"weight": 50
},
{
"key": "off",
"description": "The original page (control)",
"weight": 50
}
]
},
"defaultVariant": "on",
"experiment": null,
"owner": "@lorekit/web",
"tags": [
"dashboard",
"web",
"insights",
"rollout",
"experiment"
"rollout"
]
}
]
Expand Down
13 changes: 3 additions & 10 deletions packages/feature-flags/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,12 @@ const REGISTRY_INPUT = [
{
key: 'insights-visual',
description:
"Insights — the visual first version of `/insights`: ONE page-wide window, the Learn → Recall → Promote → Prune loop strip, the Pulse timeline (lessons written and records read on a shared time axis, chosen vs bulk), the recall flow (client → tool → scope type), and runs drawn as a weekday × hour rhythm plus swimlanes — with the existing 'earning its place' grid, runs list and operational health kept below. Gates the whole page body through the `InsightsView` resolver; with it off `/insights` renders exactly as before. Reads only routes that already ship (`/memories/activity`, `/read-activity`, `/usage`, `/usage/runs`, `/utility`), so there is no backend prerequisite and flipping it is a pure UI decision. RUNNING as a 50/50 experiment: each signed-in user is assigned an arm by a hash of their user id (the same arm on every device), a developer override still wins, and setting `enabled: false` sends everyone back to `off`. `on` is listed first so ramping its weight up never moves a user from `on` back to `off`; lowering it or reordering the arms reassigns users mid-experiment. Exposure is measured by `feature_flag.exposed`, emitted by the `InsightsView` resolver — see docs/feature-flags.md § Running an experiment.",
"Insights — the visual first version of `/insights`: ONE page-wide window, the Learn → Recall → Promote → Prune loop strip, the Pulse timeline (lessons written and records read on a shared time axis, chosen vs bulk), the recall flow (client → tool → scope type), and runs drawn as a weekday × hour rhythm plus swimlanes — with the existing 'earning its place' grid, runs list and operational health kept below. Gates the whole page body through the `InsightsView` resolver; with it off `/insights` renders exactly as before. Reads only routes that already ship (`/memories/activity`, `/read-activity`, `/usage`, `/usage/runs`, `/utility`), so there is no backend prerequisite and flipping it is a pure UI decision. DEFAULTS ON for every user: it ran as a 50/50 experiment (users bucketed by a hash of their user id, exposure measured by `feature_flag.exposed` from the `InsightsView` resolver — see docs/feature-flags.md § Running an experiment), and that split has ended. The flag is now a kill switch for turning the visual page back OFF, not a rollout gate; a developer override still wins. Retiring it is the copy-and-suffix removal: promote `InsightsView.on.tsx`, delete `InsightsView.off.tsx` and the `useFeatureFlagExposure` call, drop this entry.",
type: 'boolean',
variants: { off: false, on: true },
defaultVariant: 'off',
experiment: {
enabled: true,
variants: [
{ key: 'on', weight: 50, description: 'The visual page' },
{ key: 'off', weight: 50, description: 'The original page (control)' },
],
},
defaultVariant: 'on',
owner: '@lorekit/web',
tags: ['dashboard', 'web', 'insights', 'rollout', 'experiment'],
tags: ['dashboard', 'web', 'insights', 'rollout'],
},
] satisfies unknown[];

Expand Down
Loading