Skip to content

Commit beaf779

Browse files
authored
🤖 refactor: simplify stats tab experiment to toggle (#1224)
Convert the Stats Tab experiment setting from a 3-option dropdown to a simple on/off toggle, matching the UX pattern used for other experiments. **Changes:** - Replace dropdown with Switch component in ExperimentsSection - Simplify FeatureFlagsContext API: `setStatsTabOverride` → `setStatsTabEnabled` - Remove PostHog implementation details from user-facing description - Backend schema unchanged (preserves flexibility) --- <details> <summary>📋 Implementation Plan</summary> # Plan: Simplify Stats Tab Experiment Toggle ## Goal Convert the Stats Tab experiment from a 3-option dropdown (`Default (experiment)`, `Always on`, `Always off`) to a simple on/off toggle, matching the pattern used for other experiments like Post-Compaction Context. ## Current State - **UI**: `ExperimentsSection.tsx` renders `StatsTabOverrideRow` with a `<select>` dropdown containing 3 options - **Frontend context**: `FeatureFlagsContext.tsx` exposes `StatsTabState` with `variant`, `override`, and `enabled` fields - **Backend service**: `featureFlagService.ts` fetches PostHog variant and applies 3-way override logic - **Storage**: Backend stores override in `~/.mux/config.json` under `featureFlagOverrides[stats_tab_v1]` - **Schema**: `api.ts` defines `StatsTabOverrideSchema = z.enum(["default", "on", "off"])` ## Proposed Changes ### 1. Frontend: Replace dropdown with toggle (~-30 LoC) **File: `src/browser/components/Settings/sections/ExperimentsSection.tsx`** - Replace `StatsTabOverrideRow` with a simpler row using `<Switch>`: - Label: "Stats tab" - Description: "Show timing statistics in the right sidebar" (no PostHog mention) - Toggle reflects `statsTabState.enabled` - Toggle calls `setStatsTabOverride("on" | "off")` based on new state ### 2. Context: Simplify `setStatsTabOverride` signature (~-5 LoC) **File: `src/browser/contexts/FeatureFlagsContext.tsx`** - `setStatsTabOverride(override: StatsTabOverride)` → `setStatsTabEnabled(enabled: boolean)` - Internally convert boolean to `"on" | "off"` when calling backend - No more "default" option exposed to UI ### 3. Backend & Schema: Unchanged The backend already stores `"on" | "off" | "default"` and computes `enabled` based on PostHog variant + override. We keep this to allow future flexibility, but the UI will only ever set `"on"` or `"off"` (never `"default"`). <details> <summary>Alternative: Migrate to experiments system</summary> We could move Stats Tab to the existing `EXPERIMENTS` + `ExperimentsContext` pattern (localStorage-based), but: - That would lose PostHog A/B tracking capability - Would require data migration from `config.json` to localStorage - The current feature flag service is already correct; simplifying the UI is sufficient This alternative is **not recommended** for this change. </details> ## Net LoC Estimate ~-25 lines (remove dropdown complexity, add simpler toggle, simplify context API) ## Files Modified 1. `src/browser/components/Settings/sections/ExperimentsSection.tsx` 2. `src/browser/contexts/FeatureFlagsContext.tsx` ## Testing - Manual: Open Settings → Experiments → verify toggle works - Existing typecheck and lint will catch regressions </details> --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high`_ Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 32712c6 commit beaf779

File tree

2 files changed

+29
-40
lines changed

2 files changed

+29
-40
lines changed

src/browser/components/Settings/sections/ExperimentsSection.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
type ExperimentId,
77
} from "@/common/constants/experiments";
88
import { Switch } from "@/browser/components/ui/switch";
9-
import { useFeatureFlags, type StatsTabOverride } from "@/browser/contexts/FeatureFlagsContext";
9+
import { useFeatureFlags } from "@/browser/contexts/FeatureFlagsContext";
1010
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
1111
import { useTelemetry } from "@/browser/hooks/useTelemetry";
1212

@@ -48,35 +48,29 @@ function ExperimentRow(props: ExperimentRowProps) {
4848
);
4949
}
5050

51-
function StatsTabOverrideRow() {
52-
const { statsTabState, setStatsTabOverride } = useFeatureFlags();
51+
function StatsTabRow() {
52+
const { statsTabState, setStatsTabEnabled } = useFeatureFlags();
5353

54-
const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
55-
const value = e.target.value as StatsTabOverride;
56-
setStatsTabOverride(value).catch(() => {
57-
// ignore
58-
});
59-
};
54+
const handleToggle = useCallback(
55+
(enabled: boolean) => {
56+
setStatsTabEnabled(enabled).catch(() => {
57+
// ignore
58+
});
59+
},
60+
[setStatsTabEnabled]
61+
);
6062

6163
return (
6264
<div className="flex items-center justify-between py-3">
6365
<div className="flex-1 pr-4">
6466
<div className="text-foreground text-sm font-medium">Stats tab</div>
65-
<div className="text-muted mt-0.5 text-xs">
66-
PostHog experiment-gated timing stats sidebar. Experiment variant:{" "}
67-
{statsTabState?.variant ?? "—"}.
68-
</div>
67+
<div className="text-muted mt-0.5 text-xs">Show timing statistics in the right sidebar</div>
6968
</div>
70-
<select
71-
className="bg-background text-foreground border-border-light rounded-md border px-2 py-1 text-xs"
72-
value={statsTabState?.override ?? "default"}
73-
onChange={onChange}
74-
aria-label="Stats tab override"
75-
>
76-
<option value="default">Default (experiment)</option>
77-
<option value="on">Always on</option>
78-
<option value="off">Always off</option>
79-
</select>
69+
<Switch
70+
checked={statsTabState?.enabled ?? false}
71+
onCheckedChange={handleToggle}
72+
aria-label="Toggle Stats tab"
73+
/>
8074
</div>
8175
);
8276
}
@@ -105,7 +99,7 @@ export function ExperimentsSection() {
10599
Experimental features that are still in development. Enable at your own risk.
106100
</p>
107101
<div className="divide-border-light divide-y">
108-
<StatsTabOverrideRow />
102+
<StatsTabRow />
109103
{experiments.map((exp) => (
110104
<ExperimentRow
111105
key={exp.id}

src/browser/contexts/FeatureFlagsContext.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,14 @@ function isStorybook(): boolean {
2626
return false;
2727
}
2828

29-
export type StatsTabVariant = "control" | "stats";
30-
export type StatsTabOverride = "default" | "on" | "off";
31-
3229
export interface StatsTabState {
3330
enabled: boolean;
34-
variant: StatsTabVariant;
35-
override: StatsTabOverride;
3631
}
3732

3833
interface FeatureFlagsContextValue {
3934
statsTabState: StatsTabState | null;
4035
refreshStatsTabState: () => Promise<void>;
41-
setStatsTabOverride: (override: StatsTabOverride) => Promise<void>;
36+
setStatsTabEnabled: (enabled: boolean) => Promise<void>;
4237
}
4338

4439
const FeatureFlagsContext = createContext<FeatureFlagsContextValue | null>(null);
@@ -53,29 +48,29 @@ export function FeatureFlagsProvider(props: { children: ReactNode }) {
5348
const { api } = useAPI();
5449
const [statsTabState, setStatsTabState] = useState<StatsTabState | null>(() => {
5550
if (isStorybook()) {
56-
return { enabled: true, variant: "stats", override: "default" };
51+
return { enabled: true };
5752
}
5853

5954
return null;
6055
});
6156

6257
const refreshStatsTabState = async (): Promise<void> => {
6358
if (!api) {
64-
setStatsTabState({ enabled: false, variant: "control", override: "default" });
59+
setStatsTabState({ enabled: false });
6560
return;
6661
}
6762

6863
const state = await api.features.getStatsTabState();
69-
setStatsTabState(state);
64+
setStatsTabState({ enabled: state.enabled });
7065
};
7166

72-
const setStatsTabOverride = async (override: StatsTabOverride): Promise<void> => {
67+
const setStatsTabEnabled = async (enabled: boolean): Promise<void> => {
7368
if (!api) {
7469
throw new Error("ORPC client not initialized");
7570
}
7671

77-
const state = await api.features.setStatsTabOverride({ override });
78-
setStatsTabState(state);
72+
const state = await api.features.setStatsTabOverride({ override: enabled ? "on" : "off" });
73+
setStatsTabState({ enabled: state.enabled });
7974
};
8075

8176
useEffect(() => {
@@ -86,22 +81,22 @@ export function FeatureFlagsProvider(props: { children: ReactNode }) {
8681
(async () => {
8782
try {
8883
if (!api) {
89-
setStatsTabState({ enabled: false, variant: "control", override: "default" });
84+
setStatsTabState({ enabled: false });
9085
return;
9186
}
9287

9388
const state = await api.features.getStatsTabState();
94-
setStatsTabState(state);
89+
setStatsTabState({ enabled: state.enabled });
9590
} catch {
9691
// Treat as disabled if we can't fetch.
97-
setStatsTabState({ enabled: false, variant: "control", override: "default" });
92+
setStatsTabState({ enabled: false });
9893
}
9994
})();
10095
}, [api]);
10196

10297
return (
10398
<FeatureFlagsContext.Provider
104-
value={{ statsTabState, refreshStatsTabState, setStatsTabOverride }}
99+
value={{ statsTabState, refreshStatsTabState, setStatsTabEnabled }}
105100
>
106101
{props.children}
107102
</FeatureFlagsContext.Provider>

0 commit comments

Comments
 (0)