diff --git a/components/dashboard/src/Analytics.tsx b/components/dashboard/src/Analytics.tsx index 3ff8c09fece6f9..11831cafcaa0f7 100644 --- a/components/dashboard/src/Analytics.tsx +++ b/components/dashboard/src/Analytics.tsx @@ -47,7 +47,7 @@ interface TrackPathChanged { } interface TrackUIExperiments { - ui_experiments?: string[], + ui_experiments?: {}, } //call this to track all events outside of button and anchor clicks @@ -56,7 +56,7 @@ export const trackEvent = (event: Event, properties: EventProperties) => { } const trackEventInternal = (event: InternalEvent, properties: InternalEventProperties, userKnown?: boolean) => { - properties.ui_experiments = Experiment.getAsArray(); + properties.ui_experiments = Experiment.get(); getGitpodService().server.trackEvent({ //if the user is authenticated, let server determine the id. else, pass anonymousId explicitly. @@ -141,7 +141,7 @@ export const trackLocation = async (userKnown: boolean) => { path: window.location.pathname, host: window.location.hostname, url: window.location.href, - ui_experiments: Experiment.getAsArray(), + ui_experiments: Experiment.get(), }; getGitpodService().server.trackLocation({ diff --git a/components/dashboard/src/experiments.ts b/components/dashboard/src/experiments.ts index e40d62020dd566..bac27ec8a99d65 100644 --- a/components/dashboard/src/experiments.ts +++ b/components/dashboard/src/experiments.ts @@ -30,57 +30,68 @@ const Experiments = { // "example": 0.1, "login-from-context-6826": 0.5, // https://github.com/gitpod-io/gitpod/issues/6826 }; -const ExperimentsSet = new Set(Object.keys(Experiments)) as Set; +type Experiments = Partial<{ [e in Experiment]: boolean }>; export type Experiment = keyof (typeof Experiments); export namespace Experiment { - export function seed(keepCurrent: boolean): Set { - const current = keepCurrent ? get() : undefined; - - // add all current experiments to ensure stability - const result = new Set([...(current || [])].filter(e => ExperimentsSet.has(e))); + /** + * Randomly decides what the set of Experiments is the user participates in + * @param keepCurrent + * @returns Experiments + */ + export function seed(keepCurrent: boolean): Experiments { + const result = keepCurrent ? get() || {} : {}; - // identify all new experiments and add if random - const newExperiment = new Set([...ExperimentsSet].filter(e => !result.has(e))); - for (const e of newExperiment) { - if (Math.random() < Experiments[e]) { - result.add(e); + for (const experiment of Object.keys(Experiments) as Experiment[]) { + if (!(experiment in result)) { + result[experiment] = Math.random() < Experiments[experiment]; } } return result; } - export function set(set: Set): void { + export function set(set: Experiments): void { try { - const arr = Array.from(set); - window.localStorage.setItem(UI_EXPERIMENTS_KEY, JSON.stringify(arr)); + window.localStorage.setItem(UI_EXPERIMENTS_KEY, JSON.stringify(set)); } catch (err) { - console.error(`error setting ${UI_EXPERIMENTS_KEY}`, err); + console.warn(`error setting ${UI_EXPERIMENTS_KEY}`, err); } } export function has(experiment: Experiment): boolean { - const set = get(); - if (!set) { + try { + const set = get(); + if (!set) { + return false; + } + return set[experiment] === true; + } catch (err) { + console.warn(`error checking experiment '${experiment}'`, err); return false; } - return set.has(experiment); } - export function get(): Set | undefined { - const arr = window.localStorage.getItem(UI_EXPERIMENTS_KEY); - if (arr === null) { - return undefined; - } - return new Set(JSON.parse(arr)) as Set; - } + /** Retrieves all currently valid Experiments from localStorage */ + export function get(): Experiments | undefined { + try { + const objStr = window.localStorage.getItem(UI_EXPERIMENTS_KEY); + if (objStr === null) { + return undefined; + } - export function getAsArray(): Experiment[] { - const set = get(); - if (!set) { - return []; + const obj = JSON.parse(objStr) as Experiments; + // trim to contain only known keys so we're type-safe + for (const e of Object.keys(obj)) { + if (!(e in Experiments)) { + delete (obj as any)[e]; + } + } + return obj; + } catch (err) { + // we definitely don't want to break anybody because of weird errors + console.warn(`error getting ${UI_EXPERIMENTS_KEY}`, err); + return undefined; } - return Array.from(set); } } \ No newline at end of file