Bug 1436113 - Part 2: Refactor "shield-recipe-client" to "normandy" draft
authorMike Cooper <mcooper@mozilla.com>
Wed, 21 Feb 2018 15:02:04 -0800
changeset 761371 2237e1584c32d3a77639d179f7006f60216eab10
parent 761370 4b8e21686fd84c2948ddca2ea09ff4d618c4f159
push id100945
push userbmo:mcooper@mozilla.com
push dateWed, 28 Feb 2018 23:49:58 +0000
bugs1436113
milestone60.0a1
Bug 1436113 - Part 2: Refactor "shield-recipe-client" to "normandy" This includes simplifiying the startup process, migrating to a new pref namespace, renaming files, and updating references to the code. MozReview-Commit-ID: A2cYpsjCOAE
browser/app/profile/firefox.js
browser/components/enterprisepolicies/tests/browser/browser_policy_disable_shield.js
layout/tools/reftest/reftest-preferences.js
testing/geckodriver/CHANGES.md
testing/geckodriver/src/prefs.rs
testing/marionette/client/marionette_driver/geckoinstance.py
testing/marionette/server.js
testing/profiles/prefs_general.js
testing/talos/talos/config.py
testing/xpcshell/head.js
toolkit/components/normandy/Normandy.jsm
toolkit/components/normandy/content/AboutPages.jsm
toolkit/components/normandy/docs/data-collection.rst
toolkit/components/normandy/lib/ClientEnvironment.jsm
toolkit/components/normandy/lib/LogManager.jsm
toolkit/components/normandy/lib/NormandyApi.jsm
toolkit/components/normandy/lib/PreferenceExperiments.jsm
toolkit/components/normandy/lib/RecipeRunner.jsm
toolkit/components/normandy/lib/Uptake.jsm
toolkit/components/normandy/test/browser/browser.ini
toolkit/components/normandy/test/browser/browser_ClientEnvironment.js
toolkit/components/normandy/test/browser/browser_Normandy.js
toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
toolkit/components/normandy/test/browser/browser_RecipeRunner.js
toolkit/components/normandy/test/browser/browser_ShieldRecipeClient.js
toolkit/components/normandy/test/browser/browser_about_studies.js
toolkit/components/normandy/test/unit/test_NormandyApi.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1765,8 +1765,21 @@ pref("browser.chrome.errorReporter.sampl
 pref("browser.chrome.errorReporter.publicKey", "c709cb7a2c0b4f0882fcc84a5af161ec");
 pref("browser.chrome.errorReporter.projectId", "339");
 pref("browser.chrome.errorReporter.submitUrl", "https://sentry.prod.mozaws.net/api/339/store/");
 pref("browser.chrome.errorReporter.logLevel", "Error");
 
 // URL for Learn More link for browser error logging in preferences
 pref("browser.chrome.errorReporter.infoURL",
      "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/nightly-error-collection");
+
+// Normandy client preferences
+pref("app.normandy.api_url", "https://normandy.cdn.mozilla.net/api/v1");
+pref("app.normandy.dev_mode", false);
+pref("app.normandy.enabled", true);
+pref("app.normandy.logging.level", 50); // Warn
+pref("app.normandy.run_interval_seconds", 86400); // 24 hours
+pref("app.normandy.shieldLearnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield");
+#ifdef MOZ_DATA_REPORTING
+pref("app.shield.optoutstudies.enabled", true);
+#else
+pref("app.shield.optoutstudies.enabled", false);
+#endif
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_disable_shield.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_disable_shield.js
@@ -1,18 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(async function test_policy_disable_shield() {
   const { RecipeRunner } = ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", {});
 
-  await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.api_url",
-                                            "https://localhost/selfsupport-dummy/"],
+  await SpecialPowers.pushPrefEnv({ set: [["app.normandy.api_url",
+                                           "https://localhost/selfsupport-dummy/"],
                                           ["datareporting.healthreport.uploadEnabled",
                                             true]]});
 
   ok(RecipeRunner, "RecipeRunner exists");
   RecipeRunner.checkPrefs();
   is(RecipeRunner.enabled, true, "RecipeRunner is enabled");
 
   await setupPolicyEngineWithJson({
--- a/layout/tools/reftest/reftest-preferences.js
+++ b/layout/tools/reftest/reftest-preferences.js
@@ -59,17 +59,17 @@ user_pref("layout.interruptible-reflow.e
 
 // Tell the search service we are running in the US.  This also has the
 // desired side-effect of preventing our geoip lookup.
 user_pref("browser.search.isUS", true);
 user_pref("browser.search.countryCode", "US");
 user_pref("browser.search.geoSpecificDefaults", false);
 
 // Make sure Shield doesn't hit the network.
-user_pref("extensions.shield-recipe-client.api_url", "https://localhost/selfsupport-dummy/");
+user_pref("app.normandy.api_url", "https://localhost/selfsupport-dummy/");
 
 // Make sure Ping Centre doesn't hit the network.
 user_pref("browser.ping-centre.staging.endpoint", "https://localhost");
 user_pref("browser.ping-centre.production.endpoint", "https://localhost");
 
 // use about:blank, not browser.startup.homepage
 user_pref("browser.startup.page", 0);
 
--- a/testing/geckodriver/CHANGES.md
+++ b/testing/geckodriver/CHANGES.md
@@ -4,33 +4,35 @@ Change log
 All notable changes to this program is documented in this file.
 
 Unreleased
 ----------
 
 ### Added
 
 - New `--jsdebugger` flag to open the Browser Toolbox when Firefox
-  launches.  This is useful for debugging Marionette internals
+  launches.  This is useful for debugging Marionette internals.
 
 - Introduced the temporary, boolean capability
   `moz:useNonSpecCompliantPointerOrigin` to disable the WebDriver
-  conforming behavior of calculating the Pointer Origin
+  conforming behavior of calculating the Pointer Origin.
 
 ### Changed
 
 - HTTP status code for the [`StaleElementReference`] error changed
   from 400 (Bad Request) to 404 (Not Found)
 
 - Backtraces from geckodriver no longer substitute for missing
   Marionette stacktraces
 
 - `Delete Session` now allows Firefox to safely shutdown within 70s before
   force-killing the process
 
+- Changed preference used to disable shield studies to `app.normandy.api_url`.
+
 ### Fixed
 
 - Improved error messages for malformed capabilities
 
 
 0.19.1 (2017-10-30)
 -------------------
 
--- a/testing/geckodriver/src/prefs.rs
+++ b/testing/geckodriver/src/prefs.rs
@@ -147,17 +147,17 @@ lazy_static! {
 
         // Disable metadata caching for installed add-ons by default
         ("extensions.getAddons.cache.enabled", Pref::new(false)),
 
         // Disable intalling any distribution extensions or add-ons
         ("extensions.installDistroAddons", Pref::new(false)),
 
         // Make sure Shield doesn't hit the network.
-        ("extensions.shield-recipe-client.api_url", Pref::new("")),
+        ("app.normandy.api_url", Pref::new("")),
 
         ("extensions.showMismatchUI", Pref::new(false)),
 
         // Turn off extension updates so they do not bother tests
         ("extensions.update.enabled", Pref::new(false)),
         ("extensions.update.notifyUser", Pref::new(false)),
 
         // Make sure opening about:addons will not hit the network
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -48,17 +48,17 @@ class GeckoInstance(object):
         # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
         "extensions.autoDisableScopes": 0,
         "extensions.enabledScopes": 5,
         # Disable metadata caching for installed add-ons by default
         "extensions.getAddons.cache.enabled": False,
         # Disable intalling any distribution add-ons
         "extensions.installDistroAddons": False,
         # Make sure Shield doesn't hit the network.
-        "extensions.shield-recipe-client.api_url": "",
+        "app.normandy.api_url": "",
         "extensions.showMismatchUI": False,
         # Turn off extension updates so they don't bother tests
         "extensions.update.enabled": False,
         "extensions.update.notifyUser": False,
         # Make sure opening about:addons won"t hit the network
         "extensions.webservice.discoverURL": "http://%(server)s/dummy/discoveryURL",
 
         # Allow the application to have focus even it runs in the background
--- a/testing/marionette/server.js
+++ b/testing/marionette/server.js
@@ -207,17 +207,17 @@ const RECOMMENDED_PREFS = new Map([
   // Disable metadata caching for installed add-ons by default
   ["extensions.getAddons.cache.enabled", false],
 
   // Disable installing any distribution extensions or add-ons.
   // Should be set in profile.
   ["extensions.installDistroAddons", false],
 
   // Make sure Shield doesn't hit the network.
-  ["extensions.shield-recipe-client.api_url", ""],
+  ["app.normandy.api_url", ""],
 
   ["extensions.showMismatchUI", false],
 
   // Turn off extension updates so they do not bother tests
   ["extensions.update.enabled", false],
   ["extensions.update.notifyUser", false],
 
   // Make sure opening about:addons will not hit the network
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -312,17 +312,17 @@ user_pref("browser.uitour.url", "http://
 // Tell the search service we are running in the US.  This also has the desired
 // side-effect of preventing our geoip lookup.
 user_pref("browser.search.isUS", true);
 user_pref("browser.search.countryCode", "US");
 // This will prevent HTTP requests for region defaults.
 user_pref("browser.search.geoSpecificDefaults", false);
 
 // Make sure Shield doesn't hit the network.
-user_pref("extensions.shield-recipe-client.api_url", "");
+user_pref("app.normandy.api_url", "");
 
 // Make sure PingCentre doesn't hit the network.
 user_pref("browser.ping-centre.staging.endpoint", "");
 user_pref("browser.ping-centre.production.endpoint", "");
 
 user_pref("media.eme.enabled", true);
 
 // Set the number of shmems the PChromiumCDM protocol pre-allocates to 0,
--- a/testing/talos/talos/config.py
+++ b/testing/talos/talos/config.py
@@ -163,17 +163,17 @@ DEFAULTS = dict(
             '/repositoryGetWithPerformanceURL',
         'extensions.getAddons.search.browseURL':
             'http://127.0.0.1/extensions-dummy/repositoryBrowseURL',
         'media.gmp-manager.url':
             'http://127.0.0.1/gmpmanager-dummy/update.xml',
         'media.gmp-manager.updateEnabled': False,
         'extensions.systemAddon.update.url':
             'http://127.0.0.1/dummy-system-addons.xml',
-        'extensions.shield-recipe-client.api_url':
+        'app.normandy.api_url':
             'https://127.0.0.1/selfsupport-dummy/',
         'browser.ping-centre.staging.endpoint':
             'https://127.0.0.1/pingcentre/dummy/',
         'browser.ping-centre.production.endpoint':
             'https://127.0.0.1/pingcentre/dummy/',
         'media.navigator.enabled': True,
         'media.peerconnection.enabled': True,
         'media.navigator.permission.disabled': True,
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -1468,18 +1468,17 @@ try {
   }
 } catch (e) { }
 // We need to avoid hitting the network with certain components.
 try {
   if (runningInParent) {
     _Services.prefs.setCharPref("media.gmp-manager.url.override", "http://%(server)s/dummy-gmp-manager.xml");
     _Services.prefs.setCharPref("media.gmp-manager.updateEnabled", false);
     _Services.prefs.setCharPref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
-    _Services.prefs.setCharPref("extensions.shield-recipe-client.api_url",
-                                "https://%(server)s/selfsupport-dummy/");
+    _Services.prefs.setCharPref("app.normandy.api_url", "https://%(server)s/selfsupport-dummy/");
     _Services.prefs.setCharPref("toolkit.telemetry.server", "https://%(server)s/telemetry-dummy");
     _Services.prefs.setCharPref("browser.search.geoip.url", "https://%(server)s/geoip-dummy");
     _Services.prefs.setCharPref("browser.safebrowsing.downloads.remote.url", "https://%(server)s/safebrowsing-dummy");
   }
 } catch (e) { }
 
 // Make tests run consistently on DevEdition (which has a lightweight theme
 // selected by default).
--- a/toolkit/components/normandy/Normandy.jsm
+++ b/toolkit/components/normandy/Normandy.jsm
@@ -3,101 +3,157 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "LogManager",
-  "resource://normandy/lib/LogManager.jsm");
-ChromeUtils.defineModuleGetter(this, "ShieldRecipeClient",
-  "resource://normandy/lib/ShieldRecipeClient.jsm");
-ChromeUtils.defineModuleGetter(this, "PreferenceExperiments",
-  "resource://normandy/lib/PreferenceExperiments.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AboutPages: "resource://normandy-content/AboutPages.jsm",
+  AddonStudies: "resource://normandy/lib/AddonStudies.jsm",
+  CleanupManager: "resource://normandy/lib/CleanupManager.jsm",
+  LogManager: "resource://normandy/lib/LogManager.jsm",
+  PreferenceExperiments: "resource://normandy/lib/PreferenceExperiments.jsm",
+  RecipeRunner: "resource://normandy/lib/RecipeRunner.jsm",
+  ShieldPreferences: "resource://normandy/lib/ShieldPreferences.jsm",
+  TelemetryEvents: "resource://normandy/lib/TelemetryEvents.jsm",
+});
 
 var EXPORTED_SYMBOLS = ["Normandy"];
 
 const UI_AVAILABLE_NOTIFICATION = "sessionstore-windows-restored";
-const STARTUP_EXPERIMENT_PREFS_BRANCH = "extensions.shield-recipe-client.startupExperimentPrefs.";
-const PREF_LOGGING_LEVEL = "extensions.shield-recipe-client.logging.level";
-const BOOTSTRAP_LOGGER_NAME = "extensions.shield-recipe-client.bootstrap";
-const DEFAULT_PREFS = {
-  "extensions.shield-recipe-client.api_url": "https://normandy.cdn.mozilla.net/api/v1",
-  "extensions.shield-recipe-client.dev_mode": false,
-  "extensions.shield-recipe-client.enabled": true,
-  "extensions.shield-recipe-client.startup_delay_seconds": 300,
-  "extensions.shield-recipe-client.logging.level": Log.Level.Warn,
-  "extensions.shield-recipe-client.user_id": "",
-  "extensions.shield-recipe-client.run_interval_seconds": 86400, // 24 hours
-  "extensions.shield-recipe-client.first_run": true,
-  "extensions.shield-recipe-client.shieldLearnMoreUrl": (
-    "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield"
-  ),
-  "app.shield.optoutstudies.enabled": AppConstants.MOZ_DATA_REPORTING,
-};
+const BOOTSTRAP_LOGGER_NAME = "app.normandy.bootstrap";
+const SHIELD_INIT_NOTIFICATION = "shield-init-complete";
+
+const PREF_PREFIX = "app.normandy";
+const LEGACY_PREF_PREFIX = "extensions.shield-recipe-client";
+const STARTUP_EXPERIMENT_PREFS_BRANCH = `${PREF_PREFIX}.startupExperimentPrefs.`;
+const PREF_LOGGING_LEVEL = `${PREF_PREFIX}.logging.level`;
 
 // Logging
 const log = Log.repository.getLogger(BOOTSTRAP_LOGGER_NAME);
 log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
 log.level = Services.prefs.getIntPref(PREF_LOGGING_LEVEL, Log.Level.Warn);
 
 let studyPrefsChanged = {};
 
 var Normandy = {
   init() {
     // Initialization that needs to happen before the first paint on startup.
-    this.initShieldPrefs(DEFAULT_PREFS);
+    this.migrateShieldPrefs();
     this.initExperimentPrefs();
 
     // Wait until the UI is available before finishing initialization.
     Services.obs.addObserver(this, UI_AVAILABLE_NOTIFICATION);
   },
 
   observe(subject, topic, data) {
     if (topic === UI_AVAILABLE_NOTIFICATION) {
       Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION);
       this.finishInit();
     }
   },
 
   async finishInit() {
     await PreferenceExperiments.recordOriginalValues(studyPrefsChanged);
-    ShieldRecipeClient.startup();
+
+    // Setup logging and listen for changes to logging prefs
+    LogManager.configure(Services.prefs.getIntPref(PREF_LOGGING_LEVEL, Log.Level.Warn));
+    Services.prefs.addObserver(PREF_LOGGING_LEVEL, LogManager.configure);
+    CleanupManager.addCleanupHandler(
+      () => Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure),
+    );
+
+    try {
+      TelemetryEvents.init();
+    } catch (err) {
+      log.error("Failed to initialize telemetry events:", err);
+    }
+
+    try {
+      await AboutPages.init();
+    } catch (err) {
+      log.error("Failed to initialize about pages:", err);
+    }
+
+    try {
+      await AddonStudies.init();
+    } catch (err) {
+      log.error("Failed to initialize addon studies:", err);
+    }
+
+    try {
+      await PreferenceExperiments.init();
+    } catch (err) {
+      log.error("Failed to initialize preference experiments:", err);
+    }
+
+    try {
+      ShieldPreferences.init();
+    } catch (err) {
+      log.error("Failed to initialize preferences UI:", err);
+    }
+
+    await RecipeRunner.init();
+    Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION);
   },
 
   async uninit() {
-    // Wait for async write operations during shutdown before unloading modules.
-    await ShieldRecipeClient.shutdown();
+    await CleanupManager.cleanup();
+    Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure);
 
     // In case the observer didn't run, clean it up.
     try {
       Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION);
     } catch (err) {
       // It must already be removed!
     }
   },
 
-  initShieldPrefs(defaultPrefs) {
-    const prefBranch = Services.prefs.getDefaultBranch("");
-    for (const [name, value] of Object.entries(defaultPrefs)) {
-      switch (typeof value) {
-        case "string":
-          prefBranch.setCharPref(name, value);
+  migrateShieldPrefs() {
+    const legacyBranch = Services.prefs.getBranch(LEGACY_PREF_PREFIX + ".");
+    const newBranch = Services.prefs.getBranch(PREF_PREFIX + ".");
+
+    for (const prefName of legacyBranch.getChildList("")) {
+      const legacyPrefType = legacyBranch.getPrefType(prefName);
+      const newPrefType = newBranch.getPrefType(prefName);
+
+      // If new preference exists and is not the same as the legacy pref, skip it
+      if (newPrefType !== Services.prefs.PREF_INVALID && newPrefType !== legacyPrefType) {
+        log.error(`Error migrating normandy pref ${prefName}; pref type does not match.`);
+        continue;
+      }
+
+      // Now move the value over. If it matches the default, this will be a no-op
+      switch (legacyPrefType) {
+        case Services.prefs.PREF_STRING:
+          newBranch.setCharPref(prefName, legacyBranch.getCharPref(prefName));
           break;
-        case "number":
-          prefBranch.setIntPref(name, value);
+
+        case Services.prefs.PREF_INT:
+          newBranch.setIntPref(prefName, legacyBranch.getIntPref(prefName));
+          break;
+
+        case Services.prefs.PREF_BOOL:
+          newBranch.setBoolPref(prefName, legacyBranch.getBoolPref(prefName));
           break;
-        case "boolean":
-          prefBranch.setBoolPref(name, value);
+
+        case Services.prefs.PREF_INVALID:
+          // This should never happen.
+          log.error(`Error migrating pref ${prefName}; pref type is invalid (${legacyPrefType}).`);
           break;
+
         default:
-          throw new Error(`Invalid default preference type ${typeof value}`);
+          // This should never happen either.
+          log.error(`Error getting startup pref ${prefName}; unknown value type ${legacyPrefType}.`);
       }
+
+      legacyBranch.clearUserPref(prefName);
     }
   },
 
   initExperimentPrefs() {
     studyPrefsChanged = {};
     const defaultBranch = Services.prefs.getDefaultBranch("");
     const experimentBranch = Services.prefs.getBranch(STARTUP_EXPERIMENT_PREFS_BRANCH);
 
--- a/toolkit/components/normandy/content/AboutPages.jsm
+++ b/toolkit/components/normandy/content/AboutPages.jsm
@@ -14,17 +14,17 @@ ChromeUtils.defineModuleGetter(
   this, "AddonStudies", "resource://normandy/lib/AddonStudies.jsm",
 );
 ChromeUtils.defineModuleGetter(
   this, "RecipeRunner", "resource://normandy/lib/RecipeRunner.jsm",
 );
 
 var EXPORTED_SYMBOLS = ["AboutPages"];
 
-const SHIELD_LEARN_MORE_URL_PREF = "extensions.shield-recipe-client.shieldLearnMoreUrl";
+const SHIELD_LEARN_MORE_URL_PREF = "app.normandy.shieldLearnMoreUrl";
 
 // Due to bug 1051238 frame scripts are cached forever, so we can't update them
 // as a restartless add-on. The Math.random() is the work around for this.
 const PROCESS_SCRIPT = (
   `resource://normandy-content/shield-content-process.js?${Math.random()}`
 );
 const FRAME_SCRIPT = (
   `resource://normandy-content/shield-content-frame.js?${Math.random()}`
--- a/toolkit/components/normandy/docs/data-collection.rst
+++ b/toolkit/components/normandy/docs/data-collection.rst
@@ -1,27 +1,27 @@
 Data Collection
 ===============
-This document describes the types of data that Shield collects.
+This document describes the types of data that Normandy collects.
 
 Uptake
 ------
-Shield monitors the execution of recipes and reports to
+Normandy monitors the execution of recipes and reports to
 :ref:`telemetry` the amount of successful and failed runs. This data
 is reported using :ref:`telemetry/collection/uptake` under the
-``shield-recipe-client`` namespace.
+``normandy`` namespace.
 
 Runner Status
 ^^^^^^^^^^^^^
 Once per-fetch and execution of recipes, one of the following statuses is
-reported under the key ``shield-recipe-client/runner``:
+reported under the key ``normandy/runner``:
 
 .. data:: RUNNER_INVALID_SIGNATURE
 
-   Shield failed to verify the signature of the fetched recipes.
+   Normandy failed to verify the signature of the fetched recipes.
 
 .. data:: RUNNER_NETWORK_ERROR
 
    There was a network-related error while fetching recipes.
 
 .. data:: RUNNER_SERVER_ERROR
 
    The data returned by the server when fetching the recipe is invalid in some
@@ -29,19 +29,19 @@ reported under the key ``shield-recipe-c
 
 .. data:: RUNNER_SUCCESS
 
    The operation completed successfully. Individual failures with actions and
    recipes may have been reported separately.
 
 Action Status
 ^^^^^^^^^^^^^
-For each action available from the Shield service, one of the
+For each action available from the Normandy service, one of the
 following statuses is reported under the key
-``shield-recipe-client/action/<action name>``:
+``normandy/action/<action name>``:
 
 .. data:: ACTION_NETWORK_ERROR
 
    There was a network-related error while fetching actions
 
 .. data:: ACTION_PRE_EXECUTION_ERROR
 
    There was an error while running the pre-execution hook for the action.
@@ -58,17 +58,17 @@ following statuses is reported under the
 .. data:: ACTION_SUCCESS
 
    The operation completed successfully. Individual failures with recipes may
    be reported separately.
 
 Recipe Status
 ^^^^^^^^^^^^^
 For each recipe that is fetched and executed, one of the following statuses is
-reported under the key ``shield-recipe-client/recipe/<recipe id>``:
+reported under the key ``normandy/recipe/<recipe id>``:
 
 .. data:: RECIPE_ACTION_DISABLED
 
    The action for this recipe failed in some way and was disabled, so the recipe
    could not be executed.
 
 .. data:: RECIPE_EXECUTION_ERROR
 
@@ -80,17 +80,17 @@ reported under the key ``shield-recipe-c
 
 .. data:: RECIPE_SUCCESS
 
    The recipe was executed successfully.
 
 
 Enrollment
 -----------
-Shield records enrollment and unenrollment of users into studies, and
+Normandy records enrollment and unenrollment of users into studies, and
 records that data using `Telemetry Events`_. All data is stored in the
 ``normandy`` category.
 
 .. _Telemetry Events: https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html
 
 Preference Studies
 ^^^^^^^^^^^^^^^^^^
 Enrollment
@@ -129,17 +129,17 @@ Unenrollment
            applicable to this client This can be because the recipe
            was disabled, or the user no longer matches the recipe's
            filter.
          * ``"user-preference-changed"``: The study preference was
            changed on the user branch. This could mean the user
            changed the preference, or that some other mechanism set a
            non-default value for the preference.
          * ``"user-preference-changed-sideload"``: The study
-           preference was changed on the user branch while Shield was
+           preference was changed on the user branch while Normandy was
            inactive. This could mean that the value was manually
            changed in a profile while Firefox was not running.
          * ``"unknown"``: A reason was not specificied. This should be
            considered a bug.
 
 Add-on Studies
 ^^^^^^^^^^^^^^
 Enrollment
@@ -191,13 +191,13 @@ Unenrollment
          * ``"recipe-not-seen"``: The recipe was no longer applicable
            to this client. This can be because the recipe was
            disabled, or the user no longer matches the recipe's
            filter.
          * ``"uninstalled"``: The study's add-on as uninstalled by some
            mechanism. For example, this could be a user action or the
            add-on self-uninstalling.
          * ``"uninstalled-sideload"``: The study's add-on was
-           uninstalled while Shield was inactive. This could be that
+           uninstalled while Normandy was inactive. This could be that
            the add-on is no longer compatible, or was manually removed
            from a profile.
          * ``"unknown"``: A reason was not specified. This should be
            considered a bug.
--- a/toolkit/components/normandy/lib/ClientEnvironment.jsm
+++ b/toolkit/components/normandy/lib/ClientEnvironment.jsm
@@ -72,21 +72,21 @@ var ClientEnvironment = {
    * Also note that, because filter expressions implicitly resolve promises, you
    * can add getter functions that return promises for async data.
    * @return {Object}
    */
   getEnvironment() {
     const environment = {};
 
     XPCOMUtils.defineLazyGetter(environment, "userId", () => {
-      let id = Preferences.get("extensions.shield-recipe-client.user_id", "");
+      let id = Preferences.get("app.normandy.user_id", "");
       if (!id) {
         // generateUUID adds leading and trailing "{" and "}". strip them off.
         id = generateUUID().toString().slice(1, -1);
-        Preferences.set("extensions.shield-recipe-client.user_id", id);
+        Preferences.set("app.normandy.user_id", id);
       }
       return id;
     });
 
     XPCOMUtils.defineLazyGetter(environment, "country", () => {
       return ClientEnvironment.getClientClassification()
         .then(classification => classification.country);
     });
@@ -199,14 +199,14 @@ var ClientEnvironment = {
     });
 
     XPCOMUtils.defineLazyGetter(environment, "addons", async () => {
       const addons = await Addons.getAll();
       return Utils.keyBy(addons, "id");
     });
 
     XPCOMUtils.defineLazyGetter(environment, "isFirstRun", () => {
-      return Preferences.get("extensions.shield-recipe-client.first_run");
+      return Preferences.get("app.normandy.first_run");
     });
 
     return environment;
   },
 };
--- a/toolkit/components/normandy/lib/LogManager.jsm
+++ b/toolkit/components/normandy/lib/LogManager.jsm
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 
 var EXPORTED_SYMBOLS = ["LogManager"];
 
-const ROOT_LOGGER_NAME = "extensions.shield-recipe-client";
+const ROOT_LOGGER_NAME = "app.normandy";
 let rootLogger = null;
 
 var LogManager = {
   /**
    * Configure the root logger for the Recipe Client. Must be called at
    * least once before using any loggers created via getLogger.
    * @param {Number} loggingLevel
    *        Logging level to use as defined in Log.jsm
--- a/toolkit/components/normandy/lib/NormandyApi.jsm
+++ b/toolkit/components/normandy/lib/NormandyApi.jsm
@@ -11,17 +11,17 @@ ChromeUtils.import("resource://normandy/
 ChromeUtils.defineModuleGetter(
   this, "CanonicalJSON", "resource://gre/modules/CanonicalJSON.jsm");
 
 Cu.importGlobalProperties(["fetch", "URL"]); /* globals fetch, URL */
 
 var EXPORTED_SYMBOLS = ["NormandyApi"];
 
 const log = LogManager.getLogger("normandy-api");
-const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
+const prefs = Services.prefs.getBranch("app.normandy.");
 
 let indexPromise = null;
 
 var NormandyApi = {
   InvalidSignatureError: class InvalidSignatureError extends Error {},
 
   clearIndexCache() {
     indexPromise = null;
--- a/toolkit/components/normandy/lib/PreferenceExperiments.jsm
+++ b/toolkit/components/normandy/lib/PreferenceExperiments.jsm
@@ -60,17 +60,17 @@ ChromeUtils.defineModuleGetter(this, "JS
 ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 ChromeUtils.defineModuleGetter(this, "LogManager", "resource://normandy/lib/LogManager.jsm");
 ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment", "resource://gre/modules/TelemetryEnvironment.jsm");
 ChromeUtils.defineModuleGetter(this, "TelemetryEvents", "resource://normandy/lib/TelemetryEvents.jsm");
 
 var EXPORTED_SYMBOLS = ["PreferenceExperiments"];
 
 const EXPERIMENT_FILE = "shield-preference-experiments.json";
-const STARTUP_EXPERIMENT_PREFS_BRANCH = "extensions.shield-recipe-client.startupExperimentPrefs.";
+const STARTUP_EXPERIMENT_PREFS_BRANCH = "app.normandy.startupExperimentPrefs.";
 
 const MAX_EXPERIMENT_TYPE_LENGTH = 20; // enforced by TelemetryEnvironment
 const EXPERIMENT_TYPE_PREFIX = "normandy-";
 const MAX_EXPERIMENT_SUBTYPE_LENGTH = MAX_EXPERIMENT_TYPE_LENGTH - EXPERIMENT_TYPE_PREFIX.length;
 
 const PREFERENCE_TYPE_MAP = {
   boolean: Services.prefs.PREF_BOOL,
   string: Services.prefs.PREF_STRING,
--- a/toolkit/components/normandy/lib/RecipeRunner.jsm
+++ b/toolkit/components/normandy/lib/RecipeRunner.jsm
@@ -38,40 +38,40 @@ Cu.importGlobalProperties(["fetch"]);
 var EXPORTED_SYMBOLS = ["RecipeRunner"];
 
 const log = LogManager.getLogger("recipe-runner");
 const TIMER_NAME = "recipe-client-addon-run";
 const PREF_CHANGED_TOPIC = "nsPref:changed";
 
 const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
 
-const SHIELD_PREF_PREFIX = "extensions.shield-recipe-client";
-const RUN_INTERVAL_PREF = `${SHIELD_PREF_PREFIX}.run_interval_seconds`;
-const FIRST_RUN_PREF = `${SHIELD_PREF_PREFIX}.first_run`;
-const SHIELD_ENABLED_PREF = `${SHIELD_PREF_PREFIX}.enabled`;
-const DEV_MODE_PREF = `${SHIELD_PREF_PREFIX}.dev_mode`;
-const API_URL_PREF = `${SHIELD_PREF_PREFIX}.api_url`;
-const LAZY_CLASSIFY_PREF = `${SHIELD_PREF_PREFIX}.experiments.lazy_classify`;
+const PREF_PREFIX = "app.normandy";
+const RUN_INTERVAL_PREF = `${PREF_PREFIX}.run_interval_seconds`;
+const FIRST_RUN_PREF = `${PREF_PREFIX}.first_run`;
+const SHIELD_ENABLED_PREF = `${PREF_PREFIX}.enabled`;
+const DEV_MODE_PREF = `${PREF_PREFIX}.dev_mode`;
+const API_URL_PREF = `${PREF_PREFIX}.api_url`;
+const LAZY_CLASSIFY_PREF = `${PREF_PREFIX}.experiments.lazy_classify`;
 
 const PREFS_TO_WATCH = [
   RUN_INTERVAL_PREF,
   TELEMETRY_ENABLED_PREF,
   SHIELD_ENABLED_PREF,
   API_URL_PREF,
 ];
 
 var RecipeRunner = {
   async init() {
     this.enabled = null;
     this.checkPrefs(); // sets this.enabled
     this.watchPrefs();
 
     // Run if enabled immediately on first run, or if dev mode is enabled.
-    const firstRun = Services.prefs.getBoolPref(FIRST_RUN_PREF);
-    const devMode = Services.prefs.getBoolPref(DEV_MODE_PREF);
+    const firstRun = Services.prefs.getBoolPref(FIRST_RUN_PREF, true);
+    const devMode = Services.prefs.getBoolPref(DEV_MODE_PREF, false);
 
     if (this.enabled && (devMode || firstRun)) {
       await this.run();
     }
     if (firstRun) {
       Services.prefs.setBoolPref(FIRST_RUN_PREF, false);
     }
   },
--- a/toolkit/components/normandy/lib/Uptake.jsm
+++ b/toolkit/components/normandy/lib/Uptake.jsm
@@ -6,17 +6,17 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(
   this, "UptakeTelemetry", "resource://services-common/uptake-telemetry.js");
 
 var EXPORTED_SYMBOLS = ["Uptake"];
 
-const SOURCE_PREFIX = "shield-recipe-client";
+const SOURCE_PREFIX = "normandy";
 
 var Uptake = {
   // Action uptake
   ACTION_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR,
   ACTION_PRE_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_1_ERROR,
   ACTION_POST_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_2_ERROR,
   ACTION_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR,
   ACTION_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
--- a/toolkit/components/normandy/test/browser/browser.ini
+++ b/toolkit/components/normandy/test/browser/browser.ini
@@ -16,10 +16,9 @@ skip-if = !healthreport || !telemetry
 [browser_FilterExpressions.js]
 [browser_Heartbeat.js]
 [browser_LogManager.js]
 [browser_Normandy.js]
 [browser_NormandyDriver.js]
 [browser_PreferenceExperiments.js]
 [browser_RecipeRunner.js]
 [browser_ShieldPreferences.js]
-[browser_ShieldRecipeClient.js]
 [browser_Storage.js]
--- a/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js
+++ b/toolkit/components/normandy/test/browser/browser_ClientEnvironment.js
@@ -28,17 +28,17 @@ add_task(async function testTelemetry() 
 
 add_task(async function testUserId() {
   let environment = ClientEnvironment.getEnvironment();
 
   // Test that userId is available
   ok(UUID_REGEX.test(environment.userId), "userId available");
 
   // test that it pulls from the right preference
-  await SpecialPowers.pushPrefEnv({set: [["extensions.shield-recipe-client.user_id", "fake id"]]});
+  await SpecialPowers.pushPrefEnv({set: [["app.normandy.user_id", "fake id"]]});
   environment = ClientEnvironment.getEnvironment();
   is(environment.userId, "fake id", "userId is pulled from preferences");
 });
 
 add_task(async function testDistribution() {
   let environment = ClientEnvironment.getEnvironment();
 
   // distribution id defaults to "default"
@@ -133,12 +133,12 @@ add_task(withDriver(Assert, async functi
     isActive: true,
     type: "extension",
   }, "addons should be available in context");
 
   await driver.addons.uninstall(addonId);
 }));
 
 add_task(async function isFirstRun() {
-  await SpecialPowers.pushPrefEnv({set: [["extensions.shield-recipe-client.first_run", true]]});
+  await SpecialPowers.pushPrefEnv({set: [["app.normandy.first_run", true]]});
   const environment = ClientEnvironment.getEnvironment();
   ok(environment.isFirstRun, "isFirstRun is read from preferences");
 });
--- a/toolkit/components/normandy/test/browser/browser_Normandy.js
+++ b/toolkit/components/normandy/test/browser/browser_Normandy.js
@@ -1,84 +1,39 @@
 "use strict";
 
 ChromeUtils.import("resource://normandy/Normandy.jsm", this);
-ChromeUtils.import("resource://normandy/lib/ShieldRecipeClient.jsm", this);
+ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
 ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
-
-const initPref1 = "test.initShieldPrefs1";
-const initPref2 = "test.initShieldPrefs2";
-const initPref3 = "test.initShieldPrefs3";
+ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
+ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
+ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
 
 const experimentPref1 = "test.initExperimentPrefs1";
 const experimentPref2 = "test.initExperimentPrefs2";
 const experimentPref3 = "test.initExperimentPrefs3";
 const experimentPref4 = "test.initExperimentPrefs4";
 
-decorate_task(
-  async function testInitShieldPrefs() {
-    const defaultBranch = Services.prefs.getDefaultBranch("");
-
-    const prefDefaults = {
-      [initPref1]: true,
-      [initPref2]: 2,
-      [initPref3]: "string",
-    };
-
-    for (const pref of Object.keys(prefDefaults)) {
-      is(
-        defaultBranch.getPrefType(pref),
-        defaultBranch.PREF_INVALID,
-        `Pref ${pref} don't exist before being initialized.`,
-      );
-    }
-
-    Normandy.initShieldPrefs(prefDefaults);
-
-    ok(
-      defaultBranch.getBoolPref(initPref1),
-      `Pref ${initPref1} has a default value after being initialized.`,
-    );
-    is(
-      defaultBranch.getIntPref(initPref2),
-      2,
-      `Pref ${initPref2} has a default value after being initialized.`,
-    );
-    is(
-      defaultBranch.getCharPref(initPref3),
-      "string",
-      `Pref ${initPref3} has a default value after being initialized.`,
-    );
-
-    for (const pref of Object.keys(prefDefaults)) {
-      ok(
-        !defaultBranch.prefHasUserValue(pref),
-        `Pref ${pref} doesn't have a user value after being initialized.`,
-      );
-    }
-
-    defaultBranch.deleteBranch("test.");
-  },
-);
-
-decorate_task(
-  async function testInitShieldPrefsError() {
-    Assert.throws(
-      () => Normandy.initShieldPrefs({"test.prefTypeError": new Date()}),
-      "initShieldPrefs throws when given an invalid type for the pref value.",
-    );
-  },
-);
+function withStubInits(testFunction) {
+  return decorate(
+    withStub(AboutPages, "init"),
+    withStub(AddonStudies, "init"),
+    withStub(PreferenceExperiments, "init"),
+    withStub(RecipeRunner, "init"),
+    withStub(TelemetryEvents, "init"),
+    testFunction
+  );
+}
 
 decorate_task(
   withPrefEnv({
     set: [
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref1}`, true],
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref2}`, 2],
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref3}`, "string"],
+      [`app.normandy.startupExperimentPrefs.${experimentPref1}`, true],
+      [`app.normandy.startupExperimentPrefs.${experimentPref2}`, 2],
+      [`app.normandy.startupExperimentPrefs.${experimentPref3}`, "string"],
     ],
     clear: [[experimentPref1], [experimentPref2], [experimentPref3]],
   }),
   async function testInitExperimentPrefs() {
     const defaultBranch = Services.prefs.getDefaultBranch("");
     for (const pref of [experimentPref1, experimentPref2, experimentPref3]) {
       is(
         defaultBranch.getPrefType(pref),
@@ -111,17 +66,17 @@ decorate_task(
       );
     }
   },
 );
 
 decorate_task(
   withPrefEnv({
     set: [
-      ["extensions.shield-recipe-client.startupExperimentPrefs.test.existingPref", "experiment"],
+      ["app.normandy.startupExperimentPrefs.test.existingPref", "experiment"],
     ],
   }),
   async function testInitExperimentPrefsExisting() {
     const defaultBranch = Services.prefs.getDefaultBranch("");
     defaultBranch.setCharPref("test.existingPref", "default");
     Normandy.initExperimentPrefs();
     is(
       defaultBranch.getCharPref("test.existingPref"),
@@ -129,17 +84,17 @@ decorate_task(
       "initExperimentPrefs overwrites the default values of existing preferences.",
     );
   },
 );
 
 decorate_task(
   withPrefEnv({
     set: [
-      ["extensions.shield-recipe-client.startupExperimentPrefs.test.mismatchPref", "experiment"],
+      ["app.normandy.startupExperimentPrefs.test.mismatchPref", "experiment"],
     ],
   }),
   async function testInitExperimentPrefsMismatch() {
     const defaultBranch = Services.prefs.getDefaultBranch("");
     defaultBranch.setIntPref("test.mismatchPref", 2);
     Normandy.initExperimentPrefs();
     is(
       defaultBranch.getPrefType("test.mismatchPref"),
@@ -166,27 +121,27 @@ decorate_task(
   },
 );
 
 // During startup, preferences that are changed for experiments should
 // be record by calling PreferenceExperiments.recordOriginalValues.
 decorate_task(
   withPrefEnv({
     set: [
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref1}`, true],
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref2}`, 2],
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref3}`, "string"],
-      [`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref4}`, "another string"],
+      [`app.normandy.startupExperimentPrefs.${experimentPref1}`, true],
+      [`app.normandy.startupExperimentPrefs.${experimentPref2}`, 2],
+      [`app.normandy.startupExperimentPrefs.${experimentPref3}`, "string"],
+      [`app.normandy.startupExperimentPrefs.${experimentPref4}`, "another string"],
     ],
     clear: [
       [experimentPref1],
       [experimentPref2],
       [experimentPref3],
       [experimentPref4],
-      ["extensions.shield-recipe-client.startupExperimentPrefs.existingPref"],
+      ["app.normandy.startupExperimentPrefs.existingPref"],
     ],
   }),
   withStub(PreferenceExperiments, "recordOriginalValues"),
   async function testInitExperimentPrefs(recordOriginalValuesStub) {
     const defaultBranch = Services.prefs.getDefaultBranch("");
 
     defaultBranch.setBoolPref(experimentPref1, false);
     defaultBranch.setIntPref(experimentPref2, 1);
@@ -208,18 +163,117 @@ decorate_task(
     );
   },
 );
 
 // Test that startup prefs are handled correctly when there is a value on the user branch but not the default branch.
 decorate_task(
   withPrefEnv({
     set: [
-      ["extensions.shield-recipe-client.startupExperimentPrefs.testing.does-not-exist", "foo"],
+      ["app.normandy.startupExperimentPrefs.testing.does-not-exist", "foo"],
       ["testing.does-not-exist", "foo"],
     ],
   }),
   withStub(PreferenceExperiments, "recordOriginalValues"),
   async function testInitExperimentPrefsNoDefaultValue() {
     Normandy.initExperimentPrefs();
     ok(true, "initExperimentPrefs should not throw for non-existant prefs");
   },
 );
+
+decorate_task(
+  withStubInits,
+  async function testStartup() {
+    const initObserved = TestUtils.topicObserved("shield-init-complete");
+    await Normandy.finishInit();
+    ok(AboutPages.init.called, "startup calls AboutPages.init");
+    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
+    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
+    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
+    await initObserved;
+  }
+);
+
+decorate_task(
+  withStubInits,
+  async function testStartupPrefInitFail() {
+    PreferenceExperiments.init.returns(Promise.reject(new Error("oh no")));
+
+    await Normandy.finishInit();
+    ok(AboutPages.init.called, "startup calls AboutPages.init");
+    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
+    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
+    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
+    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
+  }
+);
+
+decorate_task(
+  withStubInits,
+  async function testStartupAboutPagesInitFail() {
+    AboutPages.init.returns(Promise.reject(new Error("oh no")));
+
+    await Normandy.finishInit();
+    ok(AboutPages.init.called, "startup calls AboutPages.init");
+    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
+    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
+    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
+    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
+  }
+);
+
+decorate_task(
+  withStubInits,
+  async function testStartupAddonStudiesInitFail() {
+    AddonStudies.init.returns(Promise.reject(new Error("oh no")));
+
+    await Normandy.finishInit();
+    ok(AboutPages.init.called, "startup calls AboutPages.init");
+    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
+    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
+    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
+    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
+  }
+);
+
+decorate_task(
+  withStubInits,
+  async function testStartupTelemetryEventsInitFail() {
+    TelemetryEvents.init.throws();
+
+    await Normandy.finishInit();
+    ok(AboutPages.init.called, "startup calls AboutPages.init");
+    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
+    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
+    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
+    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
+  }
+);
+
+decorate_task(
+  withMockPreferences,
+  async function testPrefMigration(mockPreferences) {
+    const legacyPref = "extensions.shield-recipe-client.test";
+    const migratedPref = "app.normandy.test";
+    mockPreferences.set(legacyPref, 1);
+
+    ok(
+      Services.prefs.prefHasUserValue(legacyPref),
+      "Legacy pref should have a user value before running migration",
+    );
+    ok(
+      !Services.prefs.prefHasUserValue(migratedPref),
+      "Migrated pref should not have a user value before running migration",
+    );
+
+    Normandy.migrateShieldPrefs();
+
+    ok(
+      !Services.prefs.prefHasUserValue(legacyPref),
+      "Legacy pref should not have a user value after running migration",
+    );
+    ok(
+      Services.prefs.prefHasUserValue(migratedPref),
+      "Migrated pref should have a user value after running migration",
+    );
+    is(Services.prefs.getIntPref(migratedPref), 1, "Value should have been migrated");
+  },
+);
--- a/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
+++ b/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
@@ -4,17 +4,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
 ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
 ChromeUtils.import("resource://normandy/lib/CleanupManager.jsm", this);
 ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
 
 // Save ourselves some typing
 const {withMockExperiments} = PreferenceExperiments;
 const DefaultPreferences = new Preferences({defaultBranch: true});
-const startupPrefs = "extensions.shield-recipe-client.startupExperimentPrefs";
+const startupPrefs = "app.normandy.startupExperimentPrefs";
 
 function experimentFactory(attrs) {
   return Object.assign({
     name: "fakename",
     branch: "fakebranch",
     expired: false,
     lastSeen: new Date().toJSON(),
     preferenceName: "fake.preference",
--- a/toolkit/components/normandy/test/browser/browser_RecipeRunner.js
+++ b/toolkit/components/normandy/test/browser/browser_RecipeRunner.js
@@ -68,31 +68,31 @@ add_task(async function checkFilter() {
 
 decorate_task(
   withMockNormandyApi,
   withStub(ClientEnvironment, "getClientClassification"),
   async function testClientClassificationCache(api, getStub) {
     getStub.returns(Promise.resolve(false));
 
     await SpecialPowers.pushPrefEnv({set: [
-      ["extensions.shield-recipe-client.api_url",
+      ["app.normandy.api_url",
        "https://example.com/selfsupport-dummy"],
     ]});
 
     // When the experiment pref is false, eagerly call getClientClassification.
     await SpecialPowers.pushPrefEnv({set: [
-      ["extensions.shield-recipe-client.experiments.lazy_classify", false],
+      ["app.normandy.experiments.lazy_classify", false],
     ]});
     ok(!getStub.called, "getClientClassification hasn't been called");
     await RecipeRunner.run();
     ok(getStub.called, "getClientClassification was called eagerly");
 
     // When the experiment pref is true, do not eagerly call getClientClassification.
     await SpecialPowers.pushPrefEnv({set: [
-      ["extensions.shield-recipe-client.experiments.lazy_classify", true],
+      ["app.normandy.experiments.lazy_classify", true],
     ]});
     getStub.reset();
     ok(!getStub.called, "getClientClassification hasn't been called");
     await RecipeRunner.run();
     ok(!getStub.called, "getClientClassification was not called eagerly");
   }
 );
 
@@ -337,112 +337,112 @@ decorate_task(
   }
 );
 
 // test init() in dev mode
 decorate_task(
   withPrefEnv({
     set: [
       ["datareporting.healthreport.uploadEnabled", true],  // telemetry enabled
-      ["extensions.shield-recipe-client.dev_mode", true],
-      ["extensions.shield-recipe-client.first_run", false],
+      ["app.normandy.dev_mode", true],
+      ["app.normandy.first_run", false],
     ],
   }),
   withStub(RecipeRunner, "run"),
   withStub(RecipeRunner, "registerTimer"),
   async function testInitDevMode(runStub, registerTimerStub, updateRunIntervalStub) {
     await RecipeRunner.init();
     ok(runStub.called, "RecipeRunner.run is called immediately when in dev mode");
     ok(registerTimerStub.called, "RecipeRunner.init registers a timer");
   }
 );
 
 // Test init() during normal operation
 decorate_task(
   withPrefEnv({
     set: [
       ["datareporting.healthreport.uploadEnabled", true],  // telemetry enabled
-      ["extensions.shield-recipe-client.dev_mode", false],
-      ["extensions.shield-recipe-client.first_run", false],
+      ["app.normandy.dev_mode", false],
+      ["app.normandy.first_run", false],
     ],
   }),
   withStub(RecipeRunner, "run"),
   withStub(RecipeRunner, "registerTimer"),
   async function testInit(runStub, registerTimerStub) {
     await RecipeRunner.init();
     ok(!runStub.called, "RecipeRunner.run is called immediately when not in dev mode or first run");
     ok(registerTimerStub.called, "RecipeRunner.init registers a timer");
   }
 );
 
 // Test init() first run
 decorate_task(
   withPrefEnv({
     set: [
       ["datareporting.healthreport.uploadEnabled", true],  // telemetry enabled
-      ["extensions.shield-recipe-client.dev_mode", false],
-      ["extensions.shield-recipe-client.first_run", true],
-      ["extensions.shield-recipe-client.api_url", "https://example.com"],
+      ["app.normandy.dev_mode", false],
+      ["app.normandy.first_run", true],
+      ["app.normandy.api_url", "https://example.com"],
     ],
   }),
   withStub(RecipeRunner, "run"),
   withStub(RecipeRunner, "registerTimer"),
   withStub(RecipeRunner, "watchPrefs"),
   async function testInitFirstRun(runStub, registerTimerStub, watchPrefsStub) {
     await RecipeRunner.init();
     ok(runStub.called, "RecipeRunner.run is called immediately on first run");
     ok(
-      !Services.prefs.getBoolPref("extensions.shield-recipe-client.first_run"),
+      !Services.prefs.getBoolPref("app.normandy.first_run"),
       "On first run, the first run pref is set to false"
     );
     ok(registerTimerStub.called, "RecipeRunner.registerTimer registers a timer");
 
     // RecipeRunner.init() sets this to false, but SpecialPowers
     // relies on the preferences it manages to actually change when it
     // tries to change them. Settings this back to true here allows
     // that to happen. Not doing this causes popPrefEnv to hang forever.
-    Services.prefs.setBoolPref("extensions.shield-recipe-client.first_run", true);
+    Services.prefs.setBoolPref("app.normandy.first_run", true);
   }
 );
 
 // Test that prefs are watched correctly
 decorate_task(
   withPrefEnv({
     set: [
       ["datareporting.healthreport.uploadEnabled", true],  // telemetry enabled
-      ["extensions.shield-recipe-client.dev_mode", false],
-      ["extensions.shield-recipe-client.first_run", false],
-      ["extensions.shield-recipe-client.enabled", true],
-      ["extensions.shield-recipe-client.api_url", "https://example.com"], // starts with "https://"
+      ["app.normandy.dev_mode", false],
+      ["app.normandy.first_run", false],
+      ["app.normandy.enabled", true],
+      ["app.normandy.api_url", "https://example.com"], // starts with "https://"
     ],
   }),
   withStub(RecipeRunner, "run"),
   withStub(RecipeRunner, "enable"),
   withStub(RecipeRunner, "disable"),
   withStub(CleanupManager, "addCleanupHandler"),
   withStub(AddonStudies, "stop"),
 
   async function testPrefWatching(runStub, enableStub, disableStub, addCleanupHandlerStub, stopStub) {
     await RecipeRunner.init();
     is(enableStub.callCount, 1, "Enable should be called initially");
     is(disableStub.callCount, 0, "Disable should not be called initially");
 
-    await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.enabled", false]] });
+    await SpecialPowers.pushPrefEnv({ set: [["app.normandy.enabled", false]] });
     is(enableStub.callCount, 1, "Enable should not be called again");
     is(disableStub.callCount, 1, "RecipeRunner should disable when Shield is disabled");
 
-    await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.enabled", true]] });
+    await SpecialPowers.pushPrefEnv({ set: [["app.normandy.enabled", true]] });
     is(enableStub.callCount, 2, "RecipeRunner should re-enable when Shield is enabled");
     is(disableStub.callCount, 1, "Disable should not be called again");
 
-    await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.api_url", "http://example.com"]] }); // does not start with https://
+    await SpecialPowers.pushPrefEnv({ set: [["app.normandy.api_url", "http://example.com"]] }); // does not start with https://
     is(enableStub.callCount, 2, "Enable should not be called again");
     is(disableStub.callCount, 2, "RecipeRunner should disable when an invalid api url is given");
 
-    await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.api_url", "https://example.com"]] }); // ends with https://
+    await SpecialPowers.pushPrefEnv({ set: [["app.normandy.api_url", "https://example.com"]] }); // ends with https://
     is(enableStub.callCount, 3, "RecipeRunner should re-enable when a valid api url is given");
     is(disableStub.callCount, 2, "Disable should not be called again");
 
     await SpecialPowers.pushPrefEnv({ set: [["datareporting.healthreport.uploadEnabled", false]] });
     is(enableStub.callCount, 3, "Enable should not be called again");
     is(disableStub.callCount, 3, "RecipeRunner should disable when telemetry is disabled");
 
     await SpecialPowers.pushPrefEnv({ set: [["datareporting.healthreport.uploadEnabled", true]] });
deleted file mode 100644
--- a/toolkit/components/normandy/test/browser/browser_ShieldRecipeClient.js
+++ /dev/null
@@ -1,88 +0,0 @@
-"use strict";
-
-ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
-ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
-ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
-ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
-ChromeUtils.import("resource://normandy/lib/ShieldRecipeClient.jsm", this);
-ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
-
-function withStubInits(testFunction) {
-  return decorate(
-    withStub(AboutPages, "init"),
-    withStub(AddonStudies, "init"),
-    withStub(PreferenceExperiments, "init"),
-    withStub(RecipeRunner, "init"),
-    withStub(TelemetryEvents, "init"),
-    testFunction
-  );
-}
-
-decorate_task(
-  withStubInits,
-  async function testStartup() {
-    const initObserved = TestUtils.topicObserved("shield-init-complete");
-    await ShieldRecipeClient.startup();
-    ok(AboutPages.init.called, "startup calls AboutPages.init");
-    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
-    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
-    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
-    await initObserved;
-  }
-);
-
-decorate_task(
-  withStubInits,
-  async function testStartupPrefInitFail() {
-    PreferenceExperiments.init.returns(Promise.reject(new Error("oh no")));
-
-    await ShieldRecipeClient.startup();
-    ok(AboutPages.init.called, "startup calls AboutPages.init");
-    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
-    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
-    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
-    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
-  }
-);
-
-decorate_task(
-  withStubInits,
-  async function testStartupAboutPagesInitFail() {
-    AboutPages.init.returns(Promise.reject(new Error("oh no")));
-
-    await ShieldRecipeClient.startup();
-    ok(AboutPages.init.called, "startup calls AboutPages.init");
-    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
-    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
-    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
-    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
-  }
-);
-
-decorate_task(
-  withStubInits,
-  async function testStartupAddonStudiesInitFail() {
-    AddonStudies.init.returns(Promise.reject(new Error("oh no")));
-
-    await ShieldRecipeClient.startup();
-    ok(AboutPages.init.called, "startup calls AboutPages.init");
-    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
-    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
-    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
-    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
-  }
-);
-
-decorate_task(
-  withStubInits,
-  async function testStartupTelemetryEventsInitFail() {
-    TelemetryEvents.init.throws();
-
-    await ShieldRecipeClient.startup();
-    ok(AboutPages.init.called, "startup calls AboutPages.init");
-    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
-    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
-    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
-    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
-  }
-);
--- a/toolkit/components/normandy/test/browser/browser_about_studies.js
+++ b/toolkit/components/normandy/test/browser/browser_about_studies.js
@@ -17,17 +17,17 @@ decorate_task(
   async function testAboutStudiesWorks(browser) {
     // eslint-disable-next-line mozilla/no-cpows-in-tests
     ok(browser.contentDocumentAsCPOW.getElementById("app"), "App element was found");
   }
 );
 
 decorate_task(
   withPrefEnv({
-    set: [["extensions.shield-recipe-client.shieldLearnMoreUrl", "http://test/%OS%/"]],
+    set: [["app.normandy.shieldLearnMoreUrl", "http://test/%OS%/"]],
   }),
   withAboutStudies,
   async function testLearnMore(browser) {
     ContentTask.spawn(browser, null, () => {
       content.document.getElementById("shield-studies-learn-more").click();
     });
     await BrowserTestUtils.waitForLocationChange(gBrowser);
 
--- a/toolkit/components/normandy/test/unit/test_NormandyApi.js
+++ b/toolkit/components/normandy/test/unit/test_NormandyApi.js
@@ -21,17 +21,17 @@ class MockResponse {
   async json() {
     return JSON.parse(this.content);
   }
 }
 
 function withServer(server, task) {
   return withMockPreferences(async function inner(preferences) {
     const serverUrl = `http://localhost:${server.identity.primaryPort}`;
-    preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`);
+    preferences.set("app.normandy.api_url", `${serverUrl}/api/v1`);
     preferences.set(
       "security.content.signature.root_hash",
       // Hash of the key that signs the normandy dev certificates
       "4C:35:B1:C3:E3:12:D9:55:E7:78:ED:D0:A7:E7:8A:38:83:04:EF:01:BF:FA:03:29:B2:46:9F:3C:C5:EC:36:04"
     );
     NormandyApi.clearIndexCache();
 
     try {
@@ -106,27 +106,27 @@ add_task(withMockApiServer(async functio
 
 add_task(withMockApiServer(async function test_getApiUrlSlashes(serverUrl, preferences) {
   const fakeResponse = new MockResponse(JSON.stringify({"test-endpoint": `${serverUrl}/test/`}));
   const mockGet = sinon.stub(NormandyApi, "get", async () => fakeResponse);
 
   // without slash
   {
     NormandyApi.clearIndexCache();
-    preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`);
+    preferences.set("app.normandy.api_url", `${serverUrl}/api/v1`);
     const endpoint = await NormandyApi.getApiUrl("test-endpoint");
     equal(endpoint, `${serverUrl}/test/`);
     ok(mockGet.calledWithExactly(`${serverUrl}/api/v1/`), "trailing slash was added");
     mockGet.reset();
   }
 
   // with slash
   {
     NormandyApi.clearIndexCache();
-    preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1/`);
+    preferences.set("app.normandy.api_url", `${serverUrl}/api/v1/`);
     const endpoint = await NormandyApi.getApiUrl("test-endpoint");
     equal(endpoint, `${serverUrl}/test/`);
     ok(mockGet.calledWithExactly(`${serverUrl}/api/v1/`), "existing trailing slash was preserved");
     mockGet.reset();
   }
 
   NormandyApi.clearIndexCache();
   mockGet.restore();