This repository has been archived by the owner on Jul 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30 from mozilla-rally/randomization
Refactored randomization module #24
- Loading branch information
Showing
2 changed files
with
110 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* This module enables running measurements and interventions with randomization, | ||
* such as A/B tests, multivariate tests, and randomized controlled trials. | ||
* | ||
* @module webScience.randomization | ||
*/ | ||
|
||
/** | ||
* A condition for a measurement or intervention that can be randomly selected. | ||
* @typedef {Object} Condition | ||
* @property {string} name - A name that uniquely identifies the condition within | ||
* the set of conditions. | ||
* @property {number} weight - The positive weight to give this condition when randomly | ||
* selecting a condition from a set. | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} ConditionSet | ||
* @property {string} name - A name that uniquely identifies the set of conditions. | ||
* @property {Condition[]} conditions - The conditions in the set. | ||
*/ | ||
|
||
/** | ||
* @type {Object|null} | ||
* @private | ||
* A map of condition set names to condition names. Maintaining a cache avoids | ||
* storage race conditions. The cache is an Object rather than a Map so it can | ||
* be easily stored in extension local storage. | ||
*/ | ||
let conditionCache = null; | ||
|
||
/** | ||
* @type {string} | ||
* @const | ||
* @private | ||
* A unique key for storing selected conditions in extension local storage. | ||
*/ | ||
const storageKey = "webScience.randomization.conditions"; | ||
|
||
/** | ||
* Selects a condition from a set of conditions. If a condition has previously | ||
* been selected from the set, that same condition will be returned. If not, | ||
* a condition will be randomly selected according to the provided weights. | ||
* @param {ConditionSet} conditionSet - The set of conditions. | ||
* @returns {string} - The name of the selected condition in the condition set. | ||
* @example | ||
* // on first run, returns "red" with 0.5 probability and "blue" with 0.5 probability | ||
* // on subsequent runs, returns the same value as before | ||
* randomization.selectCondition({ | ||
* name: "color", | ||
* conditions: [ | ||
* { | ||
* name: "red", | ||
* weight: 1 | ||
* }, | ||
* { | ||
* name: "blue", | ||
* weight: 1 | ||
* } | ||
* ] | ||
* }); | ||
*/ | ||
export async function selectCondition(conditionSet) { | ||
// Initialize the cache of selected conditions | ||
if(conditionCache === null) { | ||
const retrievedConditions = await browser.storage.local.get(storageKey); | ||
// Check the cache once more, to avoid a race condition | ||
if(conditionCache === null) { | ||
if(storageKey in retrievedConditions) | ||
conditionCache = retrievedConditions[storageKey]; | ||
else | ||
conditionCache = { }; | ||
} | ||
} | ||
|
||
// Try to load the selected condition from the cache | ||
if(conditionSet.name in conditionCache) | ||
return conditionCache[conditionSet.name]; | ||
|
||
// If there isn't a previously selected condition, select a condition, | ||
// save it to the cache and extension local storage, and return it | ||
let totalWeight = 0; | ||
const conditionNames = new Set(); | ||
if(!Array.isArray(conditionSet.conditions) || conditionSet.length === 0) | ||
throw "The condition set must include an array with at least one condition." | ||
for(const condition of conditionSet.conditions) { | ||
if(condition.weight <= 0) | ||
throw "Condition weights must be positive values." | ||
totalWeight += condition.weight; | ||
if(conditionNames.has(condition.name)) | ||
throw "Conditions must have unique names." | ||
conditionNames.add(condition.name); | ||
} | ||
let randomValue = Math.random(); | ||
let selectedCondition = ""; | ||
for(const condition of conditionSet.conditions) { | ||
randomValue -= (condition.weight / totalWeight); | ||
if(randomValue <= 0) { | ||
selectedCondition = condition.name; | ||
break; | ||
} | ||
} | ||
conditionCache[conditionSet.name] = selectedCondition; | ||
// No need to wait for storage to complete | ||
browser.storage.local.set({ [storageKey]: conditionCache }); | ||
return selectedCondition.repeat(1); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters