Bug 1432986 - Sync shield-recipe-client v83 from Github (commit 43f0ce2) r?Gijs draft
authorMike Cooper <mcooper@mozilla.com>
Wed, 24 Jan 2018 15:40:22 -0800
changeset 748462 2b4d865a1b08f74f44abcd9692656f5d0766a5f2
parent 724404 32b850fa28ae1c29039cb7ddcdfd71b324762c05
push id97172
push userbmo:mcooper@mozilla.com
push dateMon, 29 Jan 2018 21:21:54 +0000
reviewersGijs
bugs1432986
milestone60.0a1
Bug 1432986 - Sync shield-recipe-client v83 from Github (commit 43f0ce2) r?Gijs MozReview-Commit-ID: 9TGdnKZ5zaf
browser/extensions/shield-recipe-client/bootstrap.js
browser/extensions/shield-recipe-client/content/AboutPages.jsm
browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js
browser/extensions/shield-recipe-client/install.rdf.in
browser/extensions/shield-recipe-client/lib/AddonStudies.jsm
browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm
browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm
browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm
browser/extensions/shield-recipe-client/lib/TelemetryEvents.jsm
browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js
browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js
browser/extensions/shield-recipe-client/test/browser/head.js
browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY
--- a/browser/extensions/shield-recipe-client/bootstrap.js
+++ b/browser/extensions/shield-recipe-client/bootstrap.js
@@ -197,16 +197,17 @@ this.Bootstrap = {
       "lib/NormandyDriver.jsm",
       "lib/PreferenceExperiments.jsm",
       "lib/RecipeRunner.jsm",
       "lib/Sampling.jsm",
       "lib/SandboxManager.jsm",
       "lib/ShieldPreferences.jsm",
       "lib/ShieldRecipeClient.jsm",
       "lib/Storage.jsm",
+      "lib/TelemetryEvents.jsm",
       "lib/Uptake.jsm",
       "lib/Utils.jsm",
     ].map(m => `resource://shield-recipe-client/${m}`);
     modules = modules.concat([
       "resource://shield-recipe-client-content/AboutPages.jsm",
       "resource://shield-recipe-client-vendor/mozjexl.js",
     ]);
 
--- a/browser/extensions/shield-recipe-client/content/AboutPages.jsm
+++ b/browser/extensions/shield-recipe-client/content/AboutPages.jsm
@@ -160,17 +160,17 @@ XPCOMUtils.defineLazyGetter(this.AboutPa
      *   See the nsIMessageListener documentation for details about this object.
      */
     receiveMessage(message) {
       switch (message.name) {
         case "Shield:GetStudyList":
           this.sendStudyList(message.target);
           break;
         case "Shield:RemoveStudy":
-          this.removeStudy(message.data);
+          this.removeStudy(message.data.recipeId, message.data.reason);
           break;
         case "Shield:OpenDataPreferences":
           this.openDataPreferences();
           break;
       }
     },
 
     /**
@@ -190,18 +190,18 @@ XPCOMUtils.defineLazyGetter(this.AboutPa
         Cu.reportError(err);
       }
     },
 
     /**
      * Disable an active study and remove its add-on.
      * @param {String} studyName
      */
-    async removeStudy(recipeId) {
-      await AddonStudies.stop(recipeId);
+    async removeStudy(recipeId, reason) {
+      await AddonStudies.stop(recipeId, reason);
 
       // Update any open tabs with the new study list now that it has changed.
       Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", {
         studies: await AddonStudies.getAll(),
       });
     },
 
     openDataPreferences() {
--- a/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js
+++ b/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js
@@ -104,17 +104,17 @@ class StudyList extends React.Component 
 
 class StudyListItem extends React.Component {
   constructor(props) {
     super(props);
     this.handleClickRemove = this.handleClickRemove.bind(this);
   }
 
   handleClickRemove() {
-    sendPageEvent("RemoveStudy", this.props.study.recipeId);
+    sendPageEvent("RemoveStudy", {recipeId: this.props.study.recipeId, reason: "individual-opt-out"});
   }
 
   render() {
     const study = this.props.study;
     return (
       r("li", {
         className: classnames("study", {disabled: !study.active}),
         "data-study-name": study.name,
--- a/browser/extensions/shield-recipe-client/install.rdf.in
+++ b/browser/extensions/shield-recipe-client/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>shield-recipe-client@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>80</em:version>
+    <em:version>83</em:version>
     <em:name>Shield Recipe Client</em:name>
     <em:description>Client to download and run recipes for SHIELD, Heartbeat, etc.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm
+++ b/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm
@@ -30,20 +30,19 @@ const {utils: Cu, interfaces: Ci} = Comp
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm");
-XPCOMUtils.defineLazyModuleGetter(
-  this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm"
-);
+XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents", "resource://shield-recipe-client/lib/TelemetryEvents.jsm");
 
 Cu.importGlobalProperties(["fetch"]); /* globals fetch */
 
 this.EXPORTED_SYMBOLS = ["AddonStudies"];
 
 const DB_NAME = "shield";
 const STORE_NAME = "addon-studies";
 const DB_OPTIONS = {
@@ -87,22 +86,33 @@ async function getDatabase() {
 function getStore(db) {
   return db.objectStore(STORE_NAME, "readwrite");
 }
 
 /**
  * Mark a study object as having ended. Modifies the study in-place.
  * @param {IDBDatabase} db
  * @param {Study} study
+ * @param {String} reason Why the study is ending.
  */
-async function markAsEnded(db, study) {
+async function markAsEnded(db, study, reason) {
+  if (reason === "unknown") {
+    log.warn(`Study ${study.name} ending for unknown reason.`);
+  }
+
   study.active = false;
   study.studyEndDate = new Date();
   await getStore(db).put(study);
+
   Services.obs.notifyObservers(study, STUDY_ENDED_TOPIC, `${study.recipeId}`);
+  TelemetryEvents.sendEvent("unenroll", "addon_study", study.name, {
+    addonId: study.addonId,
+    addonVersion: study.addonVersion,
+    reason,
+  });
 }
 
 this.AddonStudies = {
   /**
    * Test wrapper that temporarily replaces the stored studies with the given
    * ones. The original stored studies are restored upon completion.
    *
    * This is defined here instead of in test code since it needs to access the
@@ -140,17 +150,17 @@ this.AddonStudies = {
   async init() {
     // If an active study's add-on has been removed since we last ran, stop the
     // study.
     const activeStudies = (await this.getAll()).filter(study => study.active);
     const db = await getDatabase();
     for (const study of activeStudies) {
       const addon = await AddonManager.getAddonByID(study.addonId);
       if (!addon) {
-        await markAsEnded(db, study);
+        await markAsEnded(db, study, "uninstalled-sideload");
       }
     }
     await this.close();
 
     // Listen for add-on uninstalls so we can stop the corresponding studies.
     AddonManager.addAddonListener(this);
     CleanupManager.addCleanupHandler(() => {
       AddonManager.removeAddonListener(this);
@@ -163,17 +173,17 @@ this.AddonStudies = {
    */
   async onUninstalled(addon) {
     const activeStudies = (await this.getAll()).filter(study => study.active);
     const matchingStudy = activeStudies.find(study => study.addonId === addon.id);
     if (matchingStudy) {
       // Use a dedicated DB connection instead of the shared one so that we can
       // close it without fear of affecting other users of the shared connection.
       const db = await openDatabase();
-      await markAsEnded(db, matchingStudy);
+      await markAsEnded(db, matchingStudy, "uninstalled");
       await db.close();
     }
   },
 
   /**
    * Remove all stored studies.
    */
   async clear() {
@@ -253,22 +263,34 @@ this.AddonStudies = {
       description,
       addonId: install.addon.id,
       addonVersion: install.addon.version,
       addonUrl,
       active: true,
       studyStartDate: new Date(),
     };
 
+    TelemetryEvents.sendEvent("enroll", "addon_study", name, {
+      addonId: install.addon.id,
+      addonVersion: install.addon.version,
+    });
+
     try {
       await getStore(db).add(study);
       await Addons.applyInstall(install, false);
       return study;
     } catch (err) {
       await getStore(db).delete(recipeId);
+
+      TelemetryEvents.sendEvent("unenroll", "addon_study", name, {
+        reason: "install-failure",
+        addonId: install.addon.id,
+        addonVersion: install.addon.version,
+      });
+
       throw err;
     } finally {
       Services.obs.notifyObservers(addonFile, "flush-cache-entry");
       await OS.File.remove(addonFile.path);
     }
   },
 
   /**
@@ -295,31 +317,32 @@ this.AddonStudies = {
     }
 
     return new FileUtils.File(uniquePath);
   },
 
   /**
    * Stop an active study, uninstalling the associated add-on.
    * @param {Number} recipeId
+   * @param {String} reason Why the study is ending. Optional, defaults to "unknown".
    * @throws
    *   If no study is found with the given recipeId.
    *   If the study is already inactive.
    */
-  async stop(recipeId) {
+  async stop(recipeId, reason = "unknown") {
     const db = await getDatabase();
     const study = await getStore(db).get(recipeId);
     if (!study) {
-      throw new Error(`No study found for recipe ${recipeId}`);
+      throw new Error(`No study found for recipe ${recipeId}.`);
     }
     if (!study.active) {
       throw new Error(`Cannot stop study for recipe ${recipeId}; it is already inactive.`);
     }
 
-    await markAsEnded(db, study);
+    await markAsEnded(db, study, reason);
 
     try {
       await Addons.uninstall(study.addonId);
     } catch (err) {
       log.warn(`Could not uninstall addon ${study.addonId} for recipe ${study.recipeId}:`, err);
     }
   },
 };
--- a/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm
+++ b/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm
@@ -56,16 +56,17 @@ const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", "resource://gre/modules/JSONFile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment", "resource://gre/modules/TelemetryEnvironment.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents", "resource://shield-recipe-client/lib/TelemetryEvents.jsm");
 
 this.EXPORTED_SYMBOLS = ["PreferenceExperiments"];
 
 const EXPERIMENT_FILE = "shield-preference-experiments.json";
 const STARTUP_EXPERIMENT_PREFS_BRANCH = "extensions.shield-recipe-client.startupExperimentPrefs.";
 
 const MAX_EXPERIMENT_TYPE_LENGTH = 20; // enforced by TelemetryEnvironment
 const EXPERIMENT_TYPE_PREFIX = "normandy-";
@@ -181,17 +182,20 @@ this.PreferenceExperiments = {
   async init() {
     CleanupManager.addCleanupHandler(this.saveStartupPrefs.bind(this));
 
     for (const experiment of await this.getAllActive()) {
       // Check that the current value of the preference is still what we set it to
       if (getPref(UserPreferences, experiment.preferenceName, experiment.preferenceType) !== experiment.preferenceValue) {
         // if not, stop the experiment, and skip the remaining steps
         log.info(`Stopping experiment "${experiment.name}" because its value changed`);
-        await this.stop(experiment.name, false);
+        await this.stop(experiment.name, {
+          didResetValue: false,
+          reason: "user-preference-changed-sideload",
+        });
         continue;
       }
 
       // Notify Telemetry of experiments we're running, since they don't persist between restarts
       TelemetryEnvironment.setExperimentActive(
         experiment.name,
         experiment.branch,
         {type: EXPERIMENT_TYPE_PREFIX + experiment.experimentType}
@@ -346,16 +350,17 @@ this.PreferenceExperiments = {
     }
 
     setPref(preferences, preferenceName, preferenceType, preferenceValue);
     PreferenceExperiments.startObserver(name, preferenceName, preferenceType, preferenceValue);
     store.data[name] = experiment;
     store.saveSoon();
 
     TelemetryEnvironment.setExperimentActive(name, branch, {type: EXPERIMENT_TYPE_PREFIX + experimentType});
+    TelemetryEvents.sendEvent("enroll", "preference_study", name, {experimentType, branch});
     await this.saveStartupPrefs();
   },
 
   /**
    * Register a preference observer that stops an experiment when the user
    * modifies the preference.
    * @param {string} experimentName
    * @param {string} preferenceName
@@ -372,18 +377,20 @@ this.PreferenceExperiments = {
       );
     }
 
     const observerInfo = {
       preferenceName,
       observer() {
         const newValue = getPref(UserPreferences, preferenceName, preferenceType);
         if (newValue !== preferenceValue) {
-          PreferenceExperiments.stop(experimentName, false)
-                               .catch(Cu.reportError);
+          PreferenceExperiments.stop(experimentName, {
+            didResetValue: false,
+            reason: "user-preference-changed",
+          }).catch(Cu.reportError);
         }
       },
     };
     experimentObservers.set(experimentName, observerInfo);
     Services.prefs.addObserver(preferenceName, observerInfo.observer);
   },
 
   /**
@@ -443,24 +450,32 @@ this.PreferenceExperiments = {
     store.data[experimentName].lastSeen = new Date().toJSON();
     store.saveSoon();
   },
 
   /**
    * Stop an active experiment, deactivate preference watchers, and optionally
    * reset the associated preference to its previous value.
    * @param {string} experimentName
-   * @param {boolean} [resetValue=true]
-   *   If true, reset the preference to its original value.
+   * @param {Object} options
+   * @param {boolean} [options.resetValue = true]
+   *   If true, reset the preference to its original value prior to
+   *   the experiment. Optional, defauls to true.
+   * @param {String} [options.reason = "unknown"]
+   *   Reason that the experiment is ending. Optional, defaults to
+   *   "unknown".
    * @rejects {Error}
    *   If there is no stored experiment with the given name, or if the
    *   experiment has already expired.
    */
-  async stop(experimentName, resetValue = true) {
+  async stop(experimentName, {resetValue = true, reason = "unknown"} = {}) {
     log.debug(`PreferenceExperiments.stop(${experimentName})`);
+    if (reason === "unknown") {
+      log.warn(`experiment ${experimentName} ending for unknown reason`);
+    }
 
     const store = await ensureStorage();
     if (!(experimentName in store.data)) {
       throw new Error(`Could not find a preference experiment named "${experimentName}"`);
     }
 
     const experiment = store.data[experimentName];
     if (experiment.expired) {
@@ -491,16 +506,20 @@ this.PreferenceExperiments = {
         Services.prefs.getDefaultBranch("").deleteBranch(preferenceName);
       }
     }
 
     experiment.expired = true;
     store.saveSoon();
 
     TelemetryEnvironment.setExperimentInactive(experimentName, experiment.branch);
+    TelemetryEvents.sendEvent("unenroll", "preference_study", experimentName, {
+      didResetValue: resetValue ? "true" : "false",
+      reason,
+    });
     await this.saveStartupPrefs();
   },
 
   /**
    * Get the experiment object for the named experiment.
    * @param {string} experimentName
    * @resolves {Experiment}
    * @rejects {Error}
--- a/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm
+++ b/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm
@@ -68,32 +68,34 @@ this.ShieldPreferences = {
         break;
     }
   },
 
   async observePrefChange(prefName) {
     let prefValue;
     switch (prefName) {
       // If the FHR pref changes, set the opt-out-study pref to the value it is changing to.
-      case FHR_UPLOAD_ENABLED_PREF:
+      case FHR_UPLOAD_ENABLED_PREF: {
         prefValue = Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF);
         Services.prefs.setBoolPref(OPT_OUT_STUDIES_ENABLED_PREF, prefValue);
         break;
+      }
 
       // If the opt-out pref changes to be false, disable all current studies.
-      case OPT_OUT_STUDIES_ENABLED_PREF:
+      case OPT_OUT_STUDIES_ENABLED_PREF: {
         prefValue = Services.prefs.getBoolPref(OPT_OUT_STUDIES_ENABLED_PREF);
         if (!prefValue) {
           for (const study of await AddonStudies.getAll()) {
             if (study.active) {
-              await AddonStudies.stop(study.recipeId);
+              await AddonStudies.stop(study.recipeId, "general-opt-out");
             }
           }
         }
         break;
+      }
     }
   },
 
   /**
    * Injects the opt-out-study preference checkbox into about:preferences and
    * handles events coming from the UI for it.
    */
   injectOptOutStudyCheckbox(doc) {
--- a/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm
+++ b/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm
@@ -17,32 +17,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PreferenceExperiments",
   "resource://shield-recipe-client/lib/PreferenceExperiments.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutPages",
   "resource://shield-recipe-client-content/AboutPages.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShieldPreferences",
   "resource://shield-recipe-client/lib/ShieldPreferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonStudies",
   "resource://shield-recipe-client/lib/AddonStudies.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEvents",
+  "resource://shield-recipe-client/lib/TelemetryEvents.jsm");
 
 this.EXPORTED_SYMBOLS = ["ShieldRecipeClient"];
 
-const {PREF_STRING, PREF_BOOL, PREF_INT} = Ci.nsIPrefBranch;
-
-const REASONS = {
-  APP_STARTUP: 1,      // The application is starting up.
-  APP_SHUTDOWN: 2,     // The application is shutting down.
-  ADDON_ENABLE: 3,     // The add-on is being enabled.
-  ADDON_DISABLE: 4,    // The add-on is being disabled. (Also sent during uninstallation)
-  ADDON_INSTALL: 5,    // The add-on is being installed.
-  ADDON_UNINSTALL: 6,  // The add-on is being uninstalled.
-  ADDON_UPGRADE: 7,    // The add-on is being upgraded.
-  ADDON_DOWNGRADE: 8,  // The add-on is being downgraded.
-};
-const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode";
 const PREF_LOGGING_LEVEL = "extensions.shield-recipe-client.logging.level";
 const SHIELD_INIT_NOTIFICATION = "shield-init-complete";
 
 let log = null;
 
 /**
  * Handles startup and shutdown of the entire add-on. Bootsrap.js defers to this
  * module for most tasks so that we can more easily test startup and shutdown
@@ -77,16 +66,22 @@ this.ShieldRecipeClient = {
     }
 
     try {
       ShieldPreferences.init();
     } catch (err) {
       log.error("Failed to initialize preferences UI:", err);
     }
 
+    try {
+      TelemetryEvents.init();
+    } catch (err) {
+      log.error("Failed to initialize telemetry events:", err);
+    }
+
     await RecipeRunner.init();
     Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION);
   },
 
   async shutdown(reason) {
     await CleanupManager.cleanup();
   },
 };
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/TelemetryEvents.jsm
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {utils: Cu, interfaces: Ci} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["TelemetryEvents"];
+
+const TELEMETRY_CATEGORY = "normandy";
+
+const TelemetryEvents = {
+  init() {
+    Services.telemetry.registerEvents(TELEMETRY_CATEGORY, {
+      enroll: {
+        methods: ["enroll"],
+        objects: ["preference_study", "addon_study"],
+        extra_keys: ["experimentType", "branch", "addonId", "addonVersion"],
+        record_on_release: true,
+      },
+
+      unenroll: {
+        methods: ["unenroll"],
+        objects: ["preference_study", "addon_study"],
+        extra_keys: ["reason", "didResetValue", "addonId", "addonVersion"],
+        record_on_release: true,
+      },
+    });
+  },
+
+  sendEvent(method, object, value, extra) {
+    Services.telemetry.recordEvent(TELEMETRY_CATEGORY, method, object, value, extra);
+  },
+};
--- a/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js
@@ -1,15 +1,16 @@
 "use strict";
 
 Cu.import("resource://gre/modules/IndexedDB.jsm", this);
 Cu.import("resource://testing-common/TestUtils.jsm", this);
 Cu.import("resource://testing-common/AddonTestUtils.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this);
 
 // Initialize test utils
 AddonTestUtils.initMochitest(this);
 
 let _startArgsFactoryId = 1;
 function startArgsFactory(args) {
   return Object.assign({
     recipeId: _startArgsFactoryId++,
@@ -170,18 +171,19 @@ decorate_task(
     );
 
     await Addons.uninstall(testOverwriteId);
   }
 );
 
 decorate_task(
   withWebExtension({version: "2.0"}),
+  withStub(TelemetryEvents, "sendEvent"),
   AddonStudies.withStudies(),
-  async function testStart([addonId, addonFile]) {
+  async function testStart([addonId, addonFile], sendEventStub) {
     const startupPromise = AddonTestUtils.promiseWebExtensionStartup(addonId);
     const addonUrl = Services.io.newFileURI(addonFile).spec;
 
     let addon = await Addons.get(addonId);
     is(addon, null, "Before start is called, the add-on is not installed.");
 
     const args = startArgsFactory({
       name: "Test Study",
@@ -206,16 +208,22 @@ decorate_task(
         addonUrl,
         active: true,
         studyStartDate: study.studyStartDate,
       },
       "start saves study data to storage",
     );
     ok(study.studyStartDate, "start assigns a value to the study start date.");
 
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["enroll", "addon_study", args.name, {addonId, addonVersion: "2.0"}],
+      "AddonStudies.start() should send the correct telemetry event"
+    );
+
     await AddonStudies.stop(args.recipeId);
   }
 );
 
 decorate_task(
   AddonStudies.withStudies(),
   async function testStopNoStudy() {
     await Assert.rejects(
@@ -240,24 +248,35 @@ decorate_task(
 );
 
 const testStopId = "testStop@example.com";
 decorate_task(
   AddonStudies.withStudies([
     studyFactory({active: true, addonId: testStopId, studyEndDate: null}),
   ]),
   withInstalledWebExtension({id: testStopId}),
-  async function testStop([study], [addonId, addonFile]) {
-    await AddonStudies.stop(study.recipeId);
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStop([study], [addonId, addonFile], sendEventStub) {
+    await AddonStudies.stop(study.recipeId, "test-reason");
     const newStudy = await AddonStudies.get(study.recipeId);
     ok(!newStudy.active, "stop marks the study as inactive.");
     ok(newStudy.studyEndDate, "stop saves the study end date.");
 
     const addon = await Addons.get(addonId);
     is(addon, null, "stop uninstalls the study add-on.");
+
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["unenroll", "addon_study", study.name, {
+        addonId,
+        addonVersion: study.addonVersion,
+        reason: "test-reason",
+      }],
+      "stop should send the correct telemetry event"
+    );
   }
 );
 
 decorate_task(
   AddonStudies.withStudies([
     studyFactory({active: true, addonId: "testStopWarn@example.com", studyEndDate: null}),
   ]),
   async function testStopWarn([study]) {
@@ -275,40 +294,53 @@ decorate_task(
 );
 
 decorate_task(
   AddonStudies.withStudies([
     studyFactory({active: true, addonId: "does.not.exist@example.com", studyEndDate: null}),
     studyFactory({active: true, addonId: "installed@example.com"}),
     studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}),
   ]),
+  withStub(TelemetryEvents, "sendEvent"),
   withInstalledWebExtension({id: "installed@example.com"}),
-  async function testInit([activeStudy, activeInstalledStudy, inactiveStudy]) {
+  async function testInit([activeUninstalledStudy, activeInstalledStudy, inactiveStudy], sendEventStub) {
     await AddonStudies.init();
 
-    const newActiveStudy = await AddonStudies.get(activeStudy.recipeId);
+    const newActiveStudy = await AddonStudies.get(activeUninstalledStudy.recipeId);
     ok(!newActiveStudy.active, "init marks studies as inactive if their add-on is not installed.");
     ok(
       newActiveStudy.studyEndDate,
       "init sets the study end date if a study's add-on is not installed."
     );
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["unenroll", "addon_study", activeUninstalledStudy.name, {
+        addonId: activeUninstalledStudy.addonId,
+        addonVersion: activeUninstalledStudy.addonVersion,
+        reason: "uninstalled-sideload",
+      }],
+      "AddonStudies.init() should send the correct telemetry event"
+    );
 
     const newInactiveStudy = await AddonStudies.get(inactiveStudy.recipeId);
     is(
       newInactiveStudy.studyEndDate.getFullYear(),
       2012,
       "init does not modify inactive studies."
     );
 
     const newActiveInstalledStudy = await AddonStudies.get(activeInstalledStudy.recipeId);
     Assert.deepEqual(
       activeInstalledStudy,
       newActiveInstalledStudy,
       "init does not modify studies whose add-on is still installed."
     );
+
+    // Only activeUninstalledStudy should have generated any events
+    ok(sendEventStub.calledOnce);
   }
 );
 
 decorate_task(
   AddonStudies.withStudies([
     studyFactory({active: true, addonId: "installed@example.com", studyEndDate: null}),
   ]),
   withInstalledWebExtension({id: "installed@example.com"}),
@@ -319,8 +351,23 @@ decorate_task(
     const newStudy = await AddonStudies.get(study.recipeId);
     ok(!newStudy.active, "Studies are marked as inactive when their add-on is uninstalled.");
     ok(
       newStudy.studyEndDate,
       "The study end date is set when the add-on for the study is uninstalled."
     );
   }
 );
+
+// stop should pass "unknown" to TelemetryEvents for `reason` if none specified
+decorate_task(
+  AddonStudies.withStudies([studyFactory({ active: true })]),
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStopUnknownReason([study], sendEventStub) {
+    await AddonStudies.stop(study.recipeId);
+    is(
+      sendEventStub.getCall(0).args[3].reason,
+      "unknown",
+      "stop should send the correct telemetry event",
+      "AddonStudies.stop() should use unknown as the default reason",
+    );
+  }
+);
--- a/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
@@ -1,14 +1,15 @@
 "use strict";
 
 Cu.import("resource://gre/modules/Preferences.jsm", this);
 Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this);
 
 // Save ourselves some typing
 const {withMockExperiments} = PreferenceExperiments;
 const DefaultPreferences = new Preferences({defaultBranch: true});
 const startupPrefs = "extensions.shield-recipe-client.startupExperimentPrefs";
 
 function experimentFactory(attrs) {
   return Object.assign({
@@ -96,17 +97,18 @@ decorate_task(
 );
 
 // start should save experiment data, modify the preference, and register a
 // watcher.
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "startObserver"),
-  async function testStart(experiments, mockPreferences, startObserverStub) {
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStart(experiments, mockPreferences, startObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference", "oldvalue", "default");
     mockPreferences.set("fake.preference", "uservalue", "user");
 
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "fake.preference",
       preferenceValue: "newvalue",
@@ -402,44 +404,57 @@ decorate_task(
 );
 
 // stop should mark the experiment as expired, stop its observer, and revert the
 // preference value.
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withSpy(PreferenceExperiments, "stopObserver"),
-  async function testStop(experiments, mockPreferences, stopObserverSpy) {
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStop(experiments, mockPreferences, stopObserverSpy, sendEventStub) {
+    // this assertion is mostly useful for --verify test runs, to make
+    // sure that tests clean up correctly.
     is(Preferences.get("fake.preference"), null, "preference should start unset");
+
     mockPreferences.set(`${startupPrefs}.fake.preference`, "experimentvalue", "user");
     mockPreferences.set("fake.preference", "experimentvalue", "default");
     experiments.test = experimentFactory({
       name: "test",
       expired: false,
       preferenceName: "fake.preference",
       preferenceValue: "experimentvalue",
       preferenceType: "string",
       previousPreferenceValue: "oldvalue",
       preferenceBranchType: "default",
     });
     PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue");
 
-    await PreferenceExperiments.stop("test");
+    await PreferenceExperiments.stop("test", {reason: "test-reason"});
     ok(stopObserverSpy.calledWith("test"), "stop removed an observer");
     is(experiments.test.expired, true, "stop marked the experiment as expired");
     is(
       DefaultPreferences.get("fake.preference"),
       "oldvalue",
       "stop reverted the preference to its previous value",
     );
     ok(
       !Services.prefs.prefHasUserValue(`${startupPrefs}.fake.preference`),
       "stop cleared the startup preference for fake.preference.",
     );
 
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["unenroll", "preference_study", experiments.test.name, {
+        didResetValue: "true",
+        reason: "test-reason",
+      }],
+      "stop should send the correct telemetry event"
+    );
+
     PreferenceExperiments.stopAllObservers();
   },
 );
 
 // stop should also support user pref experiments
 decorate_task(
   withMockExperiments,
   withMockPreferences,
@@ -500,35 +515,43 @@ decorate_task(
   }
 );
 
 // stop should not modify a preference if resetValue is false
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "stopObserver"),
-
-  async function(experiments, mockPreferences, stopObserver) {
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStopReset(experiments, mockPreferences, stopObserverStub, sendEventStub) {
     mockPreferences.set("fake.preference", "customvalue", "default");
     experiments.test = experimentFactory({
       name: "test",
       expired: false,
       preferenceName: "fake.preference",
       preferenceValue: "experimentvalue",
       preferenceType: "string",
       previousPreferenceValue: "oldvalue",
       peferenceBranchType: "default",
     });
 
-    await PreferenceExperiments.stop("test", false);
+    await PreferenceExperiments.stop("test", {reason: "test-reason", resetValue: false});
     is(
       DefaultPreferences.get("fake.preference"),
       "customvalue",
       "stop did not modify the preference",
     );
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["unenroll", "preference_study", experiments.test.name, {
+        didResetValue: "false",
+        reason: "test-reason",
+      }],
+      "stop should send the correct telemetry event"
+    );
   }
 );
 
 // get should throw if no experiment exists with the given name
 decorate_task(
   withMockExperiments,
   async function() {
     await Assert.rejects(
@@ -673,58 +696,87 @@ decorate_task(
   },
 );
 
 // starting and stopping experiments should register in telemetry
 decorate_task(
   withMockExperiments,
   withStub(TelemetryEnvironment, "setExperimentActive"),
   withStub(TelemetryEnvironment, "setExperimentInactive"),
-  async function testInitTelemetry(experiments, setActiveStub, setInactiveStub) {
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStartAndStopTelemetry(experiments, setActiveStub, setInactiveStub, sendEventStub) {
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "fake.preference",
       preferenceValue: "value",
       preferenceType: "string",
       preferenceBranchType: "default",
     });
 
     Assert.deepEqual(
       setActiveStub.getCall(0).args,
       ["test", "branch", {type: "normandy-exp"}],
-      "Experiment is registerd by start()",
+      "Experiment is registered by start()",
     );
-    await PreferenceExperiments.stop("test");
+    await PreferenceExperiments.stop("test", {reason: "test-reason"});
     ok(setInactiveStub.calledWith("test", "branch"), "Experiment is unregistered by stop()");
+
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["enroll", "preference_study", "test", {
+        experimentType: "exp",
+        branch: "branch",
+      }],
+      "PreferenceExperiments.start() should send the correct telemetry event"
+    );
+
+    Assert.deepEqual(
+      sendEventStub.getCall(1).args,
+      ["unenroll", "preference_study", "test", {
+        reason: "test-reason",
+        didResetValue: "true",
+      }],
+      "PreferenceExperiments.stop() should send the correct telemetry event"
+    );
   },
 );
 
 // starting experiments should use the provided experiment type
 decorate_task(
   withMockExperiments,
   withStub(TelemetryEnvironment, "setExperimentActive"),
   withStub(TelemetryEnvironment, "setExperimentInactive"),
-  async function testInitTelemetry(experiments, setActiveStub, setInactiveStub) {
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testInitTelemetryExperimentType(experiments, setActiveStub, setInactiveStub, sendEventStub) {
     await PreferenceExperiments.start({
       name: "test",
       branch: "branch",
       preferenceName: "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",
     );
 
+    Assert.deepEqual(
+      sendEventStub.getCall(0).args,
+      ["enroll", "preference_study", "test", {
+        experimentType: "pref-test",
+        branch: "branch",
+      }],
+      "start should include the passed reason in the telemetry event"
+    );
+
     // start sets the passed preference in a way that is hard to mock.
     // Reset the preference so it doesn't interfere with other tests.
     Services.prefs.getDefaultBranch("fake.preference").deleteBranch("");
   },
 );
 
 // Experiments shouldn't be recorded by init() in telemetry if they are expired
 decorate_task(
@@ -737,28 +789,29 @@ decorate_task(
   },
 );
 
 // Experiments should end if the preference has been changed when init() is called
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "stop"),
-  async function testInitChanges(experiments, mockPreferences, stopStub) {
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testInitChanges(experiments, mockPreferences, stopStub, sendEventStub) {
     mockPreferences.set("fake.preference", "experiment value", "default");
     experiments.test = experimentFactory({
       name: "test",
       preferenceName: "fake.preference",
       preferenceValue: "experiment value",
     });
     mockPreferences.set("fake.preference", "changed value");
     await PreferenceExperiments.init();
     ok(stopStub.calledWith("test"), "Experiment is stopped because value changed");
     is(Preferences.get("fake.preference"), "changed value", "Preference value was not changed");
-  }
+  },
 );
 
 // init should register an observer for experiments
 decorate_task(
   withMockExperiments,
   withMockPreferences,
   withStub(PreferenceExperiments, "startObserver"),
   withStub(PreferenceExperiments, "stop"),
@@ -930,8 +983,26 @@ decorate_task(
     await PreferenceExperiments.stop("test");
     is(
       Services.prefs.getCharPref(prefName, "DEFAULT"),
       "DEFAULT",
       "Preference should be absent",
     );
   },
 );
+
+// stop should pass "unknown" to telemetry event for `reason` if none is specified
+decorate_task(
+  withMockExperiments,
+  withMockPreferences,
+  withStub(PreferenceExperiments, "stopObserver"),
+  withStub(TelemetryEvents, "sendEvent"),
+  async function testStopUnknownReason(experiments, mockPreferences, stopObserverStub, sendEventStub) {
+    mockPreferences.set("fake.preference", "default value", "default");
+    experiments.test = experimentFactory({ name: "test", preferenceName: "fake.preference" });
+    await PreferenceExperiments.stop("test");
+    is(
+      sendEventStub.getCall(0).args[3].reason,
+      "unknown",
+      "PreferenceExperiments.stop() should use unknown as the default reason",
+    );
+  }
+);
--- a/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js
@@ -1,22 +1,24 @@
 "use strict";
 
 Cu.import("resource://shield-recipe-client/lib/ShieldRecipeClient.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this);
 Cu.import("resource://shield-recipe-client-content/AboutPages.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this);
+Cu.import("resource://shield-recipe-client/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");
@@ -34,36 +36,53 @@ decorate_task(
   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/browser/extensions/shield-recipe-client/test/browser/head.js
+++ b/browser/extensions/shield-recipe-client/test/browser/head.js
@@ -3,16 +3,17 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/Preferences.jsm", this);
 Cu.import("resource://testing-common/AddonTestUtils.jsm", this);
 Cu.import("resource://testing-common/TestUtils.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/TelemetryEvents.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/Utils.jsm", this);
 
 // Load mocking/stubbing library, sinon
 // docs: http://sinonjs.org/docs/
 /* global sinon */
 Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
 
 // Make sinon assertions fail in a way that mochitest understands
@@ -20,16 +21,18 @@ sinon.assert.fail = function(message) {
   ok(false, message);
 };
 
 registerCleanupFunction(async function() {
   // Cleanup window or the test runner will throw an error
   delete window.sinon;
 });
 
+// Prep Telemetry to receive events from tests
+TelemetryEvents.init();
 
 this.UUID_REGEX = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;
 
 this.TEST_XPI_URL = (function() {
   const dir = getChromeDir(getResolvedURI(gTestPath));
   dir.append("fixtures");
   dir.append("normandy.xpi");
   return Services.io.newFileURI(dir).spec;
--- a/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY
+++ b/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY
@@ -1,40 +1,29 @@
-fbjs@0.8.14 BSD-3-Clause
-BSD License
-
-For fbjs software
+fbjs@0.8.16 MIT
+MIT License
 
 Copyright (c) 2013-present, Facebook, Inc.
-All rights reserved.
 
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice, this
-   list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
 
- * Neither the name Facebook nor the names of its contributors may be used to
-   endorse or promote products derived from this software without specific
-   prior written permission.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 
 react-dom@15.6.1 BSD-3-Clause
 BSD License
 
 For React software
 
 Copyright (c) 2013-present, Facebook, Inc.
@@ -152,48 +141,38 @@ WARRANTIES OF MERCHANTABILITY AND FITNES
 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-create-react-class@15.6.0 BSD-3-Clause
-BSD License
-
-For React software
+create-react-class@15.6.2 MIT
+MIT License
 
 Copyright (c) 2013-present, Facebook, Inc.
-All rights reserved.
 
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice, this
-   list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
- * Neither the name Facebook nor the names of its contributors may be used to
-   endorse or promote products derived from this software without specific
-   prior written permission.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
 
 
 mozjexl@1.1.5 MIT
 Copyright for portions of mozJexl are held by TechnologyAdvice, 2015 as part of Jexl.
 All other copyright for mozJexl are held by the Mozilla Foundation, 2017.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal