UNPKG

react-experiment-hoc

Version:

Simple hoc to provide a/b-test features.

247 lines (204 loc) 6.04 kB
import React, { Component } from 'react'; import hoistNonReactStatic from 'hoist-non-react-statics'; import cookies from 'js-cookie'; import Context from './Context'; import weighted from './weighted'; const DEFAULT_OPTIONS = { /** * Wheter the experiment should be persistant betwen renders & sesions. * * -1 evaluate on every render * 0 evaluate on every session * 1 evaluate once * * @default 1 */ persistent: 1, /** * Auto play experiment on render * * @default false */ autoPlay: false, /** * Name of the fallback variation if fetch fails * * @default original */ fallbackName: 'original', /** * Prefix for the prop names passed to BaseComponent * * @default null */ propPrefix: null, /** * Fetch method returns variations of the given experiment * * @param experiment * * @returns Promise(array) * @default Promise.resolve([]) */ onFetch: () => Promise.resolve([]), /** * Trigger play * * @param experiment * @param variation * @param data * * @returns Promise() * @default Promise.resolve() */ onPlay: () => Promise.resolve([]), /** * Trigger win * * @param experiment * @param variation * @param data * * @returns Promise() * @default Promise.resolve() */ onWin: () => Promise.resolve(), /** * Set cookie method * * @param name * @param value * @param options * * @default js-cookie.set */ setCookie: cookies.set, /** * Get cookie method * * @param name * @param options * * @default js-cookie.get */ getCookie: cookies.get, }; export default (experiment, options = {}) => BaseComponent => { class ExperimentComponent extends Component { constructor(props) { super(props); this.state = { started: false, fallback: false, variants: [], variant: null } this._options = Object.assign({}, DEFAULT_OPTIONS, options) } componentWillMount() { // Apply context options const context = this.context || {} this._options = Object.assign({}, this._options, context) } componentDidMount() { const { variants } = this.state; const { autoPlay, fallbackName } = this._options; if (variants.length === 0) { this.fetch(experiment) .then(data => { const variant = this.getVariant(data); this.setState({ fallback: false, variant, variants: data }); if (autoPlay && variant) this.play(variant); }) .catch(() => { const fallbackVariant = { name: fallbackName, weight: 100 }; this.setState({ fallback: true, variant: fallbackName, variants: [fallbackVariant] }) }) } } getVariant(variants) { const { fallback } = this.state; if (variants.length === 0) return null const { persistent, setCookie, getCookie } = this._options; const key = `experiment_${experiment}${persistent}`; const cookieIndex = getCookie(key); let index = cookieIndex; if (!index || !variants[index] || persistent < 0) index = weighted(variants.map(v => v.weight)); if (!cookieIndex && !fallback && persistent >= 0) setCookie(key, index, { path: '/', expires: !persistent ? null : new Date(Date.now()+(60 * 60 * 24 * 365 * 10 * 1000)), }); return variants[index].name; } win(variant, data) { if (!this.state.started) return console.warn( `[react-experiment-hoc] Experiment "${experiment}" triggerd win without beeing started. Make sure you trigger play before win.` ); if (this.state.fallback) return console.warn( `[react-experiment-hoc] Experiment "${experiment}" is using fallback. Win will not be registerd.` ); this._options.onWin(experiment, variant, data); console.info(`[react-experiment-hoc] Experiment "${experiment}" with variant "${variant}" win.`); } play(variant, data) { if (this.state.started) return console.warn(`[react-experiment-hoc] Experiment "${experiment}" has already started.`); if (this.state.fallback) return console.warn( `[react-experiment-hoc] Experiment "${experiment}" is using a fallback variant. Play will not be registerd.` ); this.setState({ started: true }) this._options.onPlay(experiment, variant, data); console.info(`[react-experiment-hoc] Experiment "${experiment}" with variant "${variant}" started.`); } fetch(e) { return this._options.onFetch(e) } render() { const { variant } = this.state; if (!this.state.started && !variant) console.info(`[react-experiment-hoc] Render component while waiting for variants of experiment "${experiment}".`); if (!this.state.started && variant) console.info(`[react-experiment-hoc] Render experiment "${experiment}" with variant "${variant}".`); if (!variant) return null; const prefix = name => { if (!this._options.propPrefix) return name return this._options.propPrefix + (name.charAt(0).toUpperCase() + name.slice(1)) } const props = { ...this.props, [prefix('experimentName')]: experiment, [prefix('experimentVariant')]: variant, [prefix('experimentPlay')]: (data = {}) => this.play(variant, data), [prefix('experimentWin')]: (data = {}) => this.win(variant, data), } return ( <BaseComponent {...props} /> ); } } ExperimentComponent.contextType = Context; return hoistNonReactStatic(ExperimentComponent, BaseComponent); }