@the-control-group/react-split-test
Version:
A/B Split Testing Component for React
118 lines (117 loc) • 4.81 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importStar(require("react"));
const variationPromiseCache = new Map();
class Experiment extends react_1.Component {
constructor(props) {
super(props);
this.cacheKey = `-split-test:${this.props.id}`;
this.storage = this.props.session ? sessionStorage : localStorage;
this.state = {
selectedVariation: this.chooseVariation()
};
}
chooseVariation() {
let cachedData;
const cachedString = this.storage.getItem(this.cacheKey);
if (cachedString) {
try {
cachedData = JSON.parse(cachedString);
}
catch (e) {
console.error('Invalid experiment cache data', e);
}
}
if (cachedData) {
return cachedData.selectedVariation;
}
else {
// Cache promises so only one decision is made per experiment ID
let variationDecider;
if (variationPromiseCache.has(this.props.id)) {
variationDecider = variationPromiseCache.get(this.props.id);
}
else {
variationDecider = this.props.variationDecider(this.props.children);
variationPromiseCache.set(this.props.id, variationDecider);
}
variationDecider
.then(selectedVariation => {
const experimentData = {
id: this.props.id,
selectedVariation
};
// Since this is async, we need to check if another component with the same experiment
// has already made this decision in a potential race condition
let raceData;
const cachedRaceString = this.storage.getItem(this.cacheKey);
if (cachedRaceString) {
try {
raceData = JSON.parse(cachedRaceString);
}
catch (e) {
console.error('Invalid experiment race cache data', e);
}
}
if (!raceData) {
this.storage.setItem(this.cacheKey, JSON.stringify(experimentData));
this.props.onParticipate(experimentData);
// Remove the initial promise from the cache since subsequent renders/mounts will work off of the cache
variationPromiseCache.delete(this.props.id);
}
this.setState({
selectedVariation: raceData ? raceData.selectedVariation : selectedVariation
});
});
return null;
}
}
render() {
const { selectedVariation } = this.state;
return react_1.default.Children.map(this.props.children, child => {
if (react_1.default.isValidElement(child) && child.props.isVariation && child.props.id !== selectedVariation)
return;
return child;
});
}
}
Experiment.defaultProps = {
onParticipate: () => { },
/**
* Stub A/B decider
* @param {React.node} experimentChildren - React children nodes of the experiment
* @return {Promise<variation id>} Selected variation ID
*/
variationDecider: (experimentChildren) => {
const variations = [];
react_1.default.Children.forEach(experimentChildren, c => {
if (react_1.default.isValidElement(c) && c.props.isVariation)
variations.push(c.props.id);
});
return Promise.resolve(variations[Math.floor(Math.random() * variations.length)]);
}
};
exports.default = Experiment;