UNPKG

engauge

Version:

Javascript A/B Testing Library for Engauge

179 lines (154 loc) 5.46 kB
import utils from './utils'; import Adapters from './adapters'; import options from './options'; if (typeof process !== 'undefined') { const window = undefined; } class Engauge { constructor(key) { if (!key) { console.warn("Warning: you must provide an API key to Engauge e.g. var engauge = new Engauge(MY_ENGAUGE_API_KEY)") return; } this.options = options; this.options.key = key; this.hasActive = false; this.failedConnection = false; this.options.active = []; this.utils = utils; } experiment(options) { return new Experiment(options, this.options.key); } goal(name) { this.options.goal_name = name; return new Goal(this.options); } } const Experiment = class Experiment { constructor(opts, key) { //utils.log(opts, key); this.options = opts; this._options = { name: null, variants: null, sample: 1.0, trigger: () => { return true; }, tracking_adapter: new Adapters.GimelAdapter(key), storage_adapter: new Adapters.LocalStorageAdapter(key) } this.options = utils.assign(this._options, this.options); this._validate(); this.name = this.options.name; this.variants = this.options.variants; utils.log(this.variants, this.options); this.variant_names = utils.keys(this.variants); this._run(); } run() { const variant = this.get_stored_variant(); //utils.log(`running with options: ${JSON.stringify(this.options)}`, variant); if (variant) { // a variant was already chosen. activate it utils.log(`${variant} active`); return this.activate_variant(variant); } else { return this.conditionally_activate_variant(); } } _runWithControl() { //utils.log(`running control only with options: ${JSON.stringify(this.options)}`); for (var variant in this.variants) { if (variant.control) { return this.activate_variant(variant); } }; } _run() { return this.run(); } activate_variant(variant) { this.__guard__(this.variants[variant], x => x.activate(this)); utils.log('setting variant in storage', this.options.name); return this.storage().set(`${this.options.name}:variant`, variant); } __guard__(value, transform) { return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; } // if experiment conditions match, pick and activate a variant, track experiment start conditionally_activate_variant() { if (!this.options.trigger()) { return; } if (!this.in_sample()) { return; } utils.log('in sample'); let variant = this.pick_variant(); this.tracking().experiment_start(this.options.name, variant); return this.activate_variant(variant); } goal_complete(goal_name, props) { if (props.unique && !!this.storage().get(`${this.options.name}:${goal_name}`)) { return; } let variant = this.get_stored_variant(); if (!variant) { return; } if (props.unique) { this.storage().set(`${this.options.name}:${goal_name}`, 'true'); } utils.log(`experiment: ${this.options.name} variant:${variant} goal:${goal_name} complete`); return this.tracking().goal_complete(this.options.name, variant, goal_name); } goal_complete_custom(goal_name, variant_name, props) { let variant = variant_name; if (!variant) { return; } if (props.unique) { this.storage().set(`${this.options.name}:${goal_name}`, 'true'); } utils.log(`experiment: ${this.options.name} variant:${variant} goal:${goal_name} complete`); return this.tracking().goal_complete(this.options.name, variant, goal_name); } get_stored_variant() { return this.storage().get(`${this.options.name}:variant`); } pick_variant() { let partitions = 1.0 / this.variant_names.length; let chosen_partition = Math.floor(Math.random() / partitions); let variant = this.variant_names[chosen_partition]; utils.log(`${variant} picked`); return variant; } in_sample() { let active = this.storage().get(`${this.options.name}:in_sample`); if (typeof active !== 'undefined') { return active; } active = Math.random() <= this.options.sample; this.storage().set(`${this.options.name}:in_sample`, active); return active; } add_goal(goal) { return goal.add_experiment(this); } add_goals(goals) { return goals.map((goal) => this.add_goal(goal)); } storage() { return this.options.storage_adapter; } tracking() { return this.options.tracking_adapter; } _validate() { console.info(this.options.trigger, typeof this.options.trigger) if (this.options.name === null) { throw 'an experiment name must be specified'; } if (this.options.variants === null) { throw 'variants must be provided'; } if (typeof this.options.trigger !== 'function') { throw 'trigger must be a function'; } return true; } }; const Goal = class Goal { constructor(props) { utils.log(props); this.name = props.goal_name; this.props = props; utils.assign(this.props, {unique: true}); this.experiments = []; } add_experiment(experiment) { return this.experiments.push(experiment); } add_experiments(experiments) { return experiments.map((experiment) => this.add_experiment(experiment)); } complete() { return this.experiments.map((experiment) => { experiment.goal_complete(this.name, this.props); }); } }; export default Engauge;