UNPKG

@croct/rule-engine-experiments

Version:

A rule engine extension for A/B and multivariate testing.

241 lines (240 loc) 9.88 kB
import { __read, __spread, __values } from "tslib"; import { formatCause } from '@croct/plug/sdk/validation'; import { And, Contains, Variable } from '@croct/plug-rule-engine/predicate'; import { propertiesSchema } from './schemas'; var ExperimentsExtension = /** @class */ (function () { function ExperimentsExtension(experiments, tracker, browserStorage, tagStorage, logger) { this.experiments = experiments; this.tracker = tracker; this.browserStorage = browserStorage; this.tabStorage = tagStorage; this.logger = logger; } ExperimentsExtension.prototype.getVariables = function () { var e_1, _a; var _this = this; var variables = {}; var _loop_1 = function (testId) { variables[testId] = function () { return Promise.resolve(_this.assignGroup(testId)); }; }; try { for (var _b = __values(Object.keys(this.experiments)), _c = _b.next(); !_c.done; _c = _b.next()) { var testId = _c.value; _loop_1(testId); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } return variables; }; ExperimentsExtension.prototype.getPredicate = function (_a) { var name = _a.name, experiment = _a.properties.experiment; if (experiment === undefined) { return null; } try { propertiesSchema.validate(experiment); } catch (error) { this.logger.error("Invalid experiment properties specified for rule \"" + name + "\": " + formatCause(error)); return null; } var _b = experiment, testId = _b.testId, groupId = _b.groupId; return this.getGroupCondition(testId, groupId); }; ExperimentsExtension.prototype.getGroupCondition = function (testId, groupId) { var groupCondition = new Contains(testId, groupId); var audience = this.experiments[testId].audience; if (audience === undefined) { return groupCondition; } return new And(groupCondition, new Variable(audience)); }; ExperimentsExtension.prototype.assignGroup = function (testId) { var experiment = this.experiments[testId]; switch (experiment.type) { case 'ab': { var group = this.assignAbGroup(testId, experiment); if (group !== null) { return [group]; } return []; } case 'multivariate': { var groups = this.assignMultivariateGroups(testId, experiment); return __spread([groups.join('|')], groups); } } }; ExperimentsExtension.prototype.assignAbGroup = function (testId, experiment) { var previousGroupId = null; var serializedGroupId = this.tabStorage.getItem(testId); if (serializedGroupId !== null) { previousGroupId = deserializeGroup(serializedGroupId); if (previousGroupId !== null) { return previousGroupId === '' ? null : previousGroupId; } } serializedGroupId = this.browserStorage.getItem(testId); if (serializedGroupId !== null) { previousGroupId = deserializeGroup(serializedGroupId); if (previousGroupId !== null) { this.tabStorage.setItem(testId, JSON.stringify(previousGroupId)); if (previousGroupId !== '') { this.trackAssignedAbGroup(testId, previousGroupId); } return previousGroupId === '' ? null : previousGroupId; } } this.logger.debug("No group previously assigned to A/B test \"" + testId + "\""); var groupId = this.selectAbGroup(experiment); serializedGroupId = JSON.stringify(groupId !== null && groupId !== void 0 ? groupId : ''); this.tabStorage.setItem(testId, serializedGroupId); this.browserStorage.setItem(testId, serializedGroupId); if (groupId !== null) { this.trackAssignedAbGroup(testId, groupId); } this.logger.debug(groupId === null ? "Traffic ineligible for A/B test \"" + testId + "\"." : "Group \"" + groupId + "\" assigned to A/B test \"" + testId + "\"."); return groupId; }; ExperimentsExtension.prototype.trackAssignedAbGroup = function (testId, groupId) { var _this = this; var event = { testId: testId, groupId: groupId, }; this.tracker.track('testGroupAssigned', event).catch(function () { _this.logger.error("Failed to track group assignment \"" + event.groupId + "\" for test \"" + event.testId + "\"."); }); }; ExperimentsExtension.prototype.selectAbGroup = function (_a) { var e_2, _b; var traffic = _a.traffic, groups = _a.groups; if (traffic !== undefined && Math.random() >= traffic) { return null; } if (Array.isArray(groups)) { // Evenly split groups return groups[Math.floor(Math.random() * groups.length)]; } // Weighted groups var sum = 0; var choices = []; try { for (var _c = __values(Object.entries(groups)), _d = _c.next(); !_d.done; _d = _c.next()) { var _e = __read(_d.value, 2), name_1 = _e[0], weight = _e[1].weight; sum += weight * 100; choices.push({ group: name_1, weight: weight * 100 }); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_d && !_d.done && (_b = _c.return)) _b.call(_c); } finally { if (e_2) throw e_2.error; } } var random = Math.floor(Math.random() * sum); for (var index = 0; index < choices.length - 1; index++) { random -= choices[index].weight; if (random < 0) { return choices[index].group; } } return choices[choices.length - 1].group; }; ExperimentsExtension.prototype.assignMultivariateGroups = function (testId, experiment) { var previousGroupIds = null; var serializedGroupIds = this.tabStorage.getItem(testId); if (serializedGroupIds !== null) { previousGroupIds = deserializeGroups(serializedGroupIds); if (previousGroupIds !== null) { return previousGroupIds; } } serializedGroupIds = this.browserStorage.getItem(testId); if (serializedGroupIds !== null) { previousGroupIds = deserializeGroups(serializedGroupIds); if (previousGroupIds !== null) { this.tabStorage.setItem(testId, JSON.stringify(previousGroupIds)); if (previousGroupIds.length > 0) { this.trackAssignedMultivariateGroup(testId, previousGroupIds); } return previousGroupIds; } } this.logger.debug("No group previously assigned to multivariate test \"" + testId + "\""); var groupIds = this.selectMultivariateGroups(experiment); serializedGroupIds = JSON.stringify(groupIds); this.tabStorage.setItem(testId, serializedGroupIds); this.browserStorage.setItem(testId, serializedGroupIds); if (groupIds.length > 0) { this.trackAssignedMultivariateGroup(testId, groupIds); } this.logger.debug(groupIds.length === 0 ? "Traffic ineligible for multivariate test \"" + testId + "\"." : "Groups [\"" + groupIds.join('", "') + "\"] assigned to multivariate test \"" + testId + "\"."); return groupIds; }; ExperimentsExtension.prototype.trackAssignedMultivariateGroup = function (testId, groupIds) { var _this = this; var event = { testId: testId, groupId: groupIds.join('|'), }; this.tracker.track('testGroupAssigned', event).catch(function () { _this.logger.error("Failed to track group assignment \"" + event.groupId + "\" for test \"" + event.testId + "\"."); }); }; ExperimentsExtension.prototype.selectMultivariateGroups = function (_a) { var traffic = _a.traffic, groups = _a.groups; if (traffic !== undefined && Math.random() >= traffic) { return []; } var combinationCount = groups.reduce(function (count, group) { return count * group.length; }, 1); var combinationIndex = Math.floor(Math.random() * combinationCount); var combination = []; for (var index = groups.length - 1; index >= 0; index--) { var currentGroupLength = groups[index].length; var currentVariation = groups[index][combinationIndex % currentGroupLength]; combination.unshift(currentVariation); combinationIndex = Math.floor(combinationIndex / currentGroupLength); } return combination; }; return ExperimentsExtension; }()); export default ExperimentsExtension; function deserializeGroup(json) { var value; try { value = JSON.parse(json); } catch (_a) { return null; } if (typeof value === 'string') { return value; } return null; } function deserializeGroups(json) { var value; try { value = JSON.parse(json); } catch (_a) { return null; } if (!Array.isArray(value)) { return null; } return value.filter(function (element) { return typeof element === 'string'; }); }