Source: randomization.js

  1. /**
  2. * This module enables running measurements and interventions with randomization,
  3. * such as A/B tests, multivariate tests, and randomized controlled trials.
  4. *
  5. * @module randomization
  6. */
  7. import * as permissions from "./permissions.js";
  8. /**
  9. * A condition for a measurement or intervention that can be randomly selected.
  10. * @typedef {Object} Condition
  11. * @property {string} name - A name that uniquely identifies the condition within
  12. * the set of conditions.
  13. * @property {number} weight - The positive weight to give this condition when randomly
  14. * selecting a condition from a set.
  15. */
  16. /**
  17. * @typedef {Object} ConditionSet
  18. * @property {string} name - A name that uniquely identifies the set of conditions.
  19. * @property {Condition[]} conditions - The conditions in the set.
  20. */
  21. /**
  22. * A map of condition set names to condition names. Maintaining a cache avoids
  23. * storage race conditions. The cache is an Object rather than a Map so it can
  24. * be easily stored in extension local storage.
  25. * @type {Object|null}
  26. * @private
  27. */
  28. let conditionCache = null;
  29. /**
  30. * A unique key for storing selected conditions in extension local storage.
  31. * @constant {string}
  32. * @private
  33. */
  34. const storageKey = "webScience.randomization.conditions";
  35. /**
  36. * Selects a condition from a set of conditions. If a condition has previously
  37. * been selected from the set, that same condition will be returned. If not,
  38. * a condition will be randomly selected according to the provided weights.
  39. * @param {ConditionSet} conditionSet - The set of conditions.
  40. * @returns {string} - The name of the selected condition in the condition set.
  41. * @example
  42. * // on first run, returns "red" with 0.5 probability and "blue" with 0.5 probability
  43. * // on subsequent runs, returns the same value as before
  44. * randomization.selectCondition({
  45. * name: "color",
  46. * conditions: [
  47. * {
  48. * name: "red",
  49. * weight: 1
  50. * },
  51. * {
  52. * name: "blue",
  53. * weight: 1
  54. * }
  55. * ]
  56. * });
  57. */
  58. export async function selectCondition(conditionSet) {
  59. permissions.check({
  60. module: "webScience.linkExposure",
  61. requiredPermissions: [ "storage" ],
  62. suggestedPermissions: [ "unlimitedStorage" ]
  63. });
  64. // Initialize the cache of selected conditions
  65. if(conditionCache === null) {
  66. const retrievedConditions = await browser.storage.local.get(storageKey);
  67. // Check the cache once more, to avoid a race condition
  68. if(conditionCache === null) {
  69. if(storageKey in retrievedConditions)
  70. conditionCache = retrievedConditions[storageKey];
  71. else
  72. conditionCache = { };
  73. }
  74. }
  75. // Try to load the selected condition from the cache
  76. if(conditionSet.name in conditionCache)
  77. return conditionCache[conditionSet.name];
  78. // If there isn't a previously selected condition, select a condition,
  79. // save it to the cache and extension local storage, and return it
  80. let totalWeight = 0;
  81. const conditionNames = new Set();
  82. if(!Array.isArray(conditionSet.conditions) || conditionSet.length === 0)
  83. throw "The condition set must include an array with at least one condition."
  84. for(const condition of conditionSet.conditions) {
  85. if(condition.weight <= 0)
  86. throw "Condition weights must be positive values."
  87. totalWeight += condition.weight;
  88. if(conditionNames.has(condition.name))
  89. throw "Conditions must have unique names."
  90. conditionNames.add(condition.name);
  91. }
  92. let randomValue = Math.random();
  93. let selectedCondition = "";
  94. for(const condition of conditionSet.conditions) {
  95. randomValue -= (condition.weight / totalWeight);
  96. if(randomValue <= 0) {
  97. selectedCondition = condition.name;
  98. break;
  99. }
  100. }
  101. conditionCache[conditionSet.name] = selectedCondition;
  102. // No need to wait for storage to complete
  103. browser.storage.local.set({ [storageKey]: conditionCache });
  104. return selectedCondition.repeat(1);
  105. }