UNPKG

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