redux-ab-test
Version:
A/B testing React components with Redux and debug tools. Isomorphic with a simple, universal interface. Well documented and lightweight. Tested in popular browsers and Node.js. Includes helpers for React, Redux, and Segment.io
383 lines (304 loc) • 12.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.mapDispatchToProps = exports.mapStateToProps = exports.Experiment = undefined;
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _immutable = require('immutable');
var _immutable2 = _interopRequireDefault(_immutable);
var _reactRedux = require('react-redux');
var _redux = require('redux');
var _getKey = require('../../utils/get-key');
var _getKey2 = _interopRequireDefault(_getKey);
var _selectVariation = require('../../utils/select-variation');
var _selectVariation2 = _interopRequireDefault(_selectVariation);
var _module = require('../../module');
var _variation = require('../../components/variation');
var _variation2 = _interopRequireDefault(_variation);
var _logger = require('../../utils/logger');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var isValid = function () {
function isValid(child) {
return (0, _variation.isVariation)(child);
}
return isValid;
}();
var Experiment = exports.Experiment = function (_React$Component) {
(0, _inherits3['default'])(Experiment, _React$Component);
function Experiment() {
var _ref;
var _temp, _this, _ret;
(0, _classCallCheck3['default'])(this, Experiment);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = (0, _possibleConstructorReturn3['default'])(this, (_ref = Experiment.__proto__ || (0, _getPrototypeOf2['default'])(Experiment)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
played: false,
mounted: false
}, _temp), (0, _possibleConstructorReturn3['default'])(_this, _ret);
}
(0, _createClass3['default'])(Experiment, [{
key: 'componentWillMount',
/**
* Activate the variation
*/
value: function () {
function componentWillMount() {
var _props = this.props,
experiment = _props.experiment,
variation = _props.variation,
dispatchActivate = _props.dispatchActivate,
dispatchPlay = _props.dispatchPlay;
var mounted = this.state.mounted;
var played = this.state.played;
(0, _logger.logger)(__filename + ' componentWillMount experiment, selector=\'' + this.props.selector + '\', experiment.name=\'' + (experiment && experiment.get('name')) + '\'');
// If the experiment is unavailable, then record it wasn't played and move on
if (!experiment) {
played = false;
this.setState({ played: played, mounted: mounted });
return;
}
// These will trigger `componentWillReceiveProps`
dispatchActivate({ experiment: experiment });
if (mounted) {
played = true;
dispatchPlay({ experiment: experiment, variation: variation });
}
this.setState({ played: played, mounted: mounted });
}
return componentWillMount;
}()
/**
* Update the component's state with the new properties
*/
}, {
key: 'componentWillReceiveProps',
value: function () {
function componentWillReceiveProps(nextProps) {
var experiment = nextProps.experiment,
variation = nextProps.variation,
dispatchActivate = nextProps.dispatchActivate,
dispatchPlay = nextProps.dispatchPlay;
var mounted = this.state.mounted;
(0, _logger.logger)(__filename + ' componentWillReceiveProps experiment, selector=\'' + this.props.selector + '\', experiment.name=\'' + (experiment && experiment.get('name')) + '\'');
if (!experiment) {
// If we no-longer have an experiment anymore, then update the internal state
if (this.props.experiment) {
this.setState({ played: false, mounted: mounted });
}
return;
}
if (!experiment.equals(this.props.experiment) || !variation.equals(this.props.variation)) {
// These will trigger `componentWillReceiveProps`
if (!this.state.played && mounted) {
dispatchActivate({ experiment: experiment });
dispatchPlay({ experiment: experiment, variation: variation });
this.setState({ played: true, mounted: mounted });
}
}
}
return componentWillReceiveProps;
}()
/**
* Once the experiment is mounted, dispatch the play event
*/
}, {
key: 'componentDidMount',
value: function () {
function componentDidMount() {
var _props2 = this.props,
experiment = _props2.experiment,
variation = _props2.variation,
dispatchPlay = _props2.dispatchPlay;
var played = this.state.played;
(0, _logger.logger)(__filename + ' componentDidMount experiment, selector=\'' + this.props.selector + '\', experiment.name=\'' + (experiment && experiment.get('name')) + '\'');
if (played || !experiment || !variation) {
return;
}
dispatchPlay({ experiment: experiment, variation: variation });
this.setState({ played: true, mounted: true });
}
return componentDidMount;
}()
/**
* Deactivate the variation from the state
*/
}, {
key: 'componentWillUnmount',
value: function () {
function componentWillUnmount() {
var _props3 = this.props,
experiment = _props3.experiment,
dispatchDeactivate = _props3.dispatchDeactivate;
(0, _logger.logger)(__filename + ' componentWillUnmount experiment, selector=\'' + this.props.selector + '\', experiment.name=\'' + (experiment && experiment.get('name')) + '\'');
// Dispatch the deactivation event
if (experiment) {
dispatchDeactivate({ experiment: experiment });
}
}
return componentWillUnmount;
}()
/**
* Render one of the variations or `null`
*/
}, {
key: 'render',
value: function () {
function render() {
var _props4 = this.props,
children = _props4.children,
defaultVariationName = _props4.defaultVariationName;
var experiment = this.props.experiment || _immutable2['default'].Map({ name: null, id: null });
var variation = this.props.variation || _immutable2['default'].Map({ name: defaultVariationName, id: null });
var childrenArray = _react2['default'].Children.toArray(children);
// If there are no children, render nothing
if (childrenArray.length === 0) {
(0, _logger.logger)(__filename + ' No children for experiment selector=\'' + this.props.selector + '\', experiment.name=\'' + experiment.get('name') + '\'');
return null;
}
// If the first child is text or an unknown component, simply wrap it in a Variation
if (childrenArray.length === 1 && !isValid(childrenArray[0])) {
return _react2['default'].createElement(
_variation2['default'],
{ id: variation.get('id'), name: variation.get('name'), experiment: experiment, variation: variation },
children
);
}
var selectedChild = [childrenArray.find(function (child) {
return variation.get('id') && child.props.id === variation.get('id');
}), childrenArray.find(function (child) {
return variation.get('name') && child.props.name === variation.get('name');
})].filter(function (value) {
return value;
}).find(function (value) {
return value;
});
if (!selectedChild) {
throw new Error('Expected to find a Variation child matching id=' + variation.get('id') + ' or name=' + variation.get('name'));
}
(0, _logger.logger)(__filename + ' Rendered Experiment selector=\'' + this.props.selector + '\', experiment.name=\'' + experiment.get('name') + '\'');
// Inject the helper `handleWin` into the child element
return _react2['default'].cloneElement(selectedChild, {
experiment: experiment,
variation: variation,
id: variation.get('id'),
name: variation.get('name')
});
}
return render;
}()
}]);
return Experiment;
}(_react2['default'].Component);
Experiment.defaultProps = {
reduxAbTest: _immutable2['default'].Map({}),
id: null,
name: null,
selector: null,
experiment: null,
variation: null,
defaultVariationName: null,
dispatchActivate: function () {
function dispatchActivate() {}
return dispatchActivate;
}(),
dispatchDeactivate: function () {
function dispatchDeactivate() {}
return dispatchDeactivate;
}(),
dispatchPlay: function () {
function dispatchPlay() {}
return dispatchPlay;
}(),
dispatchWin: function () {
function dispatchWin() {}
return dispatchWin;
}()
};
var getExperiment = function () {
function getExperiment(reduxAbTest, selector, id, name) {
// Select the experiment from the redux store
var groupByName = {};
var groupById = {};
reduxAbTest.get('experiments').forEach(function (experiment) {
(0, _logger.logger)(__filename + ' getExperiment forEach experiment.id=\'' + experiment.get('id') + '\'');
(0, _logger.logger)(__filename + ' getExperiment forEach experiment.name=\'' + experiment.get('name') + '\'');
if (experiment.get('name')) {
groupByName[experiment.get('name')] = experiment;
}
if (experiment.get('id')) {
groupById[experiment.get('id')] = experiment;
}
});
var experimentById = groupById[id];
var experimentByName = groupByName[name];
var experimentBySelector = groupByName[selector] || groupById[selector];
var experiment = experimentById || experimentByName || experimentBySelector;
(0, _logger.logger)(__filename + ' getExperiment selector=\'' + selector + '\'');
(0, _logger.logger)(__filename + ' getExperiment id=\'' + id + '\'');
(0, _logger.logger)(__filename + ' getExperiment name=\'' + name + '\'');
(0, _logger.logger)(__filename + ' getExperiment experiment.name=\'' + (experiment && experiment.get('name')) + '\'');
// Return the resulting experiment
return experiment;
}
return getExperiment;
}();
// Map the Redux Store to the Experiment's props
var mapStateToProps = exports.mapStateToProps = function () {
function mapStateToProps(_ref2) {
var reduxAbTest = _ref2.reduxAbTest;
return { reduxAbTest: reduxAbTest };
}
return mapStateToProps;
}();
// Map the action creators to the the Experiment's props.
var mapDispatchToProps = exports.mapDispatchToProps = function () {
function mapDispatchToProps(dispatch) {
return (0, _redux.bindActionCreators)({
dispatchActivate: _module.activate,
dispatchDeactivate: _module.deactivate,
dispatchPlay: _module.play,
dispatchWin: _module.win
}, dispatch);
}
return mapDispatchToProps;
}();
// Export the new React Container.
exports['default'] = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(function (props) {
var experiment = getExperiment(props.reduxAbTest, props.selector, props.id, props.name);
var variation = null;
if (experiment) {
variation = (0, _selectVariation2['default'])({
experiment: experiment,
active: props.reduxAbTest.get('active'),
defaultVariationName: props.defaultVariationName
});
}
return _react2['default'].createElement(
Experiment,
{
name: props.name,
id: props.id,
selector: props.selector,
experiment: experiment,
variation: variation,
defaultVariationName: props.defaultVariationName,
dispatchActivate: props.dispatchActivate,
dispatchDeactivate: props.dispatchDeactivate,
dispatchPlay: props.dispatchPlay,
dispatchWin: props.dispatchWin
},
props.children
);
});