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

350 lines (297 loc) 15.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.flattenCompact = exports.middleware = exports.generateFulfilledActions = exports.initialState = exports.fulfilled = exports.win = exports.play = exports.deactivate = exports.activate = exports.setLocation = exports.setActive = exports.setAudience = exports.load = exports.reset = exports.FULFILLED = exports.WIN = exports.PLAY = exports.DEACTIVATE = exports.ACTIVATE = exports.SET_ACTIVE = exports.SET_LOCATION = exports.SET_AUDIENCE = exports.LOAD = exports.RESET = undefined; var _defineProperty2 = require('babel-runtime/helpers/defineProperty'); var _defineProperty3 = _interopRequireDefault(_defineProperty2); var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray'); var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2); var _reducers; var _immutable = require('immutable'); var _immutable2 = _interopRequireDefault(_immutable); var _flattenDeep2 = require('lodash/flattenDeep'); var _flattenDeep3 = _interopRequireDefault(_flattenDeep2); var _compact2 = require('lodash/compact'); var _compact3 = _interopRequireDefault(_compact2); var _reduxActions = require('redux-actions'); var _createCacheStore = require('../utils/create-cache-store'); var _availableExperiments = require('../utils/available-experiments'); var _availableExperiments2 = _interopRequireDefault(_availableExperiments); var _getKey = require('../utils/get-key'); var _getKey2 = _interopRequireDefault(_getKey); var _generateWinActions = require('../utils/generate-win-actions'); var _generateWinActions2 = _interopRequireDefault(_generateWinActions); var _wrapsImmutable = require('../utils/wraps-immutable'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } // // Redux Action Types // var RESET = exports.RESET = 'redux-ab-test/RESET'; var LOAD = exports.LOAD = 'redux-ab-test/LOAD'; var SET_AUDIENCE = exports.SET_AUDIENCE = 'redux-ab-test/SET_AUDIENCE'; var SET_LOCATION = exports.SET_LOCATION = 'redux-ab-test/SET_LOCATION'; var SET_ACTIVE = exports.SET_ACTIVE = 'redux-ab-test/SET_ACTIVE'; var ACTIVATE = exports.ACTIVATE = 'redux-ab-test/ACTIVATE'; var DEACTIVATE = exports.DEACTIVATE = 'redux-ab-test/DEACTIVATE'; var PLAY = exports.PLAY = 'redux-ab-test/PLAY'; var WIN = exports.WIN = 'redux-ab-test/WIN'; var FULFILLED = exports.FULFILLED = 'redux-ab-test/FULFILLED'; // // Redux Action Creators: // var reset = exports.reset = (0, _reduxActions.createAction)(RESET, function () { return _immutable2['default'].fromJS({}); }); var load = exports.load = (0, _reduxActions.createAction)(LOAD, function () { var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return _immutable2['default'].fromJS(opts); }); var setAudience = exports.setAudience = (0, _reduxActions.createAction)(SET_AUDIENCE, function () { var audience = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return _immutable2['default'].fromJS({ audience: audience }); }); var setActive = exports.setActive = (0, _reduxActions.createAction)(SET_ACTIVE, function () { var active = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return _immutable2['default'].fromJS({ active: active }); }); var setLocation = exports.setLocation = (0, _reduxActions.createAction)(SET_LOCATION, function () { var locationBeforeTransitions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return _immutable2['default'].fromJS({ locationBeforeTransitions: locationBeforeTransitions }); }); var activate = exports.activate = (0, _reduxActions.createAction)(ACTIVATE, _wrapsImmutable.immutableExperiment); var deactivate = exports.deactivate = (0, _reduxActions.createAction)(DEACTIVATE, _wrapsImmutable.immutableExperiment); var play = exports.play = (0, _reduxActions.createAction)(PLAY, _wrapsImmutable.immutableExperimentVariation); var win = exports.win = (0, _reduxActions.createAction)(WIN, function (_ref) { var experiment = _ref.experiment, variation = _ref.variation, actionType = _ref.actionType, actionPayload = _ref.actionPayload; return _immutable2['default'].fromJS({ experiment: experiment, variation: variation, actionType: actionType, actionPayload: actionPayload }); }); var fulfilled = exports.fulfilled = (0, _reduxActions.createAction)(FULFILLED, function (_ref2) { var experiment = _ref2.experiment, variation = _ref2.variation, actionType = _ref2.actionType, actionPayload = _ref2.actionPayload; return _immutable2['default'].fromJS({ experiment: experiment, variation: variation, actionType: actionType, actionPayload: actionPayload }); }); var initialState = exports.initialState = _immutable2['default'].fromJS({ experiments: [/** Array of "experiment objects" */], fulfilled: [/** Array of "experiment keys" */], availableExperiments: {/** Hash of "experiment key" => "experiment objects" */}, running: {/** "experiment id" => counter */}, active: {/** "experiment id" => "variation id" */}, winners: {/** "experiment id" => "variation id" */}, audience: {/** Any props you want to use for user/session targeting */}, route: { pathName: null, search: null, query: {} }, key_path: ['key'], fulfilled_path: ['fulfilled_action_types'], types_path: ['win_action_types'], props_path: ['componentProps'], audience_path: ['audienceProps'], persistent_path: ['persistentExperience'], single_success_path: ['singleSuccess'], // Should the experiment be removed from availableExperiments once it has been won? route_path: ['routeProps'], win_action_types: {/** Array of Redux Action Types */} }); var generateFulfilledActions = exports.generateFulfilledActions = function () { function generateFulfilledActions(winActions, state) { return winActions.map(function (winAction) { var payload = winAction.payload; // Get the action types from the experiment var experiment = payload.get('experiment'); var variation = payload.get('variation'); var actionType = payload.get('actionType'); var actionPayload = payload.get('actionPayload'); var types = flattenCompact(experiment.getIn(state.get('fulfilled_path'))); if (!types.isEmpty() && actionType && types.includes(actionType)) { return fulfilled({ experiment: experiment, variation: variation, actionType: actionType, actionPayload: actionPayload }); } return null; }).filter(function (action) { return action; }); } return generateFulfilledActions; }(); var middleware = exports.middleware = function () { function middleware(store) { return function (next) { return function (action) { // Process the input action var output = next(action); // Multi-plex the action output if we are listening for any wins var reduxAbTest = store.getState().reduxAbTest; if (reduxAbTest) { var winActions = (0, _generateWinActions2['default'])({ reduxAbTest: reduxAbTest, win: win, actionType: action.type, actionPayload: action.payload }); var fulfilledActions = generateFulfilledActions(winActions, reduxAbTest); winActions.forEach(function (action) { return next(action); }); fulfilledActions.forEach(function (action) { return next(action); }); } switch (action.type) { /** * Intercept the react-router events, this is for v4.x.x react-router integration */ case '@@router/LOCATION_CHANGE': { var _ref3 = action.payload || {}, pathname = _ref3.pathname, search = _ref3.search, query = _ref3.query; var locationBeforeTransitions = { pathname: pathname, search: search, query: query }; next(setLocation(locationBeforeTransitions)); break; } /** * Intercept the redux-router events, this is specifically for react-redux-router integration */ case '@@reduxReactRouter/routerDidChange': { var _ref4 = action.payload || {}, _ref4$location = _ref4.location, location = _ref4$location === undefined ? {} : _ref4$location; var _pathname = location.pathname, _search = location.search, _location$query = location.query, _query = _location$query === undefined ? {} : _location$query; var _locationBeforeTransitions = { pathname: _pathname, search: _search, query: _query }; next(setLocation(_locationBeforeTransitions)); break; } } // Return the original action's output return output; }; }; } return middleware; }(); var flattenCompact = exports.flattenCompact = function () { function flattenCompact(list) { return _immutable2['default'].List((0, _compact3['default'])((0, _flattenDeep3['default'])(_immutable2['default'].fromJS([list]).toJS()))); } return flattenCompact; }(); var computeAvailableExperiments = function () { function computeAvailableExperiments(state) { return state.set('availableExperiments', (0, _availableExperiments2['default'])({ experiments: state.get('experiments'), fulfilled: state.get('fulfilled'), key_path: state.get('key_path'), active: state.get('active'), winners: state.get('winners'), persistent_path: state.get('persistent_path'), audience_path: state.get('audience_path'), audience: state.get('audience'), route: state.get('route'), route_path: state.get('route_path'), single_success_path: state.get('single_success_path') })); } return computeAvailableExperiments; }(); var reducers = (_reducers = {}, (0, _defineProperty3['default'])(_reducers, RESET, function () { _createCacheStore.cacheStore.clear(); return initialState; }), (0, _defineProperty3['default'])(_reducers, LOAD, function (state, _ref5) { var payload = _ref5.payload; var key_path = flattenCompact(payload.get('key_path', state.get('key_path'))); var fulfilled_path = flattenCompact(payload.get('fulfilled_path', state.get('fulfilled_path'))); var types_path = flattenCompact(payload.get('types_path', state.get('types_path'))); var props_path = flattenCompact(payload.get('props_path', state.get('props_path'))); var audience_path = flattenCompact(payload.get('audience_path', state.get('audience_path'))); var persistent_path = flattenCompact(payload.get('persistent_path', state.get('persistent_path'))); var single_success_path = flattenCompact(payload.get('single_success_path', state.get('single_success_path'))); var route_path = flattenCompact(payload.get('route_path', state.get('route_path'))); var experiments = payload.has('experiments') ? payload.get('experiments') : state.get('experiments'); var active = payload.has('active') ? payload.get('active') : state.get('active'); var winners = payload.has('winners') ? payload.get('winners') : state.get('winners'); var audience = payload.has('audience') ? payload.get('audience') : state.get('audience'); var route = payload.has('route') ? payload.get('route') : state.get('route'); var fulfilled = payload.has('fulfilled') ? payload.get('fulfilled') : state.get('fulfilled'); var win_action_types = experiments.reduce(function (map, experiment) { var key = (0, _getKey2['default'])(experiment); var types = flattenCompact([experiment.getIn(types_path), experiment.getIn(fulfilled_path)]); types.forEach(function (type) { var list = map[type] || []; list.push(key); map[type] = list; }); return map; }, {}); return computeAvailableExperiments(initialState.merge({ experiments: experiments, fulfilled: fulfilled, fulfilled_path: fulfilled_path, audience: audience, route: route, active: active, winners: winners, key_path: key_path, types_path: types_path, props_path: props_path, persistent_path: persistent_path, audience_path: audience_path, route_path: route_path, win_action_types: win_action_types, single_success_path: single_success_path })); }), (0, _defineProperty3['default'])(_reducers, SET_AUDIENCE, function (state, _ref6) { var payload = _ref6.payload; var audience = payload.get('audience'); return computeAvailableExperiments(state.set('audience', audience)); }), (0, _defineProperty3['default'])(_reducers, SET_LOCATION, function (state, _ref7) { var payload = _ref7.payload; var pathName = payload.getIn(['locationBeforeTransitions', 'pathname']); var search = payload.getIn(['locationBeforeTransitions', 'search']); var query = payload.getIn(['locationBeforeTransitions', 'query'], _immutable2['default'].Map()); var route = _immutable2['default'].Map({ pathName: pathName, search: search, query: query }); return computeAvailableExperiments(state.set('route', route)); }), (0, _defineProperty3['default'])(_reducers, SET_ACTIVE, function (state, _ref8) { var payload = _ref8.payload; return computeAvailableExperiments(state.set('active', payload.get('active'))); }), (0, _defineProperty3['default'])(_reducers, ACTIVATE, function (state, _ref9) { var payload = _ref9.payload; var experimentKey = (0, _getKey2['default'])(payload.get('experiment')); var counter = (state.get('running').get(experimentKey) || 0) + 1; var running = state.get('running').set(experimentKey, counter); return computeAvailableExperiments(state.set('running', running)); }), (0, _defineProperty3['default'])(_reducers, DEACTIVATE, function (state, _ref10) { var payload = _ref10.payload; var experimentKey = (0, _getKey2['default'])(payload.get('experiment')); var counter = (state.get('running').get(experimentKey) || 0) - 1; var running = counter <= 0 ? state.get('running')['delete'](experimentKey) : state.get('running').set(experimentKey, counter); return computeAvailableExperiments(state.set('running', running)); }), (0, _defineProperty3['default'])(_reducers, PLAY, function (state, _ref11) { var payload = _ref11.payload; var experimentKey = (0, _getKey2['default'])(payload.get('experiment')); var variationKey = (0, _getKey2['default'])(payload.get('variation')); var active = state.get('active').set(experimentKey, variationKey); _createCacheStore.cacheStore.removeItem(experimentKey); return computeAvailableExperiments(state.set('active', active)); }), (0, _defineProperty3['default'])(_reducers, WIN, function (state, _ref12) { var payload = _ref12.payload; var experimentKey = (0, _getKey2['default'])(payload.get('experiment')); var variationKey = (0, _getKey2['default'])(payload.get('variation')); var winners = state.get('winners').set(experimentKey, variationKey); var fulfilled = state.get('fulfilled'); // Get the action types from the experiment var actionType = payload.get('actionType'); var experiment = payload.get('experiment'); var types = flattenCompact(experiment.getIn(state.get('fulfilled_path'))); if (!types.isEmpty() && actionType && types.includes(actionType)) { fulfilled = _immutable2['default'].List([].concat((0, _toConsumableArray3['default'])(_immutable2['default'].fromJS(fulfilled).toJS()), [experimentKey])); } return computeAvailableExperiments(state.set('winners', winners).set('fulfilled', fulfilled)); }), _reducers); exports['default'] = (0, _reduxActions.handleActions)(reducers, initialState);