| | 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) |
| | } |
| | } |
| |
|