Bug 1716736 - Add schema validation for experiment enrollments in tests r=k88hudson
☠☠ backed out by 0c272222c17b ☠ ☠
authorAndrei Oprea <andrei.br92@gmail.com>
Sun, 25 Jul 2021 21:01:16 +0000
changeset 586589 e5cfc59f90639430e04b5aa9755309191553fc10
parent 586588 f02e38dccb1cff6b44025db642e4b4000d6327f2
child 586590 0c272222c17b3edd7190a24d7171c51eb2f009ba
child 586591 03a42c4949e0e874eef35c52f23a80f7c90a98aa
push id38642
push userarchaeopteryx@coole-files.de
push dateMon, 26 Jul 2021 09:34:30 +0000
treeherdermozilla-central@0c272222c17b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1716736
milestone92.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1716736 - Add schema validation for experiment enrollments in tests r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D118367
browser/base/content/test/about/browser_aboutSupport.js
browser/components/shell/test/browser_doesAppNeedPin.js
browser/components/shell/test/browser_setDefaultBrowser.js
browser/components/tests/browser/browser_browserGlue_upgradeDialog.js
toolkit/components/nimbus/ExperimentAPI.jsm
toolkit/components/nimbus/moz.build
toolkit/components/nimbus/schemas/ExperimentFeatureRemote.schema.json
toolkit/components/nimbus/schemas/NimbusEnrollment.schema.json
toolkit/components/nimbus/test/NimbusTestUtils.jsm
toolkit/components/nimbus/test/browser/browser_experiment_evaluate_jexl.js
toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_init.js
toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_remote_defaults.js
toolkit/components/nimbus/test/browser/head.js
toolkit/components/nimbus/test/unit/head.js
toolkit/components/nimbus/test/unit/test_ExperimentAPI.js
toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature.js
toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getAllVariables.js
toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getValue.js
toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getVariable.js
toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js
toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js
toolkit/components/nimbus/test/unit/test_ExperimentManager_lifecycle.js
toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
toolkit/components/nimbus/test/unit/test_ExperimentStore.js
toolkit/components/nimbus/test/unit/test_FeatureManifest.js
toolkit/components/nimbus/test/unit/test_SharedDataMap.js
--- a/browser/base/content/test/about/browser_aboutSupport.js
+++ b/browser/base/content/test/about/browser_aboutSupport.js
@@ -106,18 +106,18 @@ add_task(async function test_nimbus_expe
 });
 
 add_task(async function test_remote_configuration() {
   await ExperimentAPI.ready();
   await ExperimentFakes.remoteDefaultsHelper({
     feature: NimbusFeatures.aboutwelcome,
     configuration: {
       slug: "about:studies-configuration-slug",
-      enabled: true,
-      variables: {},
+      variables: { enabled: true },
+      targeting: "true",
     },
   });
 
   await BrowserTestUtils.withNewTab(
     { gBrowser, url: "about:support" },
     async function(browser) {
       let featureId = await SpecialPowers.spawn(browser, [], async function() {
         await ContentTaskUtils.waitForCondition(
--- a/browser/components/shell/test/browser_doesAppNeedPin.js
+++ b/browser/components/shell/test/browser_doesAppNeedPin.js
@@ -21,35 +21,36 @@ add_task(async function default_need() {
 add_task(async function remote_disable() {
   if (defaultValue === false) {
     info("Default pin already false, so nothing to test");
     return;
   }
 
   await ExperimentFakes.remoteDefaultsHelper({
     feature: NimbusFeatures.shellService,
-    configuration: { variables: { disablePin: true } },
+    configuration: {
+      slug: "shellService_remoteDisable",
+      variables: { disablePin: true, enabled: true },
+      targeting: "true",
+    },
   });
 
   Assert.equal(
     await ShellService.doesAppNeedPin(),
     false,
     "Pinning disabled via nimbus"
   );
 });
 
 add_task(async function restore_default() {
   if (defaultValue === undefined) {
     info("No default pin value set, so nothing to test");
     return;
   }
 
-  await ExperimentFakes.remoteDefaultsHelper({
-    feature: NimbusFeatures.shellService,
-    configuration: {},
-  });
+  ExperimentAPI._store._deleteForTests("shellService");
 
   Assert.equal(
     await ShellService.doesAppNeedPin(),
     defaultValue,
     "Pinning restored to original"
   );
 });
--- a/browser/components/shell/test/browser_setDefaultBrowser.js
+++ b/browser/components/shell/test/browser_setDefaultBrowser.js
@@ -43,17 +43,22 @@ add_task(async function remote_disable()
     info("Default behavior already not user choice, so nothing to test");
     return;
   }
 
   userChoiceStub.resetHistory();
   setDefaultStub.resetHistory();
   await ExperimentFakes.remoteDefaultsHelper({
     feature: NimbusFeatures.shellService,
-    configuration: { variables: { setDefaultBrowserUserChoice: false } },
+    configuration: {
+      variables: {
+        setDefaultBrowserUserChoice: false,
+        enabled: true,
+      },
+    },
   });
 
   ShellService.setDefaultBrowser();
 
   Assert.ok(
     userChoiceStub.notCalled,
     "Set default with user choice disabled via nimbus"
   );
@@ -63,20 +68,17 @@ add_task(async function remote_disable()
 add_task(async function restore_default() {
   if (defaultUserChoice === undefined) {
     info("No default user choice behavior set, so nothing to test");
     return;
   }
 
   userChoiceStub.resetHistory();
   setDefaultStub.resetHistory();
-  await ExperimentFakes.remoteDefaultsHelper({
-    feature: NimbusFeatures.shellService,
-    configuration: {},
-  });
+  ExperimentAPI._store._deleteForTests("shellService");
 
   ShellService.setDefaultBrowser();
 
   Assert.equal(
     userChoiceStub.called,
     defaultUserChoice,
     "Set default with user choice restored to original"
   );
--- a/browser/components/tests/browser/browser_browserGlue_upgradeDialog.js
+++ b/browser/components/tests/browser/browser_browserGlue_upgradeDialog.js
@@ -355,17 +355,21 @@ add_task(async function not_major_upgrad
     "not-major",
   ]);
 });
 
 add_task(async function remote_disabled() {
   await ExperimentAPI.ready();
   await ExperimentFakes.remoteDefaultsHelper({
     feature: NimbusFeatures.upgradeDialog,
-    configuration: { enabled: false, variables: {} },
+    configuration: {
+      slug: "upgradeDialog_remoteDisabled",
+      variables: { enabled: false },
+      targeting: "true",
+    },
   });
 
   // Simulate starting from a previous version.
   await SpecialPowers.pushPrefEnv({
     set: [["browser.startup.homepage_override.mstone", "88.0"]],
   });
   Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs;
 
@@ -375,17 +379,21 @@ add_task(async function remote_disabled(
     "trigger",
     "reason",
     "disabled",
   ]);
 
   // Re-enable back
   await ExperimentFakes.remoteDefaultsHelper({
     feature: NimbusFeatures.upgradeDialog,
-    configuration: { enabled: true, variables: {} },
+    configuration: {
+      slug: "upgradeDialog_remoteEnabled",
+      variables: { enabled: true },
+      targeting: "true",
+    },
   });
 });
 
 add_task(async function enterprise_disabled() {
   const defaultPrefs = Services.prefs.getDefaultBranch("");
   const pref = "browser.aboutwelcome.enabled";
   const orig = defaultPrefs.getBoolPref(pref, true);
   defaultPrefs.setBoolPref(pref, false);
--- a/toolkit/components/nimbus/ExperimentAPI.jsm
+++ b/toolkit/components/nimbus/ExperimentAPI.jsm
@@ -178,28 +178,26 @@ const ExperimentAPI = {
    * @returns {void}
    */
   on(eventName, options, callback) {
     if (!options) {
       throw new Error("Please include an experiment slug or featureId");
     }
     let fullEventName = `${eventName}:${options.slug || options.featureId}`;
 
-    // The update event will always fire after the event listener is added, either
-    // immediately if it is already ready, or on ready
-    this._store.ready().then(() => {
+    if (this._store._isReady) {
       let experiment = this.getExperiment(options);
       // Only if we have an experiment that matches what the caller requested
       if (experiment) {
         // If the store already has the experiment in the store then we should
         // notify. This covers the startup scenario or cases where listeners
         // are attached later than the `update` events.
         callback(fullEventName, experiment);
       }
-    });
+    }
 
     this._store.on(fullEventName, callback);
   },
 
   /**
    * Deregisters an event listener.
    * @param {string} eventName
    * @param {function} callback
--- a/toolkit/components/nimbus/moz.build
+++ b/toolkit/components/nimbus/moz.build
@@ -25,15 +25,16 @@ BROWSER_CHROME_MANIFESTS += [
 
 XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
 
 SPHINX_TREES["docs"] = "docs"
 
 TESTING_JS_MODULES += [
     "schemas/ExperimentFeatureManifest.schema.json",
     "schemas/ExperimentFeatureRemote.schema.json",
+    "schemas/NimbusEnrollment.schema.json",
     "schemas/NimbusExperiment.schema.json",
     "test/NimbusTestUtils.jsm",
 ]
 
 FINAL_LIBRARY = "xul"
 
 JAR_MANIFESTS += ["jar.mn"]
--- a/toolkit/components/nimbus/schemas/ExperimentFeatureRemote.schema.json
+++ b/toolkit/components/nimbus/schemas/ExperimentFeatureRemote.schema.json
@@ -74,11 +74,79 @@
             },
             "required": ["variables", "targeting", "bucketConfig", "slug"],
             "additionalProperties": false
           }
         }
       },
       "required": ["id", "configurations"],
       "additionalProperties": false
+    },
+    "RemoteFeatureConfiguration": {
+      "type": "object",
+      "properties": {
+        "slug": {
+          "type": "string",
+          "description": "Configuration identifier that will be included in Telemetry."
+        },
+        "isEarlyStartup": {
+          "type": "boolean",
+          "description": "If the feature values should be cached in prefs for fast early startup."
+        },
+        "variables": {
+          "type": "object",
+          "description": "Key value pairs that should match the feature manifest definition.",
+          "properties": {
+            "enabled": {
+              "type": "boolean"
+            }
+          },
+          "required": ["enabled"]
+        },
+        "targeting": {
+          "type": "string",
+          "description": "Target the configuration only to specific clients."
+        },
+        "bucketConfig": {
+          "type": "object",
+          "properties": {
+            "randomizationUnit": {
+              "type": "string",
+              "description": "A unique, stable identifier for the user used as an input to bucket hashing"
+            },
+            "namespace": {
+              "type": "string",
+              "description": "Additional inputs to the hashing function"
+            },
+            "start": {
+              "type": "number",
+              "description": "Index of start of the range of buckets"
+            },
+            "count": {
+              "type": "number",
+              "description": "Number of buckets to check"
+            },
+            "total": {
+              "type": "number",
+              "description": "Total number of buckets",
+              "default": 10000
+            }
+          },
+          "required": [
+            "randomizationUnit",
+            "namespace",
+            "start",
+            "count",
+            "total"
+          ],
+          "additionalProperties": false,
+          "description": "Bucketing configuration"
+        },
+        "description": {
+          "type": "string",
+          "description": "Explanation for configuration and targeting"
+        }
+      },
+      "required": ["variables", "targeting", "slug"],
+      "additionalProperties": false
     }
   }
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/nimbus/schemas/NimbusEnrollment.schema.json
@@ -0,0 +1,91 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$ref": "#/definitions/NimbusExperiment",
+  "definitions": {
+    "NimbusExperiment": {
+      "type": "object",
+      "properties": {
+        "slug": {
+          "type": "string"
+        },
+        "branch": {
+          "type": "object",
+          "properties": {
+            "feature": {
+              "type": "object",
+              "properties": {
+                "featureId": {
+                  "type": "string",
+                  "description": "The identifier for the feature flag"
+                },
+                "value": {
+                  "anyOf": [
+                    {
+                      "type": "object",
+                      "additionalProperties": {}
+                    },
+                    {
+                      "type": "null"
+                    }
+                  ],
+                  "description": "Optional extra params for the feature (this should be validated against a schema)"
+                },
+                "enabled": {
+                  "type": "boolean",
+                  "description": "(deprecated)"
+                }
+              },
+              "required": ["featureId", "value"],
+              "additionalProperties": false
+            }
+          },
+          "required": ["feature"]
+        },
+        "active": {
+          "type": "boolean",
+          "description": "Experiment status"
+        },
+        "enrollmentId": {
+          "type": "string",
+          "description": "Unique identifier used in telemetry"
+        },
+        "experimentType": {
+          "type": "string"
+        },
+        "isEnrollmentPaused": {
+          "type": "boolean"
+        },
+        "source": {
+          "type": "string",
+          "description": "What triggered the enrollment"
+        },
+        "userFacingName": {
+          "type": "string"
+        },
+        "userFacingDescription": {
+          "type": "string"
+        },
+        "lastSeen": {
+          "type": "string",
+          "description": "When was the enrollment made"
+        },
+        "force": {
+          "type": "boolean",
+          "description": "(debug) If the enrollment happened naturally or through devtools"
+        }
+      },
+      "required": [
+        "slug",
+        "branch",
+        "active",
+        "enrollmentId",
+        "experimentType",
+        "source",
+        "userFacingName",
+        "userFacingDescription"
+      ],
+      "additionalProperties": false,
+      "description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"
+    }
+  }
+}
--- a/toolkit/components/nimbus/test/NimbusTestUtils.jsm
+++ b/toolkit/components/nimbus/test/NimbusTestUtils.jsm
@@ -15,79 +15,131 @@ XPCOMUtils.defineLazyModuleGetters(this,
   _ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
   ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
   ExperimentStore: "resource://nimbus/lib/ExperimentStore.jsm",
   NormandyUtils: "resource://normandy/lib/NormandyUtils.jsm",
   FileTestUtils: "resource://testing-common/FileTestUtils.jsm",
   _RemoteSettingsExperimentLoader:
     "resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm",
   Ajv: "resource://testing-common/ajv-4.1.1.js",
+  sinon: "resource://testing-common/Sinon.jsm",
 });
 
 const { SYNC_DATA_PREF_BRANCH, SYNC_DEFAULTS_PREF_BRANCH } = ExperimentStore;
 
 const PATH = FileTestUtils.getTempFile("shared-data-map").path;
 
-XPCOMUtils.defineLazyGetter(this, "fetchExperimentSchema", async () => {
-  const response = await fetch(
-    "resource://testing-common/NimbusExperiment.schema.json"
-  );
+async function fetchSchema(url) {
+  const response = await fetch(url);
   const schema = await response.json();
   if (!schema) {
-    throw new Error("Failed to load NimbusSchema");
+    throw new Error(`Failed to load ${url}`);
   }
-  return schema.definitions.NimbusExperiment;
-});
+  return schema.definitions;
+}
 
 const EXPORTED_SYMBOLS = ["ExperimentTestUtils", "ExperimentFakes"];
 
 const ExperimentTestUtils = {
-  /**
-   * Checks if an experiment is valid acording to existing schema
-   * @param {NimbusExperiment} experiment
-   */
-  async validateExperiment(experiment) {
-    const schema = await fetchExperimentSchema;
+  _validator(schema, value, errorMsg) {
     const ajv = new Ajv({ async: "co*", allErrors: true });
     const validator = ajv.compile(schema);
-    validator(experiment);
+    validator(value);
     if (validator.errors?.length) {
       throw new Error(
-        "Experiment not valid:" + JSON.stringify(validator.errors, undefined, 2)
+        `${errorMsg}: ${JSON.stringify(validator.errors, undefined, 2)}`
       );
     }
-    return experiment;
+    return value;
+  },
+
+  /**
+   * Checks if an experiment is valid acording to existing schema
+   */
+  async validateExperiment(experiment) {
+    const schema = (
+      await fetchSchema(
+        "resource://testing-common/NimbusExperiment.schema.json"
+      )
+    ).NimbusExperiment;
+
+    return this._validator(
+      schema,
+      experiment,
+      `Experiment ${experiment.slug} not valid`
+    );
+  },
+  async validateEnrollment(enrollment) {
+    const schema = (
+      await fetchSchema(
+        "resource://testing-common/NimbusEnrollment.schema.json"
+      )
+    ).NimbusExperiment;
+
+    return this._validator(
+      schema,
+      enrollment,
+      `Enrollment ${enrollment.slug} is not valid`
+    );
+  },
+  async validateRollouts(rollout) {
+    const schema = (
+      await fetchSchema(
+        "resource://testing-common/ExperimentFeatureRemote.schema.json"
+      )
+    ).RemoteFeatureConfiguration;
+
+    return this._validator(
+      schema,
+      rollout,
+      `Rollout configuration ${rollout.slug} is not valid`
+    );
   },
 };
 
 const ExperimentFakes = {
   manager(store) {
-    return new _ExperimentManager({ store: store || this.store() });
+    let sandbox = sinon.createSandbox();
+    let manager = new _ExperimentManager({ store: store || this.store() });
+    // We want calls to `store.addExperiment` to implicitly validate the
+    // enrollment before saving to store
+    let origAddExperiment = manager.store.addExperiment.bind(manager.store);
+    sandbox.stub(manager.store, "addExperiment").callsFake(async enrollment => {
+      await ExperimentTestUtils.validateEnrollment(enrollment);
+      return origAddExperiment(enrollment);
+    });
+
+    return manager;
   },
   store() {
     return new ExperimentStore("FakeStore", { path: PATH, isParent: true });
   },
   waitForExperimentUpdate(ExperimentAPI, options) {
     if (!options) {
       throw new Error("Must specify an expected recipe update");
     }
 
     return new Promise(resolve => ExperimentAPI.on("update", options, resolve));
   },
-  remoteDefaultsHelper({
+  async remoteDefaultsHelper({
     feature,
     store = ExperimentManager.store,
     configuration,
   }) {
     if (!store._isReady) {
       throw new Error("Store not ready, need to `await ExperimentAPI.ready()`");
     }
+
+    await ExperimentTestUtils.validateRollouts(configuration);
+    // After storing the remote configuration to store and updating the feature
+    // we want to flush so that NimbusFeature usage in content process also
+    // receives the update
     store.updateRemoteConfigs(feature.featureId, configuration);
-
-    return feature.ready().then(() => store._syncToChildren({ flush: true }));
+    await feature.ready();
+    store._syncToChildren({ flush: true });
   },
   async enrollWithFeatureConfig(
     featureConfig,
     { manager = ExperimentManager } = {}
   ) {
     await manager.store.ready();
     let recipe = this.recipe(
       `${featureConfig.featureId}-experiment-${Math.random()}`,
@@ -147,17 +199,17 @@ const ExperimentFakes = {
         throw new Error("Cleanup failed");
       }
     };
 
     if (recipe.slug) {
       if (!manager.store._isReady) {
         throw new Error("Manager store not ready, call `manager.onStartup`");
       }
-      manager.enroll(recipe);
+      manager.enroll(recipe, "enrollmentHelper");
     }
 
     return { enrollmentPromise, doExperimentCleanup };
   },
   // Experiment store caches in prefs Enrollments for fast sync access
   cleanupStorePrefCache() {
     try {
       Services.prefs.deleteBranch(SYNC_DATA_PREF_BRANCH);
@@ -188,18 +240,21 @@ const ExperimentFakes = {
       branch: {
         slug: "treatment",
         feature: {
           featureId: "test-feature",
           value: { title: "hello", enabled: true },
         },
         ...props,
       },
-      source: "test",
+      source: "NimbusTestUtils",
       isEnrollmentPaused: true,
+      experimentType: "NimbusTestUtils",
+      userFacingName: "NimbusTestUtils",
+      userFacingDescription: "NimbusTestUtils",
       ...props,
     };
   },
   recipe(slug = NormandyUtils.generateUuid(), props = {}) {
     return {
       // This field is required for populating remote settings
       id: NormandyUtils.generateUuid(),
       slug,
--- a/toolkit/components/nimbus/test/browser/browser_experiment_evaluate_jexl.js
+++ b/toolkit/components/nimbus/test/browser/browser_experiment_evaluate_jexl.js
@@ -70,20 +70,36 @@ add_task(async function test_evaluate_ac
   );
 });
 
 add_task(async function test_evaluate_active_experiments_activeExperiments() {
   // Add an experiment to active experiments
   const slug = "foo" + Math.random();
   // Init the store before we use it
   await ExperimentManager.onStartup();
-  ExperimentManager.store.addExperiment(ExperimentFakes.experiment(slug));
-  registerCleanupFunction(() => {
-    ExperimentManager.store._deleteForTests(slug);
-  });
+  let {
+    enrollmentPromise,
+    doExperimentCleanup,
+  } = ExperimentFakes.enrollmentHelper(
+    ExperimentFakes.recipe(slug, {
+      branches: [
+        {
+          slug: "mochitest-active-foo",
+          feature: {
+            enabled: true,
+            featureId: "foo",
+            value: null,
+          },
+        },
+      ],
+      active: true,
+    })
+  );
+
+  await enrollmentPromise;
 
   Assert.equal(
     await RemoteSettingsExperimentLoader.evaluateJexl(
       `"${slug}" in activeExperiments`,
       FAKE_CONTEXT
     ),
     true,
     "should find an active experiment"
@@ -92,9 +108,11 @@ add_task(async function test_evaluate_ac
   Assert.equal(
     await RemoteSettingsExperimentLoader.evaluateJexl(
       `"does-not-exist-fake" in activeExperiments`,
       FAKE_CONTEXT
     ),
     false,
     "should not find an experiment that doesn't exist"
   );
+
+  await doExperimentCleanup();
 });
--- a/toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_init.js
+++ b/toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_init.js
@@ -43,24 +43,20 @@ add_task(async function test_double_feat
   Assert.ok(ExperimentManager.store.getAllActive().length === 0, "Clean state");
 
   let recipe1 = getRecipe("foo" + Math.random());
   let recipe2 = getRecipe("foo" + Math.random());
 
   let enrollPromise1 = ExperimentFakes.waitForExperimentUpdate(ExperimentAPI, {
     slug: recipe1.slug,
   });
-  let enrollPromise2 = ExperimentFakes.waitForExperimentUpdate(ExperimentAPI, {
-    slug: recipe2.slug,
-  });
 
-  ExperimentManager.enroll(recipe1);
-  ExperimentManager.enroll(recipe2);
-
-  await Promise.any([enrollPromise1, enrollPromise2]);
+  ExperimentManager.enroll(recipe1, "test_double_feature_enrollment");
+  await enrollPromise1;
+  ExperimentManager.enroll(recipe2, "test_double_feature_enrollment");
 
   Assert.equal(
     ExperimentManager.store.getAllActive().length,
     1,
     "1 active experiment"
   );
 
   await BrowserTestUtils.waitForCondition(
--- a/toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_remote_defaults.js
+++ b/toolkit/components/nimbus/test/browser/browser_remotesettingsexperimentloader_remote_defaults.js
@@ -18,44 +18,39 @@ const {
   RemoteDefaultsLoader,
   RemoteSettingsExperimentLoader,
 } = ChromeUtils.import(
   "resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm"
 );
 const { BrowserTestUtils } = ChromeUtils.import(
   "resource://testing-common/BrowserTestUtils.jsm"
 );
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { TelemetryEnvironment } = ChromeUtils.import(
   "resource://gre/modules/TelemetryEnvironment.jsm"
 );
 
 const REMOTE_CONFIGURATION_AW = {
   id: "aboutwelcome",
   description: "about:welcome",
   configurations: [
     {
       slug: "a",
-      variables: { remoteValue: 24 },
-      enabled: false,
+      variables: { remoteValue: 24, enabled: false },
       targeting: "false",
       bucketConfig: {
         namespace: "nimbus-test-utils",
         randomizationUnit: "normandy_id",
         start: 0,
         count: 1000,
         total: 1000,
       },
     },
     {
       slug: "b",
-      variables: { remoteValue: 42 },
-      enabled: true,
+      variables: { remoteValue: 42, enabled: true },
       targeting: "true",
       bucketConfig: {
         namespace: "nimbus-test-utils",
         randomizationUnit: "normandy_id",
         start: 0,
         count: 1000,
         total: 1000,
       },
@@ -63,44 +58,41 @@ const REMOTE_CONFIGURATION_AW = {
   ],
 };
 const REMOTE_CONFIGURATION_NEWTAB = {
   id: "newtab",
   description: "about:newtab",
   configurations: [
     {
       slug: "a",
-      variables: { remoteValue: 1 },
-      enabled: false,
+      variables: { remoteValue: 1, enabled: false },
       targeting: "false",
       bucketConfig: {
         namespace: "nimbus-test-utils",
         randomizationUnit: "normandy_id",
         start: 0,
         count: 1000,
         total: 1000,
       },
     },
     {
       slug: "b",
-      variables: { remoteValue: 3 },
-      enabled: true,
+      variables: { remoteValue: 3, enabled: true },
       targeting: "true",
       bucketConfig: {
         namespace: "nimbus-test-utils",
         randomizationUnit: "normandy_id",
         start: 0,
         count: 1000,
         total: 1000,
       },
     },
     {
       slug: "c",
-      variables: { remoteValue: 2 },
-      enabled: false,
+      variables: { remoteValue: 2, enabled: false },
       targeting: "false",
       bucketConfig: {
         namespace: "nimbus-test-utils",
         randomizationUnit: "normandy_id",
         start: 0,
         count: 1000,
         total: 1000,
       },
@@ -482,32 +474,31 @@ add_task(async function remote_defaults_
 
   Assert.ok(config.remoteStub, "Got back the expected value");
 
   sandbox.restore();
 });
 
 add_task(async function remote_defaults_active_experiments_check() {
   let barFeature = new ExperimentFeature("bar", {
-    bar: { description: "mochitest" },
+    description: "mochitest",
+    variables: { enabled: { type: "boolean" } },
   });
   let experimentOnlyRemoteDefault = {
     id: "bar",
     description: "if we're in the foo experiment bar should be off",
     configurations: [
       {
         slug: "a",
-        variables: {},
-        enabled: false,
+        variables: { enabled: false },
         targeting: "'mochitest-active-foo' in activeExperiments",
       },
       {
         slug: "b",
-        variables: {},
-        enabled: true,
+        variables: { enabled: true },
         targeting: "true",
       },
     ],
   };
 
   await setup([experimentOnlyRemoteDefault]);
   await RemoteDefaultsLoader.syncRemoteDefaults("mochitest");
   await barFeature.ready();
@@ -545,42 +536,42 @@ add_task(async function remote_defaults_
 
   await doExperimentCleanup();
 });
 
 add_task(async function remote_defaults_active_remote_defaults() {
   ExperimentAPI._store._deleteForTests("foo");
   ExperimentAPI._store._deleteForTests("bar");
   let barFeature = new ExperimentFeature("bar", {
-    bar: { description: "mochitest" },
+    description: "mochitest",
+    variables: { enabled: { type: "boolean" } },
   });
   let fooFeature = new ExperimentFeature("foo", {
-    foo: { description: "mochitest" },
+    description: "mochitest",
+    variables: { enabled: { type: "boolean" } },
   });
   let remoteDefaults = [
     {
       id: "bar",
       description: "will enroll first try",
       configurations: [
         {
           slug: "a",
-          variables: {},
-          enabled: true,
+          variables: { enabled: true },
           targeting: "true",
         },
       ],
     },
     {
       id: "foo",
       description: "will enroll second try after bar",
       configurations: [
         {
           slug: "b",
-          variables: {},
-          enabled: true,
+          variables: { enabled: true },
           targeting: "'bar' in activeRemoteDefaults",
         },
       ],
     },
   ];
 
   await setup(remoteDefaults);
   await RemoteDefaultsLoader.syncRemoteDefaults("mochitest");
@@ -642,24 +633,22 @@ add_task(async function test_remote_defa
 add_task(async function test_remote_defaults_no_bucketConfig() {
   const sandbox = sinon.createSandbox();
   const remoteConfigNoBucket = {
     id: "aboutwelcome",
     description: "about:welcome",
     configurations: [
       {
         slug: "a",
-        variables: { remoteValue: 24 },
-        enabled: false,
+        variables: { remoteValue: 24, enabled: false },
         targeting: "false",
       },
       {
         slug: "b",
-        variables: { remoteValue: 42 },
-        enabled: true,
+        variables: { remoteValue: 42, enabled: true },
         targeting: "true",
       },
     ],
   };
   let finalizeRemoteConfigsStub = sandbox.stub(
     ExperimentAPI._store,
     "finalizeRemoteConfigs"
   );
@@ -683,44 +672,64 @@ add_task(async function test_remote_defa
   );
 
   sandbox.restore();
   await rsClient.db.clear();
 });
 
 add_task(async function remote_defaults_variables_storage() {
   let barFeature = new ExperimentFeature("bar", {
-    bar: { description: "mochitest" },
+    bar: {
+      description: "mochitest",
+      variables: {
+        storage: {
+          type: "int",
+        },
+        object: {
+          type: "json",
+        },
+        string: {
+          type: "string",
+        },
+        bool: {
+          type: "boolean",
+        },
+      },
+    },
   });
   let remoteDefaults = [
     {
       id: "bar",
       description: "test pref storage and types",
       configurations: [
         {
           slug: "a",
           isEarlyStartup: true,
           variables: {
             storage: 42,
             object: { foo: "foo" },
             string: "string",
             bool: true,
+            enabled: true,
           },
-          enabled: true,
           targeting: "true",
         },
       ],
     },
   ];
 
   await setup(remoteDefaults);
   await RemoteDefaultsLoader.syncRemoteDefaults("mochitest");
   await barFeature.ready();
 
   Assert.ok(
+    Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}bar`, ""),
+    "Experiment stored in prefs"
+  );
+  Assert.ok(
     Services.prefs.getIntPref(`${SYNC_DEFAULTS_PREF_BRANCH}bar.storage`, 0),
     "Stores variable in separate pref"
   );
   Assert.equal(
     Services.prefs.getIntPref(`${SYNC_DEFAULTS_PREF_BRANCH}bar.storage`, 0),
     42,
     "Stores variable in correct type"
   );
--- a/toolkit/components/nimbus/test/browser/head.js
+++ b/toolkit/components/nimbus/test/browser/head.js
@@ -1,7 +1,59 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Globals
 const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm",
+  Ajv: "resource://testing-common/ajv-4.1.1.js",
+  ExperimentTestUtils: "resource://testing-common/NimbusTestUtils.jsm",
+  RemoteDefaultsLoader:
+    "resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm",
+  ExperimentFakes: "resource://testing-common/NimbusTestUtils.jsm",
+});
+
+add_task(function setup() {
+  let sandbox = sinon.createSandbox();
+
+  /* We stub the functions that operate with enrollments and remote rollouts
+   * so that any access to store something is implicitly validated against
+   * the schema and no records have missing (or extra) properties while in tests
+   */
+
+  let origAddExperiment = ExperimentManager.store.addExperiment.bind(
+    ExperimentManager.store
+  );
+  let origOnUpdatesReady = RemoteDefaultsLoader._onUpdatesReady.bind(
+    RemoteDefaultsLoader
+  );
+  sandbox
+    .stub(ExperimentManager.store, "addExperiment")
+    .callsFake(async enrollment => {
+      await ExperimentTestUtils.validateEnrollment(enrollment);
+      return origAddExperiment(enrollment);
+    });
+  // Unlike `addExperiment` the method to store remote rollouts is syncronous
+  // and our validation method would turn it async. If we had changed to `await`
+  // for remote configs storage it would have changed the code logic so we are
+  // going up one level to the function that receives the RS records and do
+  // the validation there.
+  sandbox
+    .stub(RemoteDefaultsLoader, "_onUpdatesReady")
+    .callsFake(async (remoteDefaults, reason) => {
+      for (let remoteDefault of remoteDefaults) {
+        for (let config of remoteDefault.configurations) {
+          await ExperimentTestUtils.validateRollouts(config);
+        }
+      }
+      return origOnUpdatesReady(remoteDefaults, reason);
+    });
+
+  registerCleanupFunction(() => {
+    sandbox.restore();
+  });
+});
--- a/toolkit/components/nimbus/test/unit/head.js
+++ b/toolkit/components/nimbus/test/unit/head.js
@@ -1,5 +1,11 @@
 "use strict";
 // Globals
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ExperimentFakes: "resource://testing-common/NimbusTestUtils.jsm",
+});
--- a/toolkit/components/nimbus/test/unit/test_ExperimentAPI.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI.js
@@ -22,17 +22,17 @@ add_task(async function test_getExperime
   const sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   const expected = ExperimentFakes.experiment("foo");
 
   await manager.onStartup();
 
   sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
 
-  manager.store.addExperiment(expected);
+  await manager.store.addExperiment(expected);
 
   // Wait to sync to child
   await TestUtils.waitForCondition(
     () => ExperimentAPI.getExperiment({ slug: "foo" }),
     "Wait for child to sync"
   );
 
   Assert.equal(
@@ -54,17 +54,17 @@ add_task(async function test_getExperime
   const sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   const expected = ExperimentFakes.experiment("foo");
 
   await manager.onStartup();
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
   await ExperimentAPI.ready();
 
-  manager.store.addExperiment(expected);
+  await manager.store.addExperiment(expected);
 
   Assert.equal(
     ExperimentAPI.getExperiment({ slug: "foo" }).slug,
     expected.slug,
     "should return an experiment by slug"
   );
 
   sandbox.restore();
@@ -75,17 +75,17 @@ add_task(async function test_getExperime
   const manager = ExperimentFakes.manager();
   const expected = ExperimentFakes.experiment("foo");
   let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
 
   await manager.onStartup();
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
   await ExperimentAPI.ready();
 
-  manager.store.addExperiment(expected);
+  await manager.store.addExperiment(expected);
 
   let metadata = ExperimentAPI.getExperimentMetaData({ slug: expected.slug });
 
   Assert.equal(
     Object.keys(metadata.branch).length,
     1,
     "Should only expose one property"
   );
@@ -135,26 +135,26 @@ add_task(function test_getExperimentMeta
 
 add_task(async function test_getExperiment_feature() {
   const sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   const expected = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "treatment",
       value: { title: "hi" },
-      feature: { featureId: "cfr", enabled: true },
+      feature: { featureId: "cfr", enabled: true, value: null },
     },
   });
 
   await manager.onStartup();
 
   sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
   let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
 
-  manager.store.addExperiment(expected);
+  await manager.store.addExperiment(expected);
 
   // Wait to sync to child
   await TestUtils.waitForCondition(
     () => ExperimentAPI.getExperiment({ featureId: "cfr" }),
     "Wait for child to sync"
   );
 
   Assert.equal(
@@ -263,29 +263,29 @@ add_task(async function test_getAllBranc
  */
 add_task(async function test_addExperiment_eventEmit_add() {
   const sandbox = sinon.createSandbox();
   const slugStub = sandbox.stub();
   const featureStub = sandbox.stub();
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
-      feature: { featureId: "purple", enabled: true },
+      feature: { featureId: "purple", enabled: true, value: null },
     },
   });
   const store = ExperimentFakes.store();
   sandbox.stub(ExperimentAPI, "_store").get(() => store);
 
   await store.init();
   await ExperimentAPI.ready();
 
   ExperimentAPI.on("update", { slug: "foo" }, slugStub);
   ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
 
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
 
   Assert.equal(
     slugStub.callCount,
     1,
     "should call 'update' callback for slug when experiment is added"
   );
   Assert.equal(slugStub.firstCall.args[1].slug, experiment.slug);
   Assert.equal(
@@ -298,26 +298,26 @@ add_task(async function test_addExperime
 
 add_task(async function test_updateExperiment_eventEmit_add_and_update() {
   const sandbox = sinon.createSandbox();
   const slugStub = sandbox.stub();
   const featureStub = sandbox.stub();
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
-      feature: { featureId: "purple", enabled: true },
+      feature: { featureId: "purple", enabled: true, value: null },
     },
   });
   const store = ExperimentFakes.store();
   sandbox.stub(ExperimentAPI, "_store").get(() => store);
 
   await store.init();
   await ExperimentAPI.ready();
 
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
 
   ExperimentAPI.on("update", { slug: "foo" }, slugStub);
   ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
 
   store.updateExperiment(experiment.slug, experiment);
 
   await TestUtils.waitForCondition(
     () => slugStub.callCount == 2,
@@ -332,29 +332,29 @@ add_task(async function test_updateExper
 
 add_task(async function test_updateExperiment_eventEmit_off() {
   const sandbox = sinon.createSandbox();
   const slugStub = sandbox.stub();
   const featureStub = sandbox.stub();
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
-      feature: { featureId: "purple", enabled: true },
+      feature: { featureId: "purple", enabled: true, value: null },
     },
   });
   const store = ExperimentFakes.store();
   sandbox.stub(ExperimentAPI, "_store").get(() => store);
 
   await store.init();
   await ExperimentAPI.ready();
 
   ExperimentAPI.on("update", { slug: "foo" }, slugStub);
   ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
 
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
 
   ExperimentAPI.off("update:foo", slugStub);
   ExperimentAPI.off("update:purple", featureStub);
 
   store.updateExperiment(experiment.slug, experiment);
 
   Assert.equal(slugStub.callCount, 1, "Called only once before `off`");
   Assert.equal(featureStub.callCount, 1, "Called only once before `off`");
@@ -362,22 +362,22 @@ add_task(async function test_updateExper
 
 add_task(async function test_activateBranch() {
   const sandbox = sinon.createSandbox();
   const store = ExperimentFakes.store();
   sandbox.stub(ExperimentAPI, "_store").get(() => store);
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
-      feature: { featureId: "green", enabled: true },
+      feature: { featureId: "green", enabled: true, value: null },
     },
   });
 
   await store.init();
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
 
   Assert.deepEqual(
     ExperimentAPI.activateBranch({ featureId: "green" }),
     experiment.branch,
     "Should return feature of active experiment"
   );
 
   sandbox.restore();
@@ -407,17 +407,17 @@ add_task(async function test_activateBra
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
       feature: { featureId: "green", enabled: true },
     },
   });
 
   await store.init();
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
   // Adding stub later because `addExperiment` emits update events
   const stub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
   ExperimentAPI.activateBranch({ featureId: "green" });
   Assert.equal(
     stub.callCount,
     0,
     "Exposure is not sent by default by activateBranch"
   );
@@ -444,17 +444,17 @@ add_task(async function test_activateBra
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
       feature: { featureId: "green", enabled: true },
     },
   });
 
   await store.init();
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
   // Adding stub later because `addExperiment` emits update events
   const stub = sandbox.stub(store, "emit");
   // Call activateBranch to trigger an activation event
   sandbox.stub(store, "getAllActive").throws();
   try {
     ExperimentAPI.activateBranch({ featureId: "green" });
   } catch (e) {
     /* This is expected */
@@ -471,17 +471,17 @@ add_task(async function test_activateBra
   const experiment = ExperimentFakes.experiment("foo", {
     branch: {
       slug: "variant",
       feature: { featureId: "green", enabled: true },
     },
   });
 
   await store.init();
-  store.addExperiment(experiment);
+  await store.addExperiment(experiment);
   // Adding stub later because `addExperiment` emits update events
   const stub = sandbox.stub(store, "emit");
   // Call activateBranch to trigger an activation event
   ExperimentAPI.activateBranch({ featureId: "green" });
 
   Assert.equal(stub.callCount, 0, "Not called: sendExposureEvent is false");
   sandbox.restore();
 });
--- a/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature.js
@@ -35,16 +35,17 @@ const FAKE_FEATURE_MANIFEST = {
     },
     config: {
       type: "json",
       fallbackPref: TEST_FALLBACK_PREF,
     },
   },
 };
 const FAKE_FEATURE_REMOTE_VALUE = {
+  slug: "default-remote-value",
   variables: {
     enabled: true,
   },
   targeting: "true",
 };
 
 /**
  * # ExperimentFeature.getValue
@@ -63,17 +64,17 @@ add_task(async function test_ExperimentF
       feature: {
         featureId: "foo",
         enabled: true,
         value: { whoa: true },
       },
     },
   });
 
-  manager.store.addExperiment(expected);
+  await manager.store.addExperiment(expected);
 
   await readyPromise;
 
   Assert.deepEqual(
     featureInstance.getValue(),
     { whoa: true },
     "should return getValue after waiting on ready"
   );
@@ -116,17 +117,17 @@ add_task(
         feature: {
           featureId: "foo",
           enabled: true,
           value: { whoa: true },
         },
       },
     });
 
-    manager.store.addExperiment(expected);
+    await manager.store.addExperiment(expected);
 
     setDefaultBranch(TEST_FALLBACK_PREF, `{"bar": 123}`);
 
     Assert.deepEqual(
       featureInstance.getValue(),
       { whoa: true },
       "should return the experiment feature value, not the fallback one."
     );
@@ -221,17 +222,20 @@ add_task(async function test_ExperimentF
   const { manager } = await setupForExperimentFeature();
   await manager.store.ready();
 
   const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
 
   await ExperimentFakes.remoteDefaultsHelper({
     feature: featureInstance,
     store: manager.store,
-    configuration: { variables: { remoteValue: "mochitest" }, enabled: true },
+    configuration: {
+      ...FAKE_FEATURE_REMOTE_VALUE,
+      variables: { remoteValue: "mochitest", enabled: true },
+    },
   });
 
   Assert.equal(featureInstance.isEnabled(), true, "enabled by remote config");
   Assert.equal(
     featureInstance.getValue().remoteValue,
     "mochitest",
     "set by remote config"
   );
@@ -248,17 +252,17 @@ add_task(
           value: { enabled: true },
         },
       },
     });
     const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
 
     await manager.store.ready();
 
-    manager.store.addExperiment(expected);
+    await manager.store.addExperiment(expected);
 
     const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
     manager.store.updateRemoteConfigs("foo", {
       ...FAKE_FEATURE_REMOTE_VALUE,
       variables: { enabled: false },
     });
 
     await featureInstance.ready();
@@ -300,17 +304,17 @@ add_task(async function test_ExperimentF
         value: { enabled: false },
       },
     },
   });
   const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
 
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
 
-  manager.store.addExperiment(expected);
+  await manager.store.addExperiment(expected);
 
   const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
 
   const actual = featureInstance.isEnabled({ sendExposureEvent: false });
 
   Assert.deepEqual(actual, false, "should return feature as disabled");
   Assert.ok(
     exposureSpy.notCalled,
@@ -329,17 +333,17 @@ add_task(async function test_record_expo
 
   featureInstance.recordExposureEvent();
 
   Assert.ok(
     exposureSpy.notCalled,
     "should not emit an exposure event when no experiment is active"
   );
 
-  manager.store.addExperiment(
+  await manager.store.addExperiment(
     ExperimentFakes.experiment("blah", {
       branch: {
         slug: "treatment",
         feature: {
           featureId: "foo",
           value: { enabled: false },
         },
       },
@@ -358,17 +362,17 @@ add_task(async function test_record_expo
 
 add_task(async function test_record_exposure_event_once() {
   const { sandbox, manager } = await setupForExperimentFeature();
 
   const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
   const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
 
-  manager.store.addExperiment(
+  await manager.store.addExperiment(
     ExperimentFakes.experiment("blah", {
       branch: {
         slug: "treatment",
         feature: {
           featureId: "foo",
           value: { enabled: false },
         },
       },
@@ -386,17 +390,17 @@ add_task(async function test_record_expo
 
 add_task(async function test_prevent_double_exposure_getValue() {
   const { sandbox, manager } = await setupForExperimentFeature();
 
   const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
   const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
 
-  manager.store.addExperiment(
+  await manager.store.addExperiment(
     ExperimentFakes.experiment("blah", {
       branch: {
         slug: "treatment",
         feature: {
           featureId: "foo",
           value: { enabled: false },
         },
       },
@@ -417,17 +421,17 @@ add_task(async function test_prevent_dou
 
 add_task(async function test_prevent_double_exposure_isEnabled() {
   const { sandbox, manager } = await setupForExperimentFeature();
 
   const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
   const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
 
-  manager.store.addExperiment(
+  await manager.store.addExperiment(
     ExperimentFakes.experiment("blah", {
       branch: {
         slug: "treatment",
         feature: {
           featureId: "foo",
           value: { enabled: false },
         },
       },
@@ -447,33 +451,38 @@ add_task(async function test_prevent_dou
 });
 
 add_task(async function test_set_remote_before_ready() {
   let sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
   const feature = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
 
-  Assert.throws(
-    () =>
-      ExperimentFakes.remoteDefaultsHelper({
-        feature,
-        store: manager.store,
-        configuration: { variables: { test: true } },
-      }),
+  await Assert.rejects(
+    ExperimentFakes.remoteDefaultsHelper({
+      feature,
+      store: manager.store,
+      configuration: {
+        ...FAKE_FEATURE_REMOTE_VALUE,
+        variables: { test: true, enabled: true },
+      },
+    }),
     /Store not ready/,
     "Throws if used before init finishes"
   );
 
   await manager.onStartup();
 
   await ExperimentFakes.remoteDefaultsHelper({
     feature,
     store: manager.store,
-    configuration: { variables: { test: true } },
+    configuration: {
+      ...FAKE_FEATURE_REMOTE_VALUE,
+      variables: { test: true, enabled: true },
+    },
   });
 
   Assert.ok(feature.getValue().test, "Successfully set");
 });
 
 add_task(async function test_isEnabled_backwards_compatible() {
   const PREVIOUS_FEATURE_MANIFEST = {
     variables: {
@@ -489,22 +498,25 @@ add_task(async function test_isEnabled_b
   const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
   const feature = new ExperimentFeature("foo", PREVIOUS_FEATURE_MANIFEST);
 
   await manager.onStartup();
 
   await ExperimentFakes.remoteDefaultsHelper({
     feature,
     store: manager.store,
-    configuration: { variables: {}, enabled: false },
+    configuration: {
+      ...FAKE_FEATURE_REMOTE_VALUE,
+      variables: { enabled: false },
+    },
   });
 
   Assert.ok(!feature.isEnabled(), "Disabled based on remote configs");
 
-  manager.store.addExperiment(
+  await manager.store.addExperiment(
     ExperimentFakes.experiment("blah", {
       branch: {
         slug: "treatment",
         feature: {
           featureId: "foo",
           enabled: true,
           value: {},
         },
--- a/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getAllVariables.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getAllVariables.js
@@ -6,20 +6,16 @@ const {
 } = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
 const { ExperimentFakes } = ChromeUtils.import(
   "resource://testing-common/NimbusTestUtils.jsm"
 );
 const { TestUtils } = ChromeUtils.import(
   "resource://testing-common/TestUtils.jsm"
 );
 
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
-
 const { cleanupStorePrefCache } = ExperimentFakes;
 
 async function setupForExperimentFeature() {
   const sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   await manager.onStartup();
 
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
@@ -83,17 +79,17 @@ add_task(
         feature: {
           featureId: "aboutwelcome",
           enabled: true,
           value: { screens: ["test-value"] },
         },
       },
     });
 
-    manager.store.addExperiment(recipe);
+    await manager.store.addExperiment(recipe);
 
     const featureInstance = new ExperimentFeature(
       FEATURE_ID,
       FAKE_FEATURE_MANIFEST
     );
 
     Services.prefs.clearUserPref(TEST_FALLBACK_PREF);
 
--- a/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getValue.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getValue.js
@@ -6,20 +6,16 @@ const {
 } = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
 const { ExperimentFakes } = ChromeUtils.import(
   "resource://testing-common/NimbusTestUtils.jsm"
 );
 const { TestUtils } = ChromeUtils.import(
   "resource://testing-common/TestUtils.jsm"
 );
 
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
-
 const { cleanupStorePrefCache } = ExperimentFakes;
 
 async function setupForExperimentFeature() {
   const sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   await manager.onStartup();
 
   sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
@@ -77,17 +73,17 @@ add_task(async function test_ExperimentF
       feature: {
         featureId: "aboutwelcome",
         enabled: true,
         value: { screens: ["test-value"] },
       },
     },
   });
 
-  manager.store.addExperiment(recipe);
+  await manager.store.addExperiment(recipe);
 
   const featureInstance = new ExperimentFeature(
     FEATURE_ID,
     FAKE_FEATURE_MANIFEST
   );
 
   Services.prefs.clearUserPref(TEST_FALLBACK_PREF);
 
--- a/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getVariable.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_ExperimentFeature_getVariable.js
@@ -1,23 +1,17 @@
 "use strict";
 
 const {
   ExperimentAPI,
   _ExperimentFeature: ExperimentFeature,
 } = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { TestUtils } = ChromeUtils.import(
   "resource://testing-common/TestUtils.jsm"
 );
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
 const { AppConstants } = ChromeUtils.import(
   "resource://gre/modules/AppConstants.jsm"
 );
 
 async function setupForExperimentFeature() {
   const sandbox = sinon.createSandbox();
   const manager = ExperimentFakes.manager();
   await manager.onStartup();
--- a/toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js
@@ -1,26 +1,20 @@
 "use strict";
 
 const {
   ExperimentAPI,
   NimbusFeatures,
   _ExperimentFeature: ExperimentFeature,
 } = ChromeUtils.import("resource://nimbus/ExperimentAPI.jsm");
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { TestUtils } = ChromeUtils.import(
   "resource://testing-common/TestUtils.jsm"
 );
+const { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
 
-const { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
 Cu.importGlobalProperties(["fetch"]);
 
 XPCOMUtils.defineLazyGetter(this, "fetchSchema", async () => {
   const response = await fetch(
     "resource://testing-common/ExperimentFeatureRemote.schema.json"
   );
   const schema = await response.json();
   if (!schema) {
--- a/toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_enroll.js
@@ -1,13 +1,10 @@
 "use strict";
 
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { NormandyTestUtils } = ChromeUtils.import(
   "resource://testing-common/NormandyTestUtils.jsm"
 );
 const { Sampling } = ChromeUtils.import(
   "resource://gre/modules/components-utils/Sampling.jsm"
 );
 const { ClientEnvironment } = ChromeUtils.import(
   "resource://normandy/lib/ClientEnvironment.jsm"
@@ -26,20 +23,24 @@ const { ExperimentStore } = ChromeUtils.
 const { SYNC_DATA_PREF_BRANCH } = ExperimentStore;
 
 /**
  * The normal case: Enrollment of a new experiment
  */
 add_task(async function test_add_to_store() {
   const manager = ExperimentFakes.manager();
   const recipe = ExperimentFakes.recipe("foo");
+  const enrollPromise = new Promise(resolve =>
+    manager.store.on("update:foo", resolve)
+  );
 
   await manager.onStartup();
 
-  await manager.enroll(recipe);
+  await manager.enroll(recipe, "test_add_to_store");
+  await enrollPromise;
   const experiment = manager.store.get("foo");
 
   Assert.ok(experiment, "should add an experiment with slug foo");
   Assert.ok(
     recipe.branches.includes(experiment.branch),
     "should choose a branch from the recipe.branches"
   );
   Assert.equal(experiment.active, true, "should set .active = true");
@@ -48,24 +49,31 @@ add_task(async function test_add_to_stor
     "should add a valid enrollmentId"
   );
 });
 
 add_task(
   async function test_setExperimentActive_sendEnrollmentTelemetry_called() {
     const manager = ExperimentFakes.manager();
     const sandbox = sinon.createSandbox();
+    const enrollPromise = new Promise(resolve =>
+      manager.store.on("update:foo", resolve)
+    );
     sandbox.spy(manager, "setExperimentActive");
     sandbox.spy(manager, "sendEnrollmentTelemetry");
 
     await manager.onStartup();
 
     await manager.onStartup();
 
-    await manager.enroll(ExperimentFakes.recipe("foo"));
+    await manager.enroll(
+      ExperimentFakes.recipe("foo"),
+      "test_setExperimentActive_sendEnrollmentTelemetry_called"
+    );
+    await enrollPromise;
     const experiment = manager.store.get("foo");
 
     Assert.equal(
       manager.setExperimentActive.calledWith(experiment),
       true,
       "should call setExperimentActive after an enrollment"
     );
 
@@ -86,20 +94,20 @@ add_task(
 add_task(async function test_failure_name_conflict() {
   const manager = ExperimentFakes.manager();
   const sandbox = sinon.createSandbox();
   sandbox.spy(manager, "sendFailureTelemetry");
 
   await manager.onStartup();
 
   // simulate adding a previouly enrolled experiment
-  manager.store.addExperiment(ExperimentFakes.experiment("foo"));
+  await manager.store.addExperiment(ExperimentFakes.experiment("foo"));
 
   await Assert.rejects(
-    manager.enroll(ExperimentFakes.recipe("foo")),
+    manager.enroll(ExperimentFakes.recipe("foo"), "test_failure_name_conflict"),
     /An experiment with the slug "foo" already exists/,
     "should throw if a conflicting experiment exists"
   );
 
   Assert.equal(
     manager.sendFailureTelemetry.calledWith(
       "enrollFailed",
       "foo",
@@ -116,35 +124,36 @@ add_task(async function test_failure_gro
   sandbox.spy(manager, "sendFailureTelemetry");
 
   await manager.onStartup();
 
   // Two conflicting branches that both have the group "pink"
   // These should not be allowed to exist simultaneously.
   const existingBranch = {
     slug: "treatment",
-    feature: { featureId: "pink", enabled: true },
+    feature: { featureId: "pink", enabled: true, value: {} },
   };
   const newBranch = {
     slug: "treatment",
-    feature: { featureId: "pink", enabled: true },
+    feature: { featureId: "pink", enabled: true, value: {} },
   };
 
   // simulate adding an experiment with a conflicting group "pink"
-  manager.store.addExperiment(
+  await manager.store.addExperiment(
     ExperimentFakes.experiment("foo", {
       branch: existingBranch,
     })
   );
 
   // ensure .enroll chooses the special branch with the conflict
   sandbox.stub(manager, "chooseBranch").returns(newBranch);
   Assert.equal(
     await manager.enroll(
-      ExperimentFakes.recipe("bar", { branches: [newBranch] })
+      ExperimentFakes.recipe("bar", { branches: [newBranch] }),
+      "test_failure_group_conflict"
     ),
     null,
     "should not enroll if there is a feature conflict"
   );
 
   Assert.equal(
     manager.sendFailureTelemetry.calledWith(
       "enrollFailed",
@@ -227,18 +236,22 @@ add_task(async function enroll_in_refere
     ratio: 1,
     feature: { value: content, enabled: true, featureId: "aboutwelcome" },
   }));
   let recipe = ExperimentFakes.recipe("reference-aw", { branches });
   // Ensure we get enrolled
   recipe.bucketConfig.count = recipe.bucketConfig.total;
 
   const manager = ExperimentFakes.manager();
+  const enrollPromise = new Promise(resolve =>
+    manager.store.on("update:reference-aw", resolve)
+  );
   await manager.onStartup();
-  await manager.enroll(recipe);
+  await manager.enroll(recipe, "enroll_in_reference_aw_experiment");
+  await enrollPromise;
 
   Assert.ok(manager.store.get("reference-aw"), "Successful onboarding");
   let prefValue = Services.prefs.getStringPref(
     `${SYNC_DATA_PREF_BRANCH}aboutwelcome`
   );
   Assert.ok(
     prefValue,
     "aboutwelcome experiment enrollment should be stored to prefs"
@@ -246,41 +259,49 @@ add_task(async function enroll_in_refere
   // In case some regression causes us to store a significant amount of data
   // in prefs.
   Assert.ok(prefValue.length < 3498, "Make sure we don't bloat the prefs");
 });
 
 add_task(async function test_forceEnroll_cleanup() {
   const manager = ExperimentFakes.manager();
   const sandbox = sinon.createSandbox();
+  const fooEnrollPromise = new Promise(resolve =>
+    manager.store.on("update:foo", resolve)
+  );
+  const barEnrollPromise = new Promise(resolve =>
+    manager.store.on("update:optin-bar", resolve)
+  );
   let unenrollStub = sandbox.spy(manager, "unenroll");
   let existingRecipe = ExperimentFakes.recipe("foo", {
     branches: [
       {
         slug: "treatment",
         ratio: 1,
-        feature: { featureId: "force-enrollment", enabled: true },
+        feature: { featureId: "force-enrollment", enabled: true, value: {} },
       },
     ],
   });
   let forcedRecipe = ExperimentFakes.recipe("bar", {
     branches: [
       {
         slug: "treatment",
         ratio: 1,
-        feature: { featureId: "force-enrollment", enabled: true },
+        feature: { featureId: "force-enrollment", enabled: true, value: {} },
       },
     ],
   });
 
   await manager.onStartup();
-  await manager.enroll(existingRecipe);
+  await manager.enroll(existingRecipe, "test_forceEnroll_cleanup");
+  await fooEnrollPromise;
 
   let setExperimentActiveSpy = sandbox.spy(manager, "setExperimentActive");
   manager.forceEnroll(forcedRecipe, forcedRecipe.branches[0]);
+  await barEnrollPromise;
 
   Assert.ok(unenrollStub.called, "Unenrolled from existing experiment");
   Assert.equal(
     unenrollStub.firstCall.args[0],
     existingRecipe.slug,
     "Called with existing recipe slug"
   );
   Assert.ok(setExperimentActiveSpy.calledOnce, "Activated forced experiment");
--- a/toolkit/components/nimbus/test/unit/test_ExperimentManager_lifecycle.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_lifecycle.js
@@ -1,19 +1,16 @@
 "use strict";
 
 const { _ExperimentManager } = ChromeUtils.import(
   "resource://nimbus/lib/ExperimentManager.jsm"
 );
 const { ExperimentStore } = ChromeUtils.import(
   "resource://nimbus/lib/ExperimentStore.jsm"
 );
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { Sampling } = ChromeUtils.import(
   "resource://gre/modules/components-utils/Sampling.jsm"
 );
 const { TelemetryTestUtils } = ChromeUtils.import(
   "resource://testing-common/TelemetryTestUtils.jsm"
 );
 
 /**
@@ -158,18 +155,23 @@ add_task(async function test_onRecipe_is
     false,
     "should not add recipe to the store"
   );
 
   const fooRecipe = ExperimentFakes.recipe("foo");
   const updatedRecipe = ExperimentFakes.recipe("foo", {
     isEnrollmentPaused: true,
   });
-  await manager.enroll(fooRecipe);
+  let enrollmentPromise = new Promise(resolve =>
+    manager.store.on(`update:${fooRecipe.slug}`, resolve)
+  );
+  await manager.enroll(fooRecipe, "test");
+  await enrollmentPromise;
   await manager.onRecipe(updatedRecipe, "test");
+  console.log("XXX", manager.updateEnrollment.callCount);
   Assert.equal(
     manager.updateEnrollment.calledWith(updatedRecipe),
     true,
     "should still update existing recipes, even if enrollment is paused"
   );
 });
 
 /**
@@ -181,20 +183,25 @@ add_task(async function test_onFinalize_
   const manager = ExperimentFakes.manager();
   const sandbox = sinon.createSandbox();
   sandbox.spy(manager, "unenroll");
 
   await manager.onStartup();
 
   // Add an experiment to the store without calling .onRecipe
   // This simulates an enrollment having happened in the past.
-  manager.store.addExperiment(ExperimentFakes.experiment("foo"));
+  let recipe0 = ExperimentFakes.experiment("foo", {
+    experimentType: "unittest",
+    userFacingName: "foo",
+    userFacingDescription: "foo",
+    lastSeen: Date.now().toLocaleString(),
+    source: "test",
+  });
+  await manager.store.addExperiment(recipe0);
 
-  // Simulate adding some other recipes
-  await manager.onStartup();
   const recipe1 = ExperimentFakes.recipe("bar");
   // Unique features to prevent overlap
   recipe1.branches[0].feature.featureId = "red";
   recipe1.branches[1].feature.featureId = "red";
   await manager.onRecipe(recipe1, "test");
   const recipe2 = ExperimentFakes.recipe("baz");
   recipe2.branches[0].feature.featureId = "green";
   recipe2.branches[1].feature.featureId = "green";
--- a/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
@@ -1,13 +1,10 @@
 "use strict";
 
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { NormandyTestUtils } = ChromeUtils.import(
   "resource://testing-common/NormandyTestUtils.jsm"
 );
 const { TelemetryEvents } = ChromeUtils.import(
   "resource://normandy/lib/TelemetryEvents.jsm"
 );
 const { TelemetryEnvironment } = ChromeUtils.import(
   "resource://gre/modules/TelemetryEnvironment.jsm"
@@ -26,17 +23,17 @@ registerCleanupFunction(() => {
  * - set .active to false
  * - set experiment inactive in telemetry
  * - send unrollment event
  */
 add_task(async function test_set_inactive() {
   const manager = ExperimentFakes.manager();
 
   await manager.onStartup();
-  manager.store.addExperiment(ExperimentFakes.experiment("foo"));
+  await manager.store.addExperiment(ExperimentFakes.experiment("foo"));
 
   manager.unenroll("foo", "some-reason");
 
   Assert.equal(
     manager.store.get("foo").active,
     false,
     "should set .active to false"
   );
@@ -44,17 +41,17 @@ add_task(async function test_set_inactiv
 
 add_task(async function test_unenroll_opt_out() {
   globalSandbox.reset();
   Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, true);
   const manager = ExperimentFakes.manager();
   const experiment = ExperimentFakes.experiment("foo");
 
   await manager.onStartup();
-  manager.store.addExperiment(experiment);
+  await manager.store.addExperiment(experiment);
 
   Services.prefs.setBoolPref(STUDIES_OPT_OUT_PREF, false);
 
   Assert.equal(
     manager.store.get(experiment.slug).active,
     false,
     "should set .active to false"
   );
@@ -78,33 +75,33 @@ add_task(async function test_unenroll_op
 });
 
 add_task(async function test_setExperimentInactive_called() {
   globalSandbox.reset();
   const manager = ExperimentFakes.manager();
   const experiment = ExperimentFakes.experiment("foo");
 
   await manager.onStartup();
-  manager.store.addExperiment(experiment);
+  await manager.store.addExperiment(experiment);
 
   manager.unenroll("foo", "some-reason");
 
   Assert.ok(
     TelemetryEnvironment.setExperimentInactive.calledWith("foo"),
     "should call TelemetryEnvironment.setExperimentInactive with slug"
   );
 });
 
 add_task(async function test_send_unenroll_event() {
   globalSandbox.reset();
   const manager = ExperimentFakes.manager();
   const experiment = ExperimentFakes.experiment("foo");
 
   await manager.onStartup();
-  manager.store.addExperiment(experiment);
+  await manager.store.addExperiment(experiment);
 
   manager.unenroll("foo", "some-reason");
 
   Assert.ok(TelemetryEvents.sendEvent.calledOnce);
   Assert.deepEqual(
     TelemetryEvents.sendEvent.firstCall.args,
     [
       "unenroll",
@@ -121,17 +118,17 @@ add_task(async function test_send_unenro
 });
 
 add_task(async function test_undefined_reason() {
   globalSandbox.reset();
   const manager = ExperimentFakes.manager();
   const experiment = ExperimentFakes.experiment("foo");
 
   await manager.onStartup();
-  manager.store.addExperiment(experiment);
+  await manager.store.addExperiment(experiment);
 
   manager.unenroll("foo");
 
   const options = TelemetryEvents.sendEvent.firstCall?.args[3];
   Assert.ok(
     "reason" in options,
     "options object with .reason should be the fourth param"
   );
--- a/toolkit/components/nimbus/test/unit/test_ExperimentStore.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentStore.js
@@ -1,13 +1,10 @@
 "use strict";
 
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 const { ExperimentStore } = ChromeUtils.import(
   "resource://nimbus/lib/ExperimentStore.jsm"
 );
 const { FeatureManifest } = ChromeUtils.import(
   "resource://nimbus/FeatureManifest.js"
 );
 
 const { SYNC_DATA_PREF_BRANCH, SYNC_DEFAULTS_PREF_BRANCH } = ExperimentStore;
--- a/toolkit/components/nimbus/test/unit/test_FeatureManifest.js
+++ b/toolkit/components/nimbus/test/unit/test_FeatureManifest.js
@@ -1,17 +1,14 @@
 "use strict";
 
 const { FeatureManifest } = ChromeUtils.import(
   "resource://nimbus/FeatureManifest.js"
 );
 const { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
 Cu.importGlobalProperties(["fetch"]);
 
 XPCOMUtils.defineLazyGetter(this, "fetchSchema", async () => {
   const response = await fetch(
     "resource://testing-common/ExperimentFeatureManifest.schema.json"
   );
   const schema = await response.json();
   if (!schema) {
--- a/toolkit/components/nimbus/test/unit/test_SharedDataMap.js
+++ b/toolkit/components/nimbus/test/unit/test_SharedDataMap.js
@@ -2,19 +2,16 @@ const { SharedDataMap } = ChromeUtils.im
   "resource://nimbus/lib/SharedDataMap.jsm"
 );
 const { FileTestUtils } = ChromeUtils.import(
   "resource://testing-common/FileTestUtils.jsm"
 );
 const { TestUtils } = ChromeUtils.import(
   "resource://testing-common/TestUtils.jsm"
 );
-const { ExperimentFakes } = ChromeUtils.import(
-  "resource://testing-common/NimbusTestUtils.jsm"
-);
 
 const PATH = FileTestUtils.getTempFile("shared-data-map").path;
 
 function with_sharedDataMap(test) {
   let testTask = async () => {
     const sandbox = sinon.createSandbox();
     const instance = new SharedDataMap("xpcshell", {
       path: PATH,