engauge
Version:
Javascript A/B Testing Library for Engauge
179 lines (154 loc) • 5.46 kB
JavaScript
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;