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