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
16 changes: 11 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
# AVOID CONDITIONAL BRANCHES (if/else) IN BUILD TARGETS AT ALL COSTS.
# Branches reduce reproducibility - builds should fail fast with clear errors
# if dependencies are missing, not silently fall back to different behavior.
#
# Telemetry in Development:
# Telemetry is disabled by default in dev mode (MUX_DISABLE_TELEMETRY=1).
# To enable it (e.g., for testing PostHog experiments), set:
# MUX_ENABLE_TELEMETRY_IN_DEV=1 make dev
# This single env var is sufficient - no need to also set MUX_DISABLE_TELEMETRY=0.

# Use PATH-resolved bash for portability across different systems.
# - Windows: /usr/bin/bash doesn't exist in Chocolatey's make environment or GitHub Actions
Expand Down Expand Up @@ -131,13 +137,13 @@ dev: node_modules/.installed build-main ## Start development server (Vite + node
@echo "Starting dev mode (3 watchers: nodemon for main process, esbuild for api, vite for renderer)..."
# On Windows, use npm run because bunx doesn't correctly pass arguments to concurrently
# https://github.com/oven-sh/bun/issues/18275
@MUX_DISABLE_TELEMETRY=$(or $(MUX_DISABLE_TELEMETRY),1) NODE_OPTIONS="--max-old-space-size=4096" npm x concurrently -k --raw \
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) NODE_OPTIONS="--max-old-space-size=4096" npm x concurrently -k --raw \
"bun x nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"vite"
else
dev: node_modules/.installed build-main build-preload ## Start development server (Vite + tsgo watcher for 10x faster type checking)
@MUX_DISABLE_TELEMETRY=$(or $(MUX_DISABLE_TELEMETRY),1) bun x concurrently -k \
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"vite"
Expand All @@ -151,7 +157,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
@echo ""
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
@# On Windows, use npm run because bunx doesn't correctly pass arguments
@MUX_DISABLE_TELEMETRY=$(or $(MUX_DISABLE_TELEMETRY),1) npmx concurrently -k \
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) npmx concurrently -k \
"npmx nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"npmx nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec \"node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)\"" \
Expand All @@ -163,7 +169,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
@echo " Frontend (with HMR): http://$(or $(VITE_HOST),localhost):$(or $(VITE_PORT),5173)"
@echo ""
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
@MUX_DISABLE_TELEMETRY=$(or $(MUX_DISABLE_TELEMETRY),1) bun x concurrently -k \
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"bun x nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec 'NODE_ENV=development node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)'" \
Expand All @@ -173,7 +179,7 @@ endif


start: node_modules/.installed build-main build-preload build-static ## Build and start Electron app
@NODE_ENV=development MUX_DISABLE_TELEMETRY=$(or $(MUX_DISABLE_TELEMETRY),1) bunx electron --remote-debugging-port=9222 .
@NODE_ENV=development MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) bunx electron --remote-debugging-port=9222 .

## Build targets (can run in parallel)
build: node_modules/.installed src/version.ts build-renderer build-main build-preload build-icons build-static ## Build all targets
Expand Down
22 changes: 17 additions & 5 deletions src/browser/components/Settings/sections/ExperimentsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useCallback } from "react";
import { useExperiment } from "@/browser/contexts/ExperimentsContext";
import React, { useCallback, useMemo } from "react";
import { useExperiment, useRemoteExperimentValue } from "@/browser/contexts/ExperimentsContext";
import {
getExperimentList,
EXPERIMENT_IDS,
type ExperimentId,
} from "@/common/constants/experiments";
import { Switch } from "@/browser/components/ui/switch";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
import { useTelemetry } from "@/browser/hooks/useTelemetry";

interface ExperimentRowProps {
experimentId: ExperimentId;
Expand All @@ -17,14 +18,18 @@ interface ExperimentRowProps {

function ExperimentRow(props: ExperimentRowProps) {
const [enabled, setEnabled] = useExperiment(props.experimentId);
const { onToggle } = props;
const remote = useRemoteExperimentValue(props.experimentId);
const telemetry = useTelemetry();
const { onToggle, experimentId } = props;

const handleToggle = useCallback(
(value: boolean) => {
setEnabled(value);
// Track the override for analytics
telemetry.experimentOverridden(experimentId, remote?.value ?? null, value);
onToggle?.(value);
},
[setEnabled, onToggle]
[setEnabled, telemetry, experimentId, remote?.value, onToggle]
);

return (
Expand All @@ -43,9 +48,16 @@ function ExperimentRow(props: ExperimentRowProps) {
}

export function ExperimentsSection() {
const experiments = getExperimentList();
const allExperiments = getExperimentList();
const { refreshWorkspaceMetadata } = useWorkspaceContext();

// Only show user-overridable experiments (non-overridable ones are hidden since users can't change them)
const experiments = useMemo(
() =>
allExperiments.filter((exp) => exp.showInSettings !== false && exp.userOverridable === true),
[allExperiments]
);

// When post-compaction experiment is toggled, refresh metadata to fetch/clear bundled state
const handlePostCompactionToggle = useCallback(() => {
void refreshWorkspaceMetadata();
Expand Down
77 changes: 77 additions & 0 deletions src/browser/contexts/ExperimentsContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { cleanup, render, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { GlobalWindow } from "happy-dom";
import { ExperimentsProvider, useExperimentValue } from "./ExperimentsContext";
import { EXPERIMENT_IDS } from "@/common/constants/experiments";
import type { ExperimentValue } from "@/common/orpc/types";
import type { APIClient } from "@/browser/contexts/API";
import type { RecursivePartial } from "@/browser/testUtils";

let currentClientMock: RecursivePartial<APIClient> = {};
void mock.module("@/browser/contexts/API", () => ({
useAPI: () => ({
api: currentClientMock as APIClient,
status: "connected" as const,
error: null,
}),
APIProvider: ({ children }: { children: React.ReactNode }) => children,
}));

describe("ExperimentsProvider", () => {
beforeEach(() => {
globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis;
globalThis.document = globalThis.window.document;
globalThis.window.localStorage.clear();
});

afterEach(() => {
cleanup();
globalThis.window = undefined as unknown as Window & typeof globalThis;
globalThis.document = undefined as unknown as Document;
currentClientMock = {};
});

test("polls getAll until remote variants are available", async () => {
let callCount = 0;

const getAllMock = mock(() => {
callCount += 1;

if (callCount === 1) {
return Promise.resolve({
[EXPERIMENT_IDS.POST_COMPACTION_CONTEXT]: { value: null, source: "cache" },
} satisfies Record<string, ExperimentValue>);
}

return Promise.resolve({
[EXPERIMENT_IDS.POST_COMPACTION_CONTEXT]: { value: "test", source: "posthog" },
} satisfies Record<string, ExperimentValue>);
});

currentClientMock = {
experiments: {
getAll: getAllMock,
reload: mock(() => Promise.resolve()),
},
};

function Observer() {
const enabled = useExperimentValue(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
return <div data-testid="enabled">{String(enabled)}</div>;
}

const { getByTestId } = render(
<ExperimentsProvider>
<Observer />
</ExperimentsProvider>
);

expect(getByTestId("enabled").textContent).toBe("false");

await waitFor(() => {
expect(getByTestId("enabled").textContent).toBe("true");
});

expect(getAllMock.mock.calls.length).toBeGreaterThanOrEqual(2);
});
});
Loading