From d035cde9c8d6bcdb4a5e27f6a95a5d539db12ead Mon Sep 17 00:00:00 2001 From: "mads.thines" Date: Mon, 9 Nov 2026 10:58:25 +0000 Subject: [PATCH 1/2] feat(feature-flags): turn insights-visual on for all users Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> --- docs/feature-flags.md | 6 ++++-- packages/feature-flags/src/registry.ts | 13 +++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 5fb4acc95..6103cb66b 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -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 @@ -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 diff --git a/packages/feature-flags/src/registry.ts b/packages/feature-flags/src/registry.ts index 6b0c0e755..6a7d0c78f 100644 --- a/packages/feature-flags/src/registry.ts +++ b/packages/feature-flags/src/registry.ts @@ -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[]; From 8203cd84e517d9d4787b592e047553f05ca3cffd Mon Sep 17 00:00:00 2001 From: "mads.thines" Date: Mon, 9 Nov 2026 11:02:21 +0000 Subject: [PATCH 2/2] chore(feature-flags): regenerate manifest for insights-visual default on Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> --- .../generated/flags.manifest.json | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/feature-flags/generated/flags.manifest.json b/packages/feature-flags/generated/flags.manifest.json index 95995b991..6b3ea5f8f 100644 --- a/packages/feature-flags/generated/flags.manifest.json +++ b/packages/feature-flags/generated/flags.manifest.json @@ -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" ] } ]