Bug 1547034: PreferenceExperiments.start can take multiple prefs r=mythmon
authorEthan Glasser-Camp <ethan@betacantrips.com>
Thu, 16 May 2019 15:04:20 +0000
changeset 474247 454e86015872654981d491d0867f91e4c6685e6f
parent 474246 da4380fe5ce1ea4d7dba885f1127fac61860552e
child 474248 0de43d5275d15b83e80fa021bac4175f85a5d3a4
push id36027
push usershindli@mozilla.com
push dateFri, 17 May 2019 16:24:38 +0000
treeherdermozilla-central@c94c54aff466 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmythmon
bugs1547034
milestone68.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 1547034: PreferenceExperiments.start can take multiple prefs r=mythmon Add a little bit to some existing tests to cover this new functionality. Depends on D29871 Differential Revision: https://phabricator.services.mozilla.com/D29872
browser/components/tests/browser/browser_urlbar_matchBuckets_migration60.js
toolkit/components/normandy/actions/PreferenceExperimentAction.jsm
toolkit/components/normandy/lib/PreferenceExperiments.jsm
toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
toolkit/components/normandy/test/browser/browser_actions_PreferenceExperimentAction.js
--- a/browser/components/tests/browser/browser_urlbar_matchBuckets_migration60.js
+++ b/browser/components/tests/browser/browser_urlbar_matchBuckets_migration60.js
@@ -106,18 +106,25 @@ add_task(async function installStudyAndM
 // pref has unnecessary spaces in it, and then migrates.  The study should be
 // stopped and the pref should remain cleared.  i.e., the migration code should
 // parse the pref value and compare the resulting buckets instead of comparing
 // strings directly.
 add_task(async function installStudyPrefWithSpacesAndMigrate() {
   await sanityCheckInitialState();
 
   // Install the study.  It should set the pref.
-  let preferenceValue = " suggestion : 4, general : 5 ";
-  await PreferenceExperiments.start(newExperimentOpts({ preferenceValue }));
+  const preferenceValue = " suggestion : 4, general : 5 ";
+  const experiment = newExperimentOpts({
+    preferences: {
+      [PREF_NAME]: {
+        preferenceValue,
+      },
+    },
+  });
+  await PreferenceExperiments.start(experiment);
   Assert.ok(await PreferenceExperiments.has(STUDY_NAME),
             "Study installed");
   Assert.equal(Services.prefs.getCharPref(PREF_NAME, ""), preferenceValue,
                "Pref should be set by study");
 
   // Trigger migration.  The study should be stopped, and the pref should be
   // cleared since it's the default value.
   await promiseMigration();
@@ -133,17 +140,24 @@ add_task(async function installStudyPref
 // ("ratio": 1, "value": "general:3,suggestion:6") and migrates.  The study
 // should be stopped and the pref should be preserved since it's not the new
 // default.
 add_task(async function installStudyMinorityPrefAndMigrate() {
   await sanityCheckInitialState();
 
   // Install the study.  It should set the pref.
   let preferenceValue = "general:3,suggestion:6";
-  await PreferenceExperiments.start(newExperimentOpts({ preferenceValue }));
+  const experiment = newExperimentOpts({
+    preferences: {
+      [PREF_NAME]: {
+        preferenceValue,
+      },
+    },
+  });
+  await PreferenceExperiments.start(experiment);
   Assert.ok(await PreferenceExperiments.has(STUDY_NAME),
             "Study installed");
   Assert.equal(Services.prefs.getCharPref(PREF_NAME, ""), preferenceValue,
                "Pref should be set by study");
 
   // Trigger migration.  The study should be stopped, and the pref should remain
   // the same since it's a non-default value.  It should be set on the user
   // branch.
@@ -205,25 +219,36 @@ function promiseMigration() {
     return "migrateMatchBucketsPrefForUI66-done" == data;
   });
   Cc["@mozilla.org/browser/browserglue;1"]
     .getService(Ci.nsIObserver)
     .observe(null, topic, "migrateMatchBucketsPrefForUI66");
   return donePromise;
 }
 
-function newExperimentOpts(opts) {
+function newExperimentOpts(opts = {}) {
+  const defaultPref = {
+    [PREF_NAME]: {},
+  };
+  const defaultPrefInfo = {
+    preferenceValue: PREF_VALUE_SUGGESTIONS_FIRST,
+    preferenceBranchType: "default",
+    preferenceType: "string",
+  };
+  const preferences = {};
+  for (const [prefName, prefInfo] of Object.entries(opts.preferences || defaultPref)) {
+    preferences[prefName] = { ...defaultPrefInfo, ...prefInfo };
+  }
+
   return Object.assign({
     name: STUDY_NAME,
     branch: "branch",
-    preferenceName: PREF_NAME,
-    preferenceValue: PREF_VALUE_SUGGESTIONS_FIRST,
-    preferenceBranchType: "default",
-    preferenceType: "string",
-  }, opts);
+  }, opts, {
+    preferences,
+  });
 }
 
 async function getNonExpiredExperiment() {
   try {
     let exp = await PreferenceExperiments.get(STUDY_NAME);
     if (exp.expired) {
       return null;
     }
--- a/toolkit/components/normandy/actions/PreferenceExperimentAction.jsm
+++ b/toolkit/components/normandy/actions/PreferenceExperimentAction.jsm
@@ -72,20 +72,23 @@ class PreferenceExperimentAction extends
       }
 
       // Otherwise, enroll!
       const branch = await this.chooseBranch(slug, branches);
       const experimentType = isHighPopulation ? "exp-highpop" : "exp";
       await PreferenceExperiments.start({
         name: slug,
         branch: branch.slug,
-        preferenceName,
-        preferenceValue: branch.value,
-        preferenceBranchType,
-        preferenceType,
+        preferences: {
+          [preferenceName]: {
+            preferenceValue: branch.value,
+            preferenceBranchType,
+            preferenceType,
+          },
+        },
         experimentType,
       });
     } else {
       // If the experiment exists, and isn't expired, bump the lastSeen date.
       const experiment = await PreferenceExperiments.get(slug);
       if (experiment.expired) {
         this.log.debug(`Experiment ${slug} has expired, aborting.`);
       } else {
--- a/toolkit/components/normandy/lib/PreferenceExperiments.jsm
+++ b/toolkit/components/normandy/lib/PreferenceExperiments.jsm
@@ -376,90 +376,94 @@ var PreferenceExperiments = {
    * @rejects {Error}
    *   - If an experiment with the given name already exists
    *   - if an experiment for the given preference is active
    *   - If the given preferenceType does not match the existing stored preference
    */
   async start({
     name,
     branch,
-    preferenceName,
-    preferenceValue,
-    preferenceBranchType,
-    preferenceType,
+    preferences,
     experimentType = "exp",
   }) {
     log.debug(`PreferenceExperiments.start(${name}, ${branch})`);
 
     const store = await ensureStorage();
     if (name in store.data.experiments) {
       TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "name-conflict"});
       throw new Error(`A preference experiment named "${name}" already exists.`);
     }
 
     const activeExperiments = Object.values(store.data.experiments).filter(e => !e.expired);
-    const hasConflictingExperiment = activeExperiments.some(
-      e => e.preferences.hasOwnProperty(preferenceName)
-    );
-    if (hasConflictingExperiment) {
+    const preferencesWithConflicts = Object.keys(preferences).filter(preferenceName => {
+      return activeExperiments.some(
+        e => e.preferences.hasOwnProperty(preferenceName)
+      );
+    });
+
+    if (preferencesWithConflicts.length > 0) {
       TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "pref-conflict"});
       throw new Error(
-        `Another preference experiment for the pref "${preferenceName}" is currently active.`
+        `Another preference experiment for the pref "${preferencesWithConflicts[0]}" is currently active.`
       );
     }
 
-    const preferences = PreferenceBranchType[preferenceBranchType];
-    if (!preferences) {
-      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-branch"});
-      throw new Error(`Invalid value for preferenceBranchType: ${preferenceBranchType}`);
-    }
-
     if (experimentType.length > MAX_EXPERIMENT_SUBTYPE_LENGTH) {
       TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "experiment-type-too-long"});
       throw new Error(
         `experimentType must be less than ${MAX_EXPERIMENT_SUBTYPE_LENGTH} characters. ` +
         `"${experimentType}" is ${experimentType.length} long.`
       );
     }
 
-    const prevPrefType = Services.prefs.getPrefType(preferenceName);
-    const givenPrefType = PREFERENCE_TYPE_MAP[preferenceType];
+    // Sanity check each preference
+    for (const [preferenceName, preferenceInfo] of Object.entries(preferences)) {
+      const { preferenceBranchType, preferenceType } = preferenceInfo;
+      const preferenceBranch = PreferenceBranchType[preferenceBranchType];
+      if (!preferenceBranch) {
+        TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-branch"});
+        throw new Error(`Invalid value for preferenceBranchType: ${preferenceBranchType}`);
+      }
+
+      const prevPrefType = Services.prefs.getPrefType(preferenceName);
+      const givenPrefType = PREFERENCE_TYPE_MAP[preferenceType];
 
-    if (!preferenceType || !givenPrefType) {
-      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-type"});
-      throw new Error(`Invalid preferenceType provided (given "${preferenceType}")`);
+      if (!preferenceType || !givenPrefType) {
+        TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-type"});
+        throw new Error(`Invalid preferenceType provided (given "${preferenceType}")`);
+      }
+
+      if (prevPrefType !== Services.prefs.PREF_INVALID && prevPrefType !== givenPrefType) {
+        TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-type"});
+        throw new Error(
+          `Previous preference value is of type "${prevPrefType}", but was given ` +
+            `"${givenPrefType}" (${preferenceType})`
+        );
+      }
+
+      preferenceInfo.previousPreferenceValue = getPref(preferenceBranch, preferenceName, preferenceType);
     }
 
-    if (prevPrefType !== Services.prefs.PREF_INVALID && prevPrefType !== givenPrefType) {
-      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-type"});
-      throw new Error(
-        `Previous preference value is of type "${prevPrefType}", but was given ` +
-        `"${givenPrefType}" (${preferenceType})`
-      );
+    for (const [preferenceName, preferenceInfo] of Object.entries(preferences)) {
+      const { preferenceType, preferenceValue, preferenceBranchType } = preferenceInfo;
+      const preferenceBranch = PreferenceBranchType[preferenceBranchType];
+      setPref(preferenceBranch, preferenceName, preferenceType, preferenceValue);
     }
+    PreferenceExperiments.startObserver(name, preferences);
 
     /** @type {Experiment} */
     const experiment = {
       name,
       branch,
       expired: false,
       lastSeen: new Date().toJSON(),
-      preferences: {
-        [preferenceName]: {
-          preferenceBranchType,
-          preferenceValue,
-          preferenceType,
-          previousPreferenceValue: getPref(preferences, preferenceName, preferenceType),
-        },
-      },
+      preferences,
       experimentType,
     };
 
-    setPref(preferences, preferenceName, preferenceType, preferenceValue);
-    PreferenceExperiments.startObserver(name, {[preferenceName]: {preferenceType, preferenceValue}});
     store.data.experiments[name] = experiment;
     store.saveSoon();
 
     TelemetryEnvironment.setExperimentActive(name, branch, {type: EXPERIMENT_TYPE_PREFIX + experimentType});
     TelemetryEvents.sendEvent("enroll", "preference_study", name, {experimentType, branch});
     await this.saveStartupPrefs();
   },
 
--- a/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
+++ b/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
@@ -137,44 +137,56 @@ decorate_task(
 decorate_task(
   withMockExperiments([experimentFactory({ name: "test" })]),
   withSendEventStub,
   async function(experiments, sendEventStub) {
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "test",
         branch: "branch",
-        preferenceName: "fake.preference",
-        preferenceValue: "value",
-        preferenceType: "string",
-        preferenceBranchType: "default",
+        preferences: {
+          "fake.preference": {
+            preferenceValue: "value",
+            preferenceType: "string",
+            preferenceBranchType: "default",
+          },
+        },
       }),
       /test.*already exists/,
       "start threw an error due to a conflicting experiment name",
     );
 
     sendEventStub.assertEvents(
       [["enrollFailed", "preference_study", "test", {reason: "name-conflict"}]]
     );
   }
 );
 
-// start should throw if an experiment for the given preference is active
+// start should throw if an experiment for any of the given
+// preferences are active
 decorate_task(
-  withMockExperiments([experimentFactory({ name: "test", preferences: {"fake.preference": {}} })]),
+  withMockExperiments([experimentFactory({ name: "test", preferences: {"fake.preferenceinteger": {}} })]),
   withSendEventStub,
   async function(experiments, sendEventStub) {
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "different",
         branch: "branch",
-        preferenceName: "fake.preference",
-        preferenceValue: "value",
-        preferenceType: "string",
-        preferenceBranchType: "default",
+        preferences: {
+          "fake.preference": {
+            preferenceValue: "value",
+            preferenceType: "string",
+            preferenceBranchType: "default",
+          },
+          "fake.preferenceinteger": {
+            preferenceValue: 2,
+            preferenceType: "integer",
+            preferenceBranchType: "default",
+          },
+        },
       }),
       /another.*is currently active/i,
       "start threw an error due to an active experiment for the given preference",
     );
 
     sendEventStub.assertEvents(
       [["enrollFailed", "preference_study", "different", {reason: "pref-conflict"}]]
     );
@@ -185,111 +197,151 @@ decorate_task(
 decorate_task(
   withMockExperiments(),
   withSendEventStub,
   async function(experiments, sendEventStub) {
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "test",
         branch: "branch",
-        preferenceName: "fake.preference",
-        preferenceValue: "value",
-        preferenceType: "string",
-        preferenceBranchType: "invalid",
+        preferences: {
+          "fake.preference": {
+            preferenceValue: "value",
+            preferenceType: "string",
+            preferenceBranchType: "invalid",
+          },
+        },
       }),
       /invalid value for preferenceBranchType: invalid/i,
       "start threw an error due to an invalid preference branch type",
     );
 
     sendEventStub.assertEvents(
       [["enrollFailed", "preference_study", "test", {reason: "invalid-branch"}]]
     );
   }
 );
 
-// start should save experiment data, modify the preference, and register a
+// start should save experiment data, modify preferences, and register a
 // watcher.
 decorate_task(
   withMockExperiments(),
   withMockPreferences,
   withStub(PreferenceExperiments, "startObserver"),
   withSendEventStub,
   async function testStart(experiments, mockPreferences, startObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference", "oldvalue", "default");
     mockPreferences.set("fake.preference", "uservalue", "user");
+    mockPreferences.set("fake.preferenceinteger", 1, "default");
+    mockPreferences.set("fake.preferenceinteger", 101, "user");
 
-    await PreferenceExperiments.start({
+    const experiment = {
       name: "test",
       branch: "branch",
-      preferenceName: "fake.preference",
-      preferenceValue: "newvalue",
-      preferenceBranchType: "default",
-      preferenceType: "string",
-    });
+      preferences: {
+        "fake.preference": {
+          preferenceValue: "newvalue",
+          preferenceBranchType: "default",
+          preferenceType: "string",
+        },
+        "fake.preferenceinteger": {
+          preferenceValue: 2,
+          preferenceBranchType: "default",
+          preferenceType: "integer",
+        },
+      },
+    };
+    await PreferenceExperiments.start(experiment);
     ok(await PreferenceExperiments.get("test"), "start saved the experiment");
     ok(
-      startObserverStub.calledWith("test", {"fake.preference": {preferenceType: "string", preferenceValue: "newvalue"}}),
+      startObserverStub.calledWith("test", experiment.preferences),
       "start registered an observer",
     );
 
     const expectedExperiment = {
       name: "test",
       branch: "branch",
       expired: false,
       preferences: {
         "fake.preference": {
           preferenceValue: "newvalue",
           preferenceType: "string",
           previousPreferenceValue: "oldvalue",
           preferenceBranchType: "default",
         },
+        "fake.preferenceinteger": {
+          preferenceValue: 2,
+          preferenceType: "integer",
+          previousPreferenceValue: 1,
+          preferenceBranchType: "default",
+        },
       },
     };
-    const experiment = {};
+    const experimentSubset = {};
     const actualExperiment = await PreferenceExperiments.get("test");
-    Object.keys(expectedExperiment).forEach(key => experiment[key] = actualExperiment[key]);
-    Assert.deepEqual(experiment, expectedExperiment, "start saved the experiment");
+    Object.keys(expectedExperiment).forEach(key => experimentSubset[key] = actualExperiment[key]);
+    Assert.deepEqual(experimentSubset, expectedExperiment, "start saved the experiment");
 
     is(
       DefaultPreferences.get("fake.preference"),
       "newvalue",
       "start modified the default preference",
     );
     is(
       Preferences.get("fake.preference"),
       "uservalue",
       "start did not modify the user preference",
     );
     is(
       Preferences.get(`${startupPrefs}.fake.preference`),
       "newvalue",
       "start saved the experiment value to the startup prefs tree",
     );
+    is(
+      DefaultPreferences.get("fake.preferenceinteger"),
+      2,
+      "start modified the default preference",
+    );
+    is(
+      Preferences.get("fake.preferenceinteger"),
+      101,
+      "start did not modify the user preference",
+    );
+    is(
+      Preferences.get(`${startupPrefs}.fake.preferenceinteger`),
+      2,
+      "start saved the experiment value to the startup prefs tree",
+    );
   },
 );
 
 // start should modify the user preference for the user branch type
 decorate_task(
   withMockExperiments(),
   withMockPreferences,
   withStub(PreferenceExperiments, "startObserver"),
   async function(experiments, mockPreferences, startObserver) {
     mockPreferences.set("fake.preference", "olddefaultvalue", "default");
     mockPreferences.set("fake.preference", "oldvalue", "user");
 
-    await PreferenceExperiments.start({
+    const experiment = {
       name: "test",
       branch: "branch",
-      preferenceName: "fake.preference",
-      preferenceValue: "newvalue",
-      preferenceType: "string",
-      preferenceBranchType: "user",
-    });
+      preferences: {
+        "fake.preference": {
+          preferenceValue: "newvalue",
+          preferenceType: "string",
+          preferenceBranchType: "user",
+        },
+      },
+    };
+    await PreferenceExperiments.start(experiment);
     ok(
-      startObserver.calledWith("test", {"fake.preference": {preferenceType: "string", preferenceValue: "newvalue"}}),
+
+      startObserver.calledWith("test", experiment.preferences),
       "start registered an observer",
     );
 
     const expectedExperiment = {
       name: "test",
       branch: "branch",
       expired: false,
       preferences: {
@@ -297,20 +349,20 @@ decorate_task(
           preferenceValue: "newvalue",
           preferenceType: "string",
           previousPreferenceValue: "oldvalue",
           preferenceBranchType: "user",
         },
       },
     };
 
-    const experiment = {};
+    const experimentSubset = {};
     const actualExperiment = await PreferenceExperiments.get("test");
-    Object.keys(expectedExperiment).forEach(key => experiment[key] = actualExperiment[key]);
-    Assert.deepEqual(experiment, expectedExperiment, "start saved the experiment");
+    Object.keys(expectedExperiment).forEach(key => experimentSubset[key] = actualExperiment[key]);
+    Assert.deepEqual(experimentSubset, expectedExperiment, "start saved the experiment");
 
     Assert.notEqual(
       DefaultPreferences.get("fake.preference"),
       "newvalue",
       "start did not modify the default preference",
     );
     is(Preferences.get("fake.preference"), "newvalue", "start modified the user preference");
   }
@@ -322,20 +374,23 @@ decorate_task(
   withSendEventStub,
   async function(mockPreferences, sendEventStub) {
     mockPreferences.set("fake.type_preference", "oldvalue");
 
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "test",
         branch: "branch",
-        preferenceName: "fake.type_preference",
-        preferenceBranchType: "user",
-        preferenceValue: 12345,
-        preferenceType: "integer",
+        preferences: {
+          "fake.type_preference": {
+            preferenceBranchType: "user",
+            preferenceValue: 12345,
+            preferenceType: "integer",
+          },
+        },
       }),
       /previous preference value is of type/i,
       "start threw error for incompatible preference type"
     );
 
     sendEventStub.assertEvents(
       [["enrollFailed", "preference_study", "test", {reason: "invalid-type"}]]
     );
@@ -881,20 +936,23 @@ decorate_task(
   withMockExperiments(),
   withStub(TelemetryEnvironment, "setExperimentActive"),
   withStub(TelemetryEnvironment, "setExperimentInactive"),
   withSendEventStub,
   async function testStartAndStopTelemetry(experiments, setActiveStub, setInactiveStub, sendEventStub) {
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
-      preferenceName: "fake.preference",
-      preferenceValue: "value",
-      preferenceType: "string",
-      preferenceBranchType: "default",
+      preferences: {
+        "fake.preference": {
+          preferenceValue: "value",
+          preferenceType: "string",
+          preferenceBranchType: "default",
+        },
+      },
     });
 
     Assert.deepEqual(
       setActiveStub.getCall(0).args,
       ["test", "branch", { type: "normandy-exp" }],
       "Experiment is registered by start()",
     );
     await PreferenceExperiments.stop("test", { reason: "test-reason" });
@@ -921,20 +979,23 @@ decorate_task(
   withMockExperiments(),
   withStub(TelemetryEnvironment, "setExperimentActive"),
   withStub(TelemetryEnvironment, "setExperimentInactive"),
   withSendEventStub,
   async function testInitTelemetryExperimentType(experiments, setActiveStub, setInactiveStub, sendEventStub) {
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
-      preferenceName: "fake.preference",
-      preferenceValue: "value",
-      preferenceType: "string",
-      preferenceBranchType: "default",
+      preferences: {
+        "fake.preference": {
+          preferenceValue: "value",
+          preferenceType: "string",
+          preferenceBranchType: "default",
+        },
+      },
       experimentType: "pref-test",
     });
 
     Assert.deepEqual(
       setActiveStub.getCall(0).args,
       ["test", "branch", { type: "normandy-pref-test" }],
       "start() should register the experiment with the provided type",
     );
@@ -1155,20 +1216,23 @@ decorate_task(
   async function testDefaultBranchStop(mockExperiments, mockPreferences) {
     const prefName = "fake.preference";
     mockPreferences.set(prefName, "old version's value", "default");
 
     // start an experiment
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
-      preferenceName: prefName,
-      preferenceValue: "experiment value",
-      preferenceBranchType: "default",
-      preferenceType: "string",
+      preferences: {
+        [prefName]: {
+          preferenceValue: "experiment value",
+          preferenceBranchType: "default",
+          preferenceType: "string",
+        },
+      },
     });
 
     is(
       Services.prefs.getCharPref(prefName),
       "experiment value",
       "Starting an experiment should change the pref",
     );
 
@@ -1203,20 +1267,23 @@ decorate_task(
   async function testDefaultBranchStop(mockExperiments, mockPreferences) {
     const prefName = "fake.preference";
     mockPreferences.set(prefName, "old version's value", "default");
 
     // start an experiment
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
-      preferenceName: prefName,
-      preferenceValue: "experiment value",
-      preferenceBranchType: "default",
-      preferenceType: "string",
+      preferences: {
+        [prefName]: {
+          preferenceValue: "experiment value",
+          preferenceBranchType: "default",
+          preferenceType: "string",
+        },
+      },
     });
 
     is(
       Services.prefs.getCharPref(prefName),
       "experiment value",
       "Starting an experiment should change the pref",
     );
 
--- a/toolkit/components/normandy/test/browser/browser_actions_PreferenceExperimentAction.js
+++ b/toolkit/components/normandy/test/browser/browser_actions_PreferenceExperimentAction.js
@@ -93,20 +93,23 @@ decorate_task(
     });
 
     await action.runRecipe(recipe);
     await action.finalize();
 
     Assert.deepEqual(startStub.args, [[{
       name: "test",
       branch: "branch1",
-      preferenceName: "fake.preference",
-      preferenceValue: "branch1",
-      preferenceBranchType: "user",
-      preferenceType: "string",
+      preferences: {
+        "fake.preference": {
+          preferenceValue: "branch1",
+          preferenceBranchType: "user",
+          preferenceType: "string",
+        },
+      },
       experimentType: "exp",
     }]]);
   }
 );
 
 decorate_task(
   withStub(PreferenceExperiments, "markLastSeen"),
   PreferenceExperiments.withMockExperiments([{name: "test", expired: false}]),