From 38d9e77aa0fa99c78d4993b932512ba2f403f60d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 23 Jun 2025 21:15:24 +0100 Subject: [PATCH 1/3] coderd: rename handler to match route and semantics --- coderd/coderd.go | 2 +- coderd/experiments.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 4507cd1dd7605..7124ce99bfcd2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -972,7 +972,7 @@ func New(options *Options) *API { }) r.Route("/experiments", func(r chi.Router) { r.Use(apiKeyMiddleware) - r.Get("/available", handleExperimentsSafe) + r.Get("/available", handleExperimentsAvailable) r.Get("/", api.handleExperimentsGet) }) r.Get("/updatecheck", api.updateCheck) diff --git a/coderd/experiments.go b/coderd/experiments.go index 6f03daa4e9d88..a0949e9411664 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -26,7 +26,7 @@ func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { // @Tags General // @Success 200 {array} codersdk.Experiment // @Router /experiments/available [get] -func handleExperimentsSafe(rw http.ResponseWriter, r *http.Request) { +func handleExperimentsAvailable(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() httpapi.Write(ctx, rw, http.StatusOK, codersdk.AvailableExperiments{ Safe: codersdk.ExperimentsSafe, From 9a359e820d8432954e2c6875c6b229ac441e488d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 23 Jun 2025 21:16:35 +0100 Subject: [PATCH 2/3] coderd: log on invalid experiments --- coderd/coderd.go | 4 +++- codersdk/deployment.go | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 7124ce99bfcd2..a4c4d335d738f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1895,7 +1895,9 @@ func ReadExperiments(log slog.Logger, raw []string) codersdk.Experiments { exps = append(exps, codersdk.ExperimentsSafe...) default: ex := codersdk.Experiment(strings.ToLower(v)) - if !slice.Contains(codersdk.ExperimentsSafe, ex) { + if !slice.Contains(codersdk.ExperimentsKnown, ex) { + log.Warn(context.Background(), "ignoring unknown experiment", slog.F("experiment", ex)) + } else if !slice.Contains(codersdk.ExperimentsSafe, ex) { log.Warn(context.Background(), "🐉 HERE BE DRAGONS: opting into hidden experiment", slog.F("experiment", ex)) } exps = append(exps, ex) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 90e8a4c879ec5..54390c675163e 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3372,6 +3372,18 @@ const ( ExperimentAITasks Experiment = "ai-tasks" // Enables the new AI tasks feature. ) +// ExperimentsKnown should include all experiments defined above. +var ExperimentsKnown = Experiments{ + ExperimentExample, + ExperimentAutoFillParameters, + ExperimentNotifications, + ExperimentWorkspaceUsage, + ExperimentWebPush, + ExperimentWorkspacePrebuilds, + ExperimentAgenticChat, + ExperimentAITasks, +} + // ExperimentsSafe should include all experiments that are safe for // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should From 0ba477222d69b4532fe63d4fb4aa135940e4bbb8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 23 Jun 2025 21:17:31 +0100 Subject: [PATCH 3/3] generate known experiments and warn only invalid experiments only in deployment view --- codersdk/deployment.go | 3 +++ site/src/api/queries/experiments.ts | 8 ++++++-- site/src/api/typesGenerated.ts | 12 ++++++++++-- site/src/hooks/useEmbeddedMetadata.ts | 6 +++--- site/src/modules/dashboard/DashboardProvider.tsx | 4 ++-- .../OverviewPage/OverviewPage.tsx | 8 ++++++-- .../OverviewPage/OverviewPageView.stories.tsx | 8 ++++---- .../OverviewPage/OverviewPageView.tsx | 6 +++--- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 54390c675163e..ce15ee407a8f3 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3396,6 +3396,9 @@ var ExperimentsSafe = Experiments{ // Multiple experiments may be enabled at the same time. // Experiments are not safe for production use, and are not guaranteed to // be backwards compatible. They may be removed or renamed at any time. +// The below typescript-ignore annotation allows our typescript generator +// to generate an enum list, which is used in the frontend. +// @typescript-ignore Experiments type Experiments []Experiment // Returns a list of experiments that are enabled for the deployment. diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index 546a85ab5f083..fe7e3419a7065 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,11 +1,11 @@ import { API } from "api/api"; -import type { Experiments } from "api/typesGenerated"; +import { type Experiment, Experiments } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; const experimentsKey = ["experiments"] as const; -export const experiments = (metadata: MetadataState) => { +export const experiments = (metadata: MetadataState) => { return cachedQuery({ metadata, queryKey: experimentsKey, @@ -19,3 +19,7 @@ export const availableExperiments = () => { queryFn: async () => API.getAvailableExperiments(), }; }; + +export const isKnownExperiment = (experiment: string): boolean => { + return Experiments.includes(experiment as Experiment); +}; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 9a5acb4fbe569..485c7d25d859d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -835,8 +835,16 @@ export type Experiment = | "workspace-prebuilds" | "workspace-usage"; -// From codersdk/deployment.go -export type Experiments = readonly Experiment[]; +export const Experiments: Experiment[] = [ + "ai-tasks", + "agentic-chat", + "auto-fill-parameters", + "example", + "notifications", + "web-push", + "workspace-prebuilds", + "workspace-usage", +]; // From codersdk/externalauth.go export interface ExternalAuth { diff --git a/site/src/hooks/useEmbeddedMetadata.ts b/site/src/hooks/useEmbeddedMetadata.ts index 1dd2d7c2bbeeb..908d89c9590e5 100644 --- a/site/src/hooks/useEmbeddedMetadata.ts +++ b/site/src/hooks/useEmbeddedMetadata.ts @@ -2,7 +2,7 @@ import type { AppearanceConfig, BuildInfoResponse, Entitlements, - Experiments, + Experiment, Region, User, UserAppearanceSettings, @@ -24,7 +24,7 @@ export const DEFAULT_METADATA_KEY = "property"; */ type AvailableMetadata = Readonly<{ user: User; - experiments: Experiments; + experiments: Experiment[]; appearance: AppearanceConfig; userAppearance: UserAppearanceSettings; entitlements: Entitlements; @@ -89,7 +89,7 @@ export class MetadataManager implements MetadataManagerApi { userAppearance: this.registerValue("userAppearance"), entitlements: this.registerValue("entitlements"), - experiments: this.registerValue("experiments"), + experiments: this.registerValue("experiments"), "build-info": this.registerValue("build-info"), regions: this.registerRegionValue(), tasksTabVisible: this.registerValue("tasksTabVisible"), diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index d56e30afaed8b..7eae04befa24a 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -5,7 +5,7 @@ import { organizations } from "api/queries/organizations"; import type { AppearanceConfig, Entitlements, - Experiments, + Experiment, Organization, } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -19,7 +19,7 @@ import { selectFeatureVisibility } from "./entitlements"; export interface DashboardValue { entitlements: Entitlements; - experiments: Experiments; + experiments: Experiment[]; appearance: AppearanceConfig; organizations: readonly Organization[]; showOrganizations: boolean; diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPage.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPage.tsx index fc15eca1ec4f1..c4f49e1dda31a 100644 --- a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPage.tsx @@ -1,5 +1,9 @@ import { deploymentDAUs } from "api/queries/deployment"; -import { availableExperiments, experiments } from "api/queries/experiments"; +import { + availableExperiments, + experiments, + isKnownExperiment, +} from "api/queries/experiments"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider"; import type { FC } from "react"; @@ -18,7 +22,7 @@ const OverviewPage: FC = () => { const safeExperiments = safeExperimentsQuery.data?.safe ?? []; const invalidExperiments = enabledExperimentsQuery.data?.filter((exp) => { - return !safeExperiments.includes(exp); + return !isKnownExperiment(exp); }) ?? []; const { data: dailyActiveUsers } = useQuery(deploymentDAUs()); diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx index 3535d4ffd1d47..24e121b9ff0f5 100644 --- a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx @@ -30,7 +30,7 @@ const meta: Meta = { description: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", flag: "experiments", - value: ["workspace_actions"], + value: ["example"], flag_shorthand: "", hidden: false, }, @@ -82,8 +82,8 @@ export const allExperimentsEnabled: Story = { hidden: false, }, ], - safeExperiments: ["shared-ports"], - invalidExperiments: ["invalid"], + safeExperiments: ["example"], + invalidExperiments: [], }, }; @@ -118,7 +118,7 @@ export const invalidExperimentsEnabled: Story = { hidden: false, }, ], - safeExperiments: ["shared-ports"], + safeExperiments: ["example"], invalidExperiments: ["invalid"], }, }; diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx index 505036a9c821f..37da47f4b8a16 100644 --- a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.tsx @@ -1,7 +1,7 @@ import AlertTitle from "@mui/material/AlertTitle"; import type { DAUsResponse, - Experiments, + Experiment, SerpentOption, } from "api/typesGenerated"; import { Link } from "components/Link/Link"; @@ -22,8 +22,8 @@ import { UserEngagementChart } from "./UserEngagementChart"; type OverviewPageViewProps = { deploymentOptions: SerpentOption[]; dailyActiveUsers: DAUsResponse | undefined; - readonly invalidExperiments: Experiments | string[]; - readonly safeExperiments: Experiments | string[]; + readonly invalidExperiments: readonly string[]; + readonly safeExperiments: readonly Experiment[]; }; export const OverviewPageView: FC = ({