Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
feature(addon): #1511 persist active experiments in prefs.
Browse files Browse the repository at this point in the history
  • Loading branch information
Marina Samuel committed Oct 20, 2016
1 parent 5046d12 commit a23e2b1
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 94 deletions.
2 changes: 1 addition & 1 deletion addon/ActivityStreams.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ function ActivityStreams(metadataStore, tabTracker, telemetrySender, options = {
this._newTabURL = `${this.options.pageURL}#/`;
Services.prefs.setIntPref("places.favicons.optimizeToDimension", 64);
this._experimentProvider = new ExperimentProvider(
options.clientID,
options.experiments,
options.rng
);
Expand Down Expand Up @@ -834,6 +833,7 @@ ActivityStreams.prototype = {
case "disable":
case "uninstall":
this._tabTracker.handleUserEvent({event: reason});
this._experimentProvider.clearPrefs();
this._unsetHomePage();
defaultUnload();
break;
Expand Down
117 changes: 83 additions & 34 deletions addon/ExperimentProvider.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,101 @@
const seedrandom = require("common/vendor")("seedrandom");
const simplePrefs = require("sdk/simple-prefs");
const OVERRIDE_PREF = "experimentOverrides";
const prefService = require("sdk/preferences/service");
const {PrefsTarget} = require("sdk/preferences/event-target");
const ss = require("sdk/simple-storage");
const {preferencesBranch} = require("sdk/self");
const PREF_PREFIX = `extensions.${preferencesBranch}.`;

exports.OVERRIDE_PREF = OVERRIDE_PREF;
exports.ExperimentProvider = class ExperimentProvider {
constructor(clientID, experiments = require("../experiments.json"), rng) {
this._clientID = clientID;
constructor(experiments = require("../experiments.json"), rng) {
this._experiments = experiments;
this._rng = rng || seedrandom(clientID);
this._rng = rng || Math.random;
this._data = {};
this._experimentId = null;
this._target = PrefsTarget();
}

init() {
this.setValues();
this._onPrefChange = () => this.setValues();
simplePrefs.on(OVERRIDE_PREF, this._onPrefChange);
this._onPrefChange = prefName => this.overrideExperimentPrefs(prefName);

Object.keys(this._experiments).forEach(experimentName => {
this._target.on(PREF_PREFIX + experimentName, this._onPrefChange);
Object.defineProperty(this._data, experimentName, {
get() {
return prefService.get(PREF_PREFIX + experimentName);
}
});
});
}

overrideExperimentPrefs(prefName) {
ss.storage.overrideExperimentProvider = true;
this._experimentId = null;
}

setValues() {
// Load override pref, if defined
const overrides = simplePrefs.prefs[OVERRIDE_PREF] && simplePrefs.prefs[OVERRIDE_PREF].split(/,?\s+/);
if (ss.storage.overrideExperimentProvider) {
console.log(`The following experiments were turned on via overrides:\n`); // eslint-disable-line no-console
for (let experimentName in this._experiments) {
if (prefService.has(PREF_PREFIX + experimentName)) {
console.log(`- ${experimentName} - \n`); // eslint-disable-line no-console
}
}
return;
}

const randomNumber = this._rng();
let floor = 0;
let inExperiment;

Object.keys(this._experiments).forEach(key => {
let inExperiment;
const experiment = this._experiments[key];
if (!overrides && experiment.active === false) {
return;
}
const {variant, control} = experiment;

// If an override pref is defined, use it to determine which
// experiment values are used. If not, just use our random number generator.
// Note that no _experimentId is set for manually overridden prefs.
if (overrides) {
inExperiment = overrides.includes(key);
} else {
const ceiling = variant.threshold + floor;
if (ceiling > 1) {
throw new Error("Your variant cohort sizes should add up to less than 1.");
}
inExperiment = randomNumber >= floor && randomNumber < ceiling;
if (inExperiment) {
if (prefService.get(PREF_PREFIX + key) === variant.value) {
if (experiment.active) {
// If the user is already part of an active experiment, set the experiment id.
this._experimentId = variant.id;
} else {
// If the user is part of an inactive experiment,
// reset that experiment's pref.
prefService.set(PREF_PREFIX + key, control.value);
this._experimentId = null;
}
floor = ceiling;
}
this._data[key] = inExperiment ? variant.value : control.value;
});

if (overrides) {
console.log(`The following experiments were turned on via overrides:\n - ${overrides.join("\n - ")}`); // eslint-disable-line no-console
}
Object.keys(this._experiments).forEach(key => {
const experiment = this._experiments[key];
const {variant, control} = experiment;
const ceiling = variant.threshold + floor;

// If the experiment is not new or not active you will not be assigned to it.
if (prefService.has(PREF_PREFIX + key) || !experiment.active) {
return;
}

// If the experiment pref is undefined, it's a new experiment. Start
// by assuming the user will not be in it.
prefService.set(PREF_PREFIX + key, control.value);

if (ceiling > 1) {
throw new Error("Your variant cohort sizes should add up to less than 1.");
}

// If you're already in an experiment, you can't be in another one.
if (this._experimentId) {
return;
}

// If a user is in no experiments and there are new, active experiments,
// randomly assign them to a variant (or control)
inExperiment = randomNumber >= floor && randomNumber < ceiling;
if (inExperiment) {
this._experimentId = variant.id;
prefService.set(PREF_PREFIX + key, variant.value);
}
floor = ceiling;
});
}

// This is an object representing all experiments
Expand All @@ -69,8 +110,16 @@ exports.ExperimentProvider = class ExperimentProvider {
}

destroy() {
simplePrefs.removeListener(OVERRIDE_PREF, this._onPrefChange);
this._data = {};
this._experimentId = null;
Object.keys(this._experiments).forEach(experimentName => {
this._target.removeListener(PREF_PREFIX + experimentName, this._onPrefChange);
});
}

clearPrefs() {
Object.keys(this._experiments).forEach(experimentName => {
prefService.reset(PREF_PREFIX + experimentName);
});
ss.storage.overrideExperimentProvider = false;
}
};
1 change: 0 additions & 1 deletion common/vendor-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ if (typeof platform_require !== "undefined") {
const vendorModules = {
"redux": require("redux"),
"redux-thunk": require("redux-thunk"),
"seedrandom": require("seedrandom"),
"url-parse": require("url-parse"),
"PageMetadataParser": require("page-metadata-parser"),
"redux-watch": require("redux-watch"),
Expand Down
Loading

0 comments on commit a23e2b1

Please sign in to comment.