| import murmur from 'imurmurhash' |
| import type { NextRouter } from 'next/router' |
| import { |
| CONTROL_VARIATION, |
| EXPERIMENTS, |
| ExperimentNames, |
| TREATMENT_VARIATION, |
| getActiveExperiments, |
| } from './experiments' |
| import { getUserEventsId } from '../events' |
| import type { ParsedUrlQuery } from 'querystring' |
|
|
| let experimentsInitialized = false |
| let userIsStaff = false |
|
|
| export function shouldShowExperiment( |
| experimentKey: ExperimentNames | { key: ExperimentNames }, |
| locale: string, |
| version: string, |
| isStaff: boolean, |
| routerQuery: ParsedUrlQuery, |
| ) { |
| |
| if (typeof experimentKey === 'object') { |
| experimentKey = experimentKey.key |
| } |
|
|
| |
| const experiments = getActiveExperiments('all') |
| for (const experiment of experiments) { |
| if (experiment.key === experimentKey) { |
| |
| if (controlGroupOverride[experiment.key]) { |
| const controlGroup = getExperimentControlGroupFromSession( |
| experimentKey, |
| experiment.percentOfUsersToGetExperiment, |
| ) |
| return controlGroup === TREATMENT_VARIATION |
| |
| } else if ( |
| (experiment.limitToLanguages?.length |
| ? experiment.limitToLanguages.includes(locale) |
| : true) && |
| (experiment.limitToVersions?.length ? experiment.limitToVersions.includes(version) : true) |
| ) { |
| |
| if (experiment.alwaysShowForStaff) { |
| if (isStaff) { |
| userIsStaff = true |
| console.log(`Staff cookie is set, showing '${experiment.key}' experiment`) |
| return true |
| } |
| } |
| if (experiment.turnOnWithURLParam) { |
| if ( |
| typeof routerQuery?.feature === 'string' |
| ? routerQuery.feature.toLowerCase() === experiment.turnOnWithURLParam.toLowerCase() |
| : false |
| ) { |
| return true |
| } |
| } |
| return ( |
| getExperimentControlGroupFromSession( |
| experimentKey, |
| experiment.percentOfUsersToGetExperiment, |
| ) === TREATMENT_VARIATION |
| ) |
| } |
| } |
| } |
| return false |
| } |
|
|
| |
| export const controlGroupOverride = {} as { [key in ExperimentNames]: 'treatment' | 'control' } |
| if (typeof window !== 'undefined') { |
| |
| window.overrideControlGroup = ( |
| experimentKey: ExperimentNames, |
| controlGroup: 'treatment' | 'control', |
| ): string => { |
| const activeExperiments = getActiveExperiments('all') |
| |
| if (activeExperiments.some((experiment) => experiment.key === experimentKey)) { |
| controlGroupOverride[experimentKey] = controlGroup |
| const event = new Event('controlGroupOverrideChanged') |
| window.dispatchEvent(event) |
| return `Updated ${experimentKey}. Session is now in the "${controlGroup}" group for this session.` |
| } else { |
| throw new Error( |
| `Invalid experiment key: ${experimentKey}. Must be one of: ${activeExperiments.map((experiment) => experiment.key).join(', ')}`, |
| ) |
| } |
| } |
| } |
|
|
| |
| export function getExperimentControlGroupFromSession( |
| experimentKey: ExperimentNames, |
| percentToGetExperiment = 50, |
| ): string { |
| if (controlGroupOverride[experimentKey]) { |
| return controlGroupOverride[experimentKey] |
| } else if (process.env.NODE_ENV === 'test') { |
| return CONTROL_VARIATION |
| } |
| |
| |
| const id = getUserEventsId() |
| const hash = murmur(experimentKey).hash(id).result() |
| const modHash = hash % 100 |
| return modHash < percentToGetExperiment ? TREATMENT_VARIATION : CONTROL_VARIATION |
| } |
|
|
| export function getExperimentVariationForContext(locale: string, version: string): string { |
| const experiments = getActiveExperiments(locale, version) |
| for (const experiment of experiments) { |
| if (experiment.includeVariationInContext) { |
| |
| if ( |
| (experiment.turnOnWithURLParam && |
| window.location?.search |
| ?.toLowerCase() |
| .includes(`feature=${experiment.turnOnWithURLParam.toLowerCase()}`)) || |
| (experiment.alwaysShowForStaff && userIsStaff) |
| ) { |
| return TREATMENT_VARIATION |
| } |
| return getExperimentControlGroupFromSession( |
| experiment.key, |
| experiment.percentOfUsersToGetExperiment, |
| ) |
| } |
| } |
|
|
| |
| return CONTROL_VARIATION |
| } |
|
|
| export function initializeExperiments( |
| locale: string, |
| currentVersion: string, |
| allVersions: { [key: string]: { version: string } }, |
| ) { |
| if (experimentsInitialized) return |
| experimentsInitialized = true |
|
|
| |
| for (const [experimentKey, experiment] of Object.entries(EXPERIMENTS)) { |
| if (experiment.limitToVersions?.includes('enterprise-server@latest')) { |
| |
| const latestEnterpriseServerVersion = Object.keys(allVersions) |
| .filter((version) => version.startsWith('enterprise-server@')) |
| .sort((a, b) => { |
| const aVersion = a.split('@')[1] |
| const bVersion = b.split('@')[1] |
| return Number(bVersion) - Number(aVersion) |
| })[0] |
| if (latestEnterpriseServerVersion) { |
| EXPERIMENTS[experimentKey as ExperimentNames].limitToVersions = |
| experiment.limitToVersions.map((version) => |
| version.replace( |
| 'enterprise-server@latest', |
| allVersions[latestEnterpriseServerVersion].version, |
| ), |
| ) |
| } |
| } |
| } |
|
|
| const experiments = getActiveExperiments(locale, currentVersion) |
|
|
| let numberOfExperimentsUsingContext = 0 |
| for (const experiment of experiments) { |
| if (experiment.includeVariationInContext) { |
| |
| numberOfExperimentsUsingContext++ |
| if (numberOfExperimentsUsingContext > 1) { |
| throw new Error( |
| 'Only one experiment can include its variation in the context at a time. Please update the experiments configuration.', |
| ) |
| } |
| } |
|
|
| const controlGroup = getExperimentControlGroupFromSession( |
| experiment.key, |
| experiment.percentOfUsersToGetExperiment, |
| ) |
|
|
| |
| console.log( |
| `Experiment ${experiment.key} is in the "${controlGroup === TREATMENT_VARIATION ? TREATMENT_VARIATION : CONTROL_VARIATION}" group for this browser.\nCall function window.overrideControlGroup('${experiment.key}', 'treatment' | 'control') to change your group for this session.`, |
| ) |
| } |
| } |
|
|
| |
| |
| export function initializeForwardFeatureUrlParam(router: NextRouter, currentVersion: string) { |
| const experiments = getActiveExperiments(router.locale || 'en', currentVersion) |
|
|
| if (!experiments.some((experiment) => experiment.turnOnWithURLParam)) { |
| return |
| } |
|
|
| try { |
| const searchParams = new URLSearchParams(window.location.search) |
| const featureValue = searchParams.get('feature') |
| |
| if (!featureValue) return |
|
|
| const updateAnchorHref = (anchor: HTMLAnchorElement): void => { |
| try { |
| const url = new URL(anchor.href, window.location.origin) |
| url.searchParams.set('feature', featureValue) |
| router.push(url.toString()) |
| } catch (error) { |
| console.error('Error modifying anchor URL:', error) |
| router.push(anchor.href) |
| } |
| } |
|
|
| const handleClick = (event: any) => { |
| const anchor = event.target?.closest('a') |
| if (anchor) { |
| |
| event.preventDefault() |
| updateAnchorHref(anchor) |
| } |
| } |
|
|
| const handleKeyDown = (event: any) => { |
| if (event.key !== 'Enter') return |
| const anchor = event.target?.closest('a') |
| if (anchor) { |
| |
| event.preventDefault() |
| updateAnchorHref(anchor) |
| } |
| } |
|
|
| document.addEventListener('click', handleClick) |
| document.addEventListener('keydown', handleKeyDown) |
|
|
| return () => { |
| document.removeEventListener('click', handleClick) |
| document.removeEventListener('keydown', handleKeyDown) |
| } |
| } catch (error) { |
| console.error('Error adding event listener:', error) |
| } |
| } |
|
|