import { forget } from '@xylabs/forget' import type { Log } from '@xylabs/log' import { useUserEvents } from '@xylabs/react-pixel' import { getLocalStorageObject, setLocalStorageObject } from '@xylabs/react-shared' import type { ReactElement } from 'react' import type React from 'react' import type { ExperimentProps } from './Experiment.tsx' import type { ExperimentsProps } from './ExperimentsProps.ts' import type { ExperimentsData, OutcomesData } from './models/index.ts' import { ExperimentsLocalStorageKey, OutcomesLocalStorageKey } from './models/index.ts' const defaultLocalStorageKey = 'testData' let experimentsTestData: { [index: string]: string } = {} let outcomes: OutcomesData = {} // prevent multi-outcome const saveOutcomes = () => { setLocalStorageObject(OutcomesLocalStorageKey, outcomes) } const saveExperimentsTestData = (key: string) => { const mergeData = (data: { [index: string]: string }, log?: Log): string => { const dataArray: string[] = [] for (const key in data) { dataArray.push(`${key}-${data[key]}`) } log?.info('MergeData', dataArray.join('|')) return dataArray.join('|') } localStorage.setItem(key, mergeData(experimentsTestData)) } const loadOutcomes = () => { outcomes = getLocalStorageObject(OutcomesLocalStorageKey) } const loadExperimentsTestData = (key: string) => { experimentsTestData = localStorage .getItem(key) ?.split('|') // eslint-disable-next-line unicorn/no-array-reduce .reduce( (acc, current) => { const data = current.split('-') acc[data[0]] = data[1] return acc }, {} as { [index: string]: string }, ) ?? {} } const missingKeyError = new Error('Experiment Elements must have Keys') const makeChildrenArray = (children: ReactElement[] | ReactElement) => { return Array.isArray(children) ? (children as ReactElement[]) : ([children] as ReactElement[]) } const buildLocalStorageKey = (localStorageProp: boolean | string) => { return ( localStorageProp === true ? defaultLocalStorageKey : typeof localStorageProp === 'string' ? localStorageProp ?? defaultLocalStorageKey : '' ) } const calcTotalWeight = (childList: ReactElement[]) => { let totalWeight = 0 for (const child of childList) { totalWeight += child.props.weight } return totalWeight } const saveExperimentDebugRanges = (name: string, totalWeight: number, childList: ReactElement[]) => { const experiments = getLocalStorageObject(ExperimentsLocalStorageKey) || {} experiments[name] = { totalWeight, variants: childList.map(child => ({ name: child.key?.toString(), weight: child.props.weight, })), } setLocalStorageObject(ExperimentsLocalStorageKey, experiments) } const Experiments: React.FC = (props) => { const { name, children, localStorageProp = true, } = props const userEvents = useUserEvents() const localStorageKey = buildLocalStorageKey(localStorageProp) const childList = makeChildrenArray(children) const totalWeight = calcTotalWeight(childList) loadOutcomes() loadExperimentsTestData(localStorageKey) saveExperimentDebugRanges(name, totalWeight, childList) const firstTime = outcomes[name] === undefined let targetWeight = outcomes[name] ?? Math.random() * totalWeight outcomes[name] = targetWeight saveOutcomes() for (const child of childList) { targetWeight -= child.props.weight if (targetWeight > 0) continue if (!child.key) { throw missingKeyError } experimentsTestData[name] = child.key?.toString() if (firstTime) { if (localStorageProp !== false) { saveExperimentsTestData(localStorageKey) } if (userEvents) { forget(userEvents.testStarted({ name, variation: child.key })) } } return child } throw new Error('Experiment Choice Failed') } export { Experiments }