Backed out changeset d7a94bbfa3ef (bug 1140558)
authorWes Kocher <wkocher@mozilla.com>
Fri, 27 Mar 2015 15:31:17 -0700
changeset 236249 832a874a7ccc434b7013553e191920f9fed078c9
parent 236248 83e259349f528ac62f6b4b3260a222ffa7188666
child 236250 b263e867106537f3093c2ccafa8894332ef3265e
push id14795
push userphilringnalda@gmail.com
push dateSat, 28 Mar 2015 18:59:27 +0000
treeherderb2g-inbound@b08b0a413b32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1140558
milestone39.0a1
backs outd7a94bbfa3ef6a267d711d4e806fd3c6bbde0a77
Backed out changeset d7a94bbfa3ef (bug 1140558)
toolkit/components/telemetry/TelemetryEnvironment.jsm
toolkit/components/telemetry/TelemetryPing.jsm
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/docs/environment.rst
toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -4,17 +4,16 @@
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "TelemetryEnvironment",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-const myScope = this;
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/PromiseUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -25,112 +24,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 #endif
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
                                   "resource://gre/modules/ProfileAge.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
-var gGlobalEnvironment;
-function getGlobal() {
-  if (!gGlobalEnvironment) {
-    gGlobalEnvironment = new EnvironmentCache();
-  }
-  return gGlobalEnvironment;
-}
-
-const TelemetryEnvironment = {
-  get currentEnvironment() {
-    return getGlobal().currentEnvironment;
-  },
-
-  onInitialized: function() {
-    return getGlobal().onInitialized();
-  },
-
-  registerChangeListener: function(name, listener) {
-    return getGlobal().registerChangeListener(name, listener);
-  },
-
-  unregisterChangeListener: function(name) {
-    return getGlobal().unregisterChangeListener(name);
-  },
-
-  // Policy to use when saving preferences. Exported for using them in tests.
-  RECORD_PREF_STATE: 1, // Don't record the preference value
-  RECORD_PREF_VALUE: 2, // We only record user-set prefs.
-  RECORD_PREF_NOTIFY_ONLY: 3, // Record nothing, just notify of changes.
-
-  // Testing method
-  _watchPreferences: function(prefMap) {
-    return getGlobal()._watchPreferences(prefMap);
-  },
-};
-
-const DEFAULT_ENVIRONMENT_PREFS = new Map([
-  ["app.feedback.baseURL", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.support.baseURL", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["accessibility.browsewithcaret", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["accessibility.force_disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.update.auto", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.update.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.update.interval", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.update.service.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.update.silent", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["app.update.url", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.cache.disk.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.cache.disk.capacity", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.cache.memory.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.cache.offline.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.formfill.enable", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.newtab.url", TelemetryEnvironment.RECORD_PREF_STATE],
-  ["browser.newtabpage.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.newtabpage.enhanced", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.polaris.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.shell.checkDefaultBrowser", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["browser.startup.homepage", TelemetryEnvironment.RECORD_PREF_STATE],
-  ["browser.startup.page", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["devtools.chrome.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["devtools.debugger.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["devtools.debugger.remote-enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["dom.ipc.plugins.asyncInit", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["dom.ipc.plugins.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["experiments.manifest.uri", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["extensions.blocklist.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["extensions.blocklist.url", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["extensions.strictCompatibility", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["extensions.update.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["extensions.update.url", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["extensions.update.background.url", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["general.smoothScroll", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["gfx.direct2d.disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["gfx.direct2d.force-enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["gfx.direct2d.use1_1", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.acceleration.disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.acceleration.force-enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.async-pan-zoom.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.async-video-oop.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.async-video.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.componentalpha.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.d3d11.disable-warp", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.d3d11.force-warp", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.prefer-d3d9", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layers.prefer-opengl", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["layout.css.devPixelsPerPx", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["network.proxy.autoconfig_url", TelemetryEnvironment.RECORD_PREF_STATE],
-  ["network.proxy.http", TelemetryEnvironment.RECORD_PREF_STATE],
-  ["network.proxy.ssl", TelemetryEnvironment.RECORD_PREF_STATE],
-  ["pdfjs.disabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["places.history.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["privacy.trackingprotection.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["privacy.donottrackheader.enabled", TelemetryEnvironment.RECORD_PREF_VALUE],
-  ["services.sync.serverURL", TelemetryEnvironment.RECORD_PREF_STATE],
-]);
-
 const LOGGER_NAME = "Toolkit.Telemetry";
 
 const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
 const PREF_DISTRIBUTION_ID = "distribution.id";
 const PREF_DISTRIBUTION_VERSION = "distribution.version";
 const PREF_DISTRIBUTOR = "app.distributor";
 const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
 const PREF_E10S_ENABLED = "browser.tabs.remote.autostart";
@@ -310,486 +213,205 @@ function getServicePack() {
       minor: null,
     };
   } finally {
     kernel32.close();
   }
 }
 #endif
 
-/**
- * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
- * is owned by a parent environment object and is an addon listener.
- */
-function EnvironmentAddonBuilder(environment) {
-  this._environment = environment;
+this.TelemetryEnvironment = {
+  _shutdown: true,
 
-  // The pending task blocks addon manager shutdown. It can either be the initial load
-  // or a change load.
-  this._pendingTask = null;
+  // A map of (sync) listeners that will be called on environment changes.
+  _changeListeners: new Map(),
+  // Async task for collecting the environment data.
+  _collectTask: null,
+
+  // Policy to use when saving preferences. Exported for using them in tests.
+  RECORD_PREF_STATE: 1, // Don't record the preference value
+  RECORD_PREF_VALUE: 2, // We only record user-set prefs.
+  RECORD_PREF_NOTIFY_ONLY: 3, // Record nothing, just notify of changes.
 
-  // Set to true once initial load is complete and we're watching for changes.
-  this._loaded = false;
-}
-EnvironmentAddonBuilder.prototype = {
-  /**
-   * Get the initial set of addons.
-   * @returns Promise<void> when the initial load is complete.
-   */
-  init: function() {
-    // Some tests don't initialize the addon manager. This accounts for the
-    // unfortunate reality of life.
-    try {
-      AddonManager.shutdown.addBlocker("EnvironmentAddonBuilder",
-        () => this._shutdownBlocker());
-    } catch (err) {
-      return Promise.reject(err);
-    }
+  // A map of watched preferences which trigger an Environment change when modified.
+  // Every entry contains a recording policy (RECORD_PREF_*).
+  _watchedPrefs: null,
 
-    this._pendingTask = this._updateAddons().then(
-      () => { this._pendingTask = null; },
-      (err) => {
-        this._environment._log.error("init - Exception in _updateAddons", err);
-        this._pendingTask = null;
-      }
-    );
+  // The Addons change listener, init by |_startWatchingAddons| .
+  _addonsListener: null,
 
-    return this._pendingTask;
-  },
+  // AddonManager may shut down before us, in which case we cache the addons here.
+  // It is always null if the AM didn't shut down before us.
+  // If cached, it is an object containing the addon information, suitable for use
+  // in the environment data.
+  _cachedAddons: null,
 
   /**
-   * Register an addon listener and watch for changes.
+   * Initialize TelemetryEnvironment.
    */
-  watchForChanges: function() {
-    this._loaded = true;
-    AddonManager.addAddonListener(this);
-    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
-  },
-
-  // AddonListener
-  onEnabled: function() {
-    this._onChange();
-  },
-  onDisabled: function() {
-    this._onChange();
-  },
-  onInstalled: function() {
-    this._onChange();
-  },
-  onUninstalling: function() {
-    this._onChange();
-  },
-
-  _onChange: function() {
-    if (this._pendingTask) {
-      this._environment._log.trace("_onChange - task already pending");
+  init: function () {
+    if (!this._shutdown) {
+      this._log.error("init - Already initialized");
       return;
     }
 
-    this._environment._log.trace("_onChange");
-    this._pendingTask = this._updateAddons().then(
-      (changed) => {
-        this._pendingTask = null;
-        if (changed) {
-          this._environment._onEnvironmentChange("addons-changed");
-        }
-      },
-      (err) => {
-        this._pendingTask = null;
-        this._environment._log.error("Error collecting addons", err);
-      });
-  },
-
-  // nsIObserver
-  observe: function (aSubject, aTopic, aData) {
-    this._environment._log.trace("observe - Topic " + aTopic);
+    this._configureLog();
+    this._log.trace("init");
+    this._shutdown = false;
+    this._startWatchingPrefs();
+    this._startWatchingAddons();
 
-    if (aTopic == EXPERIMENTS_CHANGED_TOPIC) {
-      if (this._pendingTask) {
-        return;
-      }
-      this._pendingTask = this._updateAddons().then(
-        (changed) => {
-          this._pendingTask = null;
-          if (changed) {
-            this._environment._onEnvironmentChange("experiment-changed");
-          }
-        },
-        (err) => {
-          this._pendingTask = null;
-          this._environment._log.error("observe: Error collecting addons", err);
-        });
-    }
-  },
-
-  _shutdownBlocker: function() {
-    if (this._loaded) {
-      AddonManager.removeAddonListener(this);
-      Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
-    }
-    return this._pendingTask;
+    AddonManager.shutdown.addBlocker("TelemetryEnvironment: caching addons",
+                                      () => this._blockAddonManagerShutdown(),
+                                      () => this._getState());
   },
 
   /**
-   * Collect the addon data for the environment.
-   *
-   * This should only be called from _pendingTask; otherwise we risk
-   * running this during addon manager shutdown.
-   *
-   * @returns Promise<bool> whether the environment changed.
+   * Shutdown TelemetryEnvironment.
+   * @return Promise<> that is resolved when shutdown is finished.
    */
-  _updateAddons: Task.async(function* () {
-    this._environment._log.trace("_updateAddons");
-    let personaId = null;
-#ifndef MOZ_WIDGET_GONK
-    let theme = LightweightThemeManager.currentTheme;
-    if (theme) {
-      personaId = theme.id;
-    }
-#endif
-
-    let addons = {
-      activeAddons: yield this._getActiveAddons(),
-      theme: yield this._getActiveTheme(),
-      activePlugins: this._getActivePlugins(),
-      activeGMPlugins: yield this._getActiveGMPlugins(),
-      activeExperiment: this._getActiveExperiment(),
-      persona: personaId,
-    };
-
-    if (JSON.stringify(addons) !=
-        JSON.stringify(this._environment._currentEnvironment.addons)) {
-      this._environment._log.trace("_updateAddons: addons differ");
-      this._environment._currentEnvironment.addons = addons;
-      return true;
-    }
-    this._environment._log.trace("_updateAddons: no changes found");
-    return false;
-  }),
-
-  /**
-   * Get the addon data in object form.
-   * @return Promise<object> containing the addon data.
-   */
-  _getActiveAddons: Task.async(function* () {
-    // Request addons, asynchronously.
-    let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
-
-    let activeAddons = {};
-    for (let addon of allAddons) {
-      // Skip addons which are not active.
-      if (!addon.isActive) {
-        continue;
+  shutdown: Task.async(function* () {
+    if (this._shutdown) {
+      if (this._log) {
+        this._log.error("shutdown - Already shut down");
+      } else {
+        Cu.reportError("TelemetryEnvironment.shutdown - Already shut down");
       }
-
-      // Make sure to have valid dates.
-      let installDate = new Date(Math.max(0, addon.installDate));
-      let updateDate = new Date(Math.max(0, addon.updateDate));
-
-      activeAddons[addon.id] = {
-        blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
-        description: addon.description,
-        name: addon.name,
-        userDisabled: addon.userDisabled,
-        appDisabled: addon.appDisabled,
-        version: addon.version,
-        scope: addon.scope,
-        type: addon.type,
-        foreignInstall: addon.foreignInstall,
-        hasBinaryComponents: addon.hasBinaryComponents,
-        installDay: truncateToDays(installDate.getTime()),
-        updateDay: truncateToDays(updateDate.getTime()),
-      };
+      return;
     }
 
-    return activeAddons;
-  }),
-
-  /**
-   * Get the currently active theme data in object form.
-   * @return Promise<object> containing the active theme data.
-   */
-  _getActiveTheme: Task.async(function* () {
-    // Request themes, asynchronously.
-    let themes = yield promiseGetAddonsByTypes(["theme"]);
+    this._log.trace("shutdown");
+    this._shutdown = true;
+    this._stopWatchingPrefs();
+    this._stopWatchingAddons();
+    this._changeListeners.clear();
+    yield this._collectTask;
 
-    let activeTheme = {};
-    // We only store information about the active theme.
-    let theme = themes.find(theme => theme.isActive);
-    if (theme) {
-      // Make sure to have valid dates.
-      let installDate = new Date(Math.max(0, theme.installDate));
-      let updateDate = new Date(Math.max(0, theme.updateDate));
-
-      activeTheme = {
-        id: theme.id,
-        blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
-        description: theme.description,
-        name: theme.name,
-        userDisabled: theme.userDisabled,
-        appDisabled: theme.appDisabled,
-        version: theme.version,
-        scope: theme.scope,
-        foreignInstall: theme.foreignInstall,
-        hasBinaryComponents: theme.hasBinaryComponents,
-        installDay: truncateToDays(installDate.getTime()),
-        updateDay: truncateToDays(updateDate.getTime()),
-      };
-    }
-
-    return activeTheme;
+    this._cachedAddons = null;
   }),
 
-  /**
-   * Get the plugins data in object form.
-   * @return Object containing the plugins data.
-   */
-  _getActivePlugins: function () {
-    let pluginTags =
-      Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({});
-
-    let activePlugins = [];
-    for (let tag of pluginTags) {
-      // Skip plugins which are not active.
-      if (tag.disabled) {
-        continue;
-      }
-
-      // Make sure to have a valid date.
-      let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
-
-      activePlugins.push({
-        name: tag.name,
-        version: tag.version,
-        description: tag.description,
-        blocklisted: tag.blocklisted,
-        disabled: tag.disabled,
-        clicktoplay: tag.clicktoplay,
-        mimeTypes: tag.getMimeTypes({}),
-        updateDay: truncateToDays(updateDate.getTime()),
-      });
-    }
-
-    return activePlugins;
-  },
-
-  /**
-   * Get the GMPlugins data in object form.
-   * @return Object containing the GMPlugins data.
-   *
-   * This should only be called from _pendingTask; otherwise we risk
-   * running this during addon manager shutdown.
-   */
-  _getActiveGMPlugins: Task.async(function* () {
-    // Request plugins, asynchronously.
-    let allPlugins = yield promiseGetAddonsByTypes(["plugin"]);
-
-    let activeGMPlugins = {};
-    for (let plugin of allPlugins) {
-      // Only get GM Plugin info.
-      if (!plugin.isGMPlugin) {
-        continue;
-      }
-
-      activeGMPlugins[plugin.id] = {
-        version: plugin.version,
-        userDisabled: plugin.userDisabled,
-        applyBackgroundUpdates: plugin.applyBackgroundUpdates,
-      };
+  _configureLog: function () {
+    if (this._log) {
+      return;
     }
-
-    return activeGMPlugins;
-  }),
-
-  /**
-   * Get the active experiment data in object form.
-   * @return Object containing the active experiment data.
-   */
-  _getActiveExperiment: function () {
-    let experimentInfo = {};
-    try {
-      let scope = {};
-      Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
-      let experiments = scope.Experiments.instance();
-      let activeExperiment = experiments.getActiveExperimentID();
-      if (activeExperiment) {
-        experimentInfo.id = activeExperiment;
-        experimentInfo.branch = experiments.getActiveExperimentBranch();
-      }
-    } catch(e) {
-      // If this is not Firefox, the import will fail.
-    }
-
-    return experimentInfo;
-  },
-};
-
-function EnvironmentCache() {
-  this._log = Log.repository.getLoggerWithMessagePrefix(
-    LOGGER_NAME, "TelemetryEnvironment::");
-  this._log.trace("constructor");
-
-  this._shutdown = false;
-
-  // A map of listeners that will be called on environment changes.
-  this._changeListeners = new Map();
-
-  // A map of watched preferences which trigger an Environment change when
-  // modified. Every entry contains a recording policy (RECORD_PREF_*).
-  this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
-
-  this._currentEnvironment = {
-    build: this._getBuild(),
-    partner: this._getPartner(),
-    system: this._getSystem(),
-  };
-
-  this._updateSettings();
-
-#ifndef MOZ_WIDGET_ANDROID
-  this._currentEnvironment.profile = {};
-#endif
-
-  // Build the remaining asynchronous parts of the environment. Don't register change listeners
-  // until the initial environment has been built.
-
-  this._addonBuilder = new EnvironmentAddonBuilder(this);
-
-  this._initTask = Promise.all([this._addonBuilder.init(), this._updateProfile()])
-    .then(
-      () => {
-        this._initTask = null;
-        this._startWatchingPrefs();
-        this._addonBuilder.watchForChanges();
-        return this.currentEnvironment;
-      },
-      (err) => {
-        // log errors but eat them for consumers
-        this._log.error("error while initializing", err);
-        this._initTask = null;
-        this._startWatchingPrefs();
-        this._addonBuilder.watchForChanges();
-        return this.currentEnvironment;
-      });
-}
-EnvironmentCache.prototype = {
-  /**
-   * The current environment data. The returned data is cloned to avoid
-   * unexpected sharing or mutation.
-   * @returns object
-   */
-  get currentEnvironment() {
-    return Cu.cloneInto(this._currentEnvironment, myScope);
-  },
-
-  /**
-   * Wait for the current enviroment to be fully initialized.
-   * @returns Promise<object>
-   */
-  onInitialized: function() {
-    if (this._initTask) {
-      return this._initTask;
-    }
-    return Promise.resolve(this.currentEnvironment);
+    this._log = Log.repository.getLoggerWithMessagePrefix(
+                                 LOGGER_NAME, "TelemetryEnvironment::");
   },
 
   /**
    * Register a listener for environment changes.
-   * @param name The name of the listener. If a new listener is registered
-   *             with the same name, the old listener will be replaced.
-   * @param listener function(reason, environment)
+   * It's fine to call this on an unitialized TelemetryEnvironment.
+   * @param name The name of the listener - good for debugging purposes.
+   * @param listener A JS callback function.
    */
   registerChangeListener: function (name, listener) {
+    this._configureLog();
     this._log.trace("registerChangeListener for " + name);
     if (this._shutdown) {
-      this._log.warn("registerChangeListener - already shutdown");
+      this._log.warn("registerChangeListener - already shutdown")
       return;
     }
     this._changeListeners.set(name, listener);
   },
 
   /**
    * Unregister from listening to environment changes.
    * It's fine to call this on an unitialized TelemetryEnvironment.
    * @param name The name of the listener to remove.
    */
   unregisterChangeListener: function (name) {
+    this._configureLog();
     this._log.trace("unregisterChangeListener for " + name);
     if (this._shutdown) {
-      this._log.warn("registerChangeListener - already shutdown");
+      this._log.warn("registerChangeListener - already shutdown")
       return;
     }
     this._changeListeners.delete(name);
   },
 
   /**
    * Only used in tests, set the preferences to watch.
    * @param aPreferences A map of preferences names and their recording policy.
    */
   _watchPreferences: function (aPreferences) {
-    this._stopWatchingPrefs();
+    if (this._watchedPrefs) {
+      this._stopWatchingPrefs();
+    }
+
     this._watchedPrefs = aPreferences;
-    this._updateSettings();
     this._startWatchingPrefs();
   },
 
   /**
    * Get an object containing the values for the watched preferences. Depending on the
    * policy, the value for a preference or whether it was changed by user is reported.
    *
    * @return An object containing the preferences values.
    */
   _getPrefData: function () {
+    if (!this._watchedPrefs) {
+      return {};
+    }
+
     let prefData = {};
     for (let pref in this._watchedPrefs) {
       // Only record preferences if they are non-default and policy allows recording.
       if (!Preferences.isSet(pref) ||
-          this._watchedPrefs[pref] == TelemetryEnvironment.RECORD_PREF_NOTIFY_ONLY) {
+          this._watchedPrefs[pref] == this.RECORD_PREF_NOTIFY_ONLY) {
         continue;
       }
 
       // Check the policy for the preference and decide if we need to store its value
       // or whether it changed from the default value.
       let prefValue = undefined;
-      if (this._watchedPrefs[pref] == TelemetryEnvironment.RECORD_PREF_STATE) {
-        prefValue = "<user-set>";
+      if (this._watchedPrefs[pref] == this.RECORD_PREF_STATE) {
+        prefValue = null;
       } else {
         prefValue = Preferences.get(pref, null);
       }
       prefData[pref] = prefValue;
     }
     return prefData;
   },
 
   /**
    * Start watching the preferences.
    */
   _startWatchingPrefs: function () {
     this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
 
+    if (!this._watchedPrefs) {
+      return;
+    }
+
     for (let pref in this._watchedPrefs) {
       Preferences.observe(pref, this._onPrefChanged, this);
     }
   },
 
-  _onPrefChanged: function() {
-    this._log.trace("_onPrefChanged");
-    this._updateSettings();
-    this._onEnvironmentChange("pref-changed");
-  },
-
   /**
    * Do not receive any more change notifications for the preferences.
    */
   _stopWatchingPrefs: function () {
     this._log.trace("_stopWatchingPrefs");
 
+    if (!this._watchedPrefs) {
+      return;
+    }
+
     for (let pref in this._watchedPrefs) {
       Preferences.ignore(pref, this._onPrefChanged, this);
     }
+
+    this._watchedPrefs = null;
+  },
+
+  _onPrefChanged: function () {
+    this._log.trace("_onPrefChanged");
+    this._onEnvironmentChange("pref-changed");
   },
 
   /**
    * Get the build data in object form.
    * @return Object containing the build data.
    */
   _getBuild: function () {
     let buildData = {
@@ -843,25 +465,26 @@ EnvironmentCache.prototype = {
         return null;
       }
     }
 
     return null;
   },
 
   /**
-   * Update the cached settings data.
+   * Get the settings data in object form.
+   * @return Object containing the settings.
    */
-  _updateSettings: function () {
+  _getSettings: function () {
     let updateChannel = null;
     try {
       updateChannel = UpdateChannel.get();
     } catch (e) {}
 
-    this._currentEnvironment.settings = {
+    return {
       blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true),
 #ifndef MOZ_WIDGET_ANDROID
       isDefaultBrowser: this._isDefaultBrowser(),
 #endif
       e10sEnabled: Preferences.get(PREF_E10S_ENABLED, false),
       telemetryEnabled: Preferences.get(PREF_TELEMETRY_ENABLED, false),
       locale: getBrowserLocale(),
       update: {
@@ -869,30 +492,33 @@ EnvironmentCache.prototype = {
         enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
         autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
       },
       userPrefs: this._getPrefData(),
     };
   },
 
   /**
-   * Update the cached profile data.
-   * @returns Promise<> resolved when the I/O is complete.
+   * Get the profile data in object form.
+   * @return Object containing the profile data.
    */
-  _updateProfile: Task.async(function* () {
+  _getProfile: Task.async(function* () {
     let profileAccessor = new ProfileAge(null, this._log);
 
     let creationDate = yield profileAccessor.created;
     let resetDate = yield profileAccessor.reset;
 
-    this._currentEnvironment.profile.creationDate =
-      truncateToDays(creationDate);
+    let profileData = {
+      creationDate: truncateToDays(creationDate),
+    };
+
     if (resetDate) {
-      this._currentEnvironment.profile.resetDate = truncateToDays(resetDate);
+      profileData.resetDate = truncateToDays(resetDate);
     }
+    return profileData;
   }),
 
   /**
    * Get the partner data in object form.
    * @return Object containing the partner data.
    */
   _getPartner: function () {
     let partnerData = {
@@ -900,17 +526,17 @@ EnvironmentCache.prototype = {
       distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null),
       partnerId: Preferences.get(PREF_PARTNER_ID, null),
       distributor: Preferences.get(PREF_DISTRIBUTOR, null),
       distributorChannel: Preferences.get(PREF_DISTRIBUTOR_CHANNEL, null),
     };
 
     // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
     let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH);
-    partnerData.partnerNames = partnerBranch.getChildList("");
+    partnerData.partnerNames = partnerBranch.getChildList("")
 
     return partnerData;
   },
 
   /**
    * Get the CPU information.
    * @return Object containing the CPU information data.
    */
@@ -1054,25 +680,398 @@ EnvironmentCache.prototype = {
       device: this._getDeviceData(),
 #endif
       os: this._getOSData(),
       hdd: this._getHDDData(),
       gfx: this._getGFXData(),
     };
   },
 
+  /**
+   * Get the addon data in object form.
+   * @return Object containing the addon data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getActiveAddons: Task.async(function* () {
+
+    // Request addons, asynchronously.
+    let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
+
+    let activeAddons = {};
+    for (let addon of allAddons) {
+      // Skip addons which are not active.
+      if (!addon.isActive) {
+        continue;
+      }
+
+      // Make sure to have valid dates.
+      let installDate = new Date(Math.max(0, addon.installDate));
+      let updateDate = new Date(Math.max(0, addon.updateDate));
+
+      activeAddons[addon.id] = {
+        blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+        description: addon.description,
+        name: addon.name,
+        userDisabled: addon.userDisabled,
+        appDisabled: addon.appDisabled,
+        version: addon.version,
+        scope: addon.scope,
+        type: addon.type,
+        foreignInstall: addon.foreignInstall,
+        hasBinaryComponents: addon.hasBinaryComponents,
+        installDay: truncateToDays(installDate.getTime()),
+        updateDay: truncateToDays(updateDate.getTime()),
+      };
+    }
+
+    return activeAddons;
+  }),
+
+  /**
+   * Get the currently active theme data in object form.
+   * @return Object containing the active theme data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getActiveTheme: Task.async(function* () {
+    // Request themes, asynchronously.
+    let themes = yield promiseGetAddonsByTypes(["theme"]);
+
+    let activeTheme = {};
+    // We only store information about the active theme.
+    let theme = themes.find(theme => theme.isActive);
+    if (theme) {
+      // Make sure to have valid dates.
+      let installDate = new Date(Math.max(0, theme.installDate));
+      let updateDate = new Date(Math.max(0, theme.updateDate));
+
+      activeTheme = {
+        id: theme.id,
+        blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+        description: theme.description,
+        name: theme.name,
+        userDisabled: theme.userDisabled,
+        appDisabled: theme.appDisabled,
+        version: theme.version,
+        scope: theme.scope,
+        foreignInstall: theme.foreignInstall,
+        hasBinaryComponents: theme.hasBinaryComponents,
+        installDay: truncateToDays(installDate.getTime()),
+        updateDay: truncateToDays(updateDate.getTime()),
+      };
+    }
+
+    return activeTheme;
+  }),
+
+  /**
+   * Get the plugins data in object form.
+   * @return Object containing the plugins data.
+   */
+  _getActivePlugins: function () {
+    let pluginTags =
+      Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({});
+
+    let activePlugins = [];
+    for (let tag of pluginTags) {
+      // Skip plugins which are not active.
+      if (tag.disabled) {
+        continue;
+      }
+
+      // Make sure to have a valid date.
+      let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
+
+      activePlugins.push({
+        name: tag.name,
+        version: tag.version,
+        description: tag.description,
+        blocklisted: tag.blocklisted,
+        disabled: tag.disabled,
+        clicktoplay: tag.clicktoplay,
+        mimeTypes: tag.getMimeTypes({}),
+        updateDay: truncateToDays(updateDate.getTime()),
+      });
+    }
+
+    return activePlugins;
+  },
+
+  /**
+   * Get the GMPlugins data in object form.
+   * @return Object containing the GMPlugins data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getActiveGMPlugins: Task.async(function* () {
+    // Request plugins, asynchronously.
+    let allPlugins = yield promiseGetAddonsByTypes(["plugin"]);
+
+    let activeGMPlugins = {};
+    for (let plugin of allPlugins) {
+      // Only get GM Plugin info.
+      if (!plugin.isGMPlugin) {
+        continue;
+      }
+
+      activeGMPlugins[plugin.id] = {
+        version: plugin.version,
+        userDisabled: plugin.userDisabled,
+        applyBackgroundUpdates: plugin.applyBackgroundUpdates,
+      };
+    }
+
+    return activeGMPlugins;
+  }),
+
+  /**
+   * Get the active experiment data in object form.
+   * @return Object containing the active experiment data.
+   */
+  _getActiveExperiment: function () {
+    let experimentInfo = {};
+    try {
+      let scope = {};
+      Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
+      let experiments = scope.Experiments.instance()
+      let activeExperiment = experiments.getActiveExperimentID();
+      if (activeExperiment) {
+        experimentInfo.id = activeExperiment;
+        experimentInfo.branch = experiments.getActiveExperimentBranch();
+      }
+    } catch(e) {
+      // If this is not Firefox, the import will fail.
+      return experimentInfo;
+    }
+
+    return experimentInfo;
+  },
+
+  /**
+   * Get the addon data in object form.
+   * @return Object containing the addon data.
+   *
+   * This should only be called from the environment collection task
+   * or _blockAddonManagerShutdown, otherwise we risk running this
+   * during addon manager shutdown.
+   */
+  _getAddons: Task.async(function* () {
+    // AddonManager may have shutdown already, in which case we should have cached addon data.
+    // It can't shutdown during the collection here because we have a blocker on the AMs
+    // shutdown barrier that waits for the collect task.
+    let addons = this._cachedAddons || {};
+    if (!this._cachedAddons) {
+      addons.activeAddons = yield this._getActiveAddons();
+      addons.activeTheme = yield this._getActiveTheme();
+      addons.activeGMPlugins = yield this._getActiveGMPlugins();
+    }
+
+    let personaId = null;
+#ifndef MOZ_WIDGET_GONK
+    let theme = LightweightThemeManager.currentTheme;
+    if (theme) {
+      personaId = theme.id;
+    }
+#endif
+
+    let addonData = {
+      activeAddons: addons.activeAddons,
+      theme: addons.activeTheme,
+      activePlugins: this._getActivePlugins(),
+      activeGMPlugins: addons.activeGMPlugins,
+      activeExperiment: this._getActiveExperiment(),
+      persona: personaId,
+    };
+
+    return addonData;
+  }),
+
+  /**
+   * Start watching the addons for changes.
+   */
+  _startWatchingAddons: function () {
+    // Define a listener to catch addons changes from the AddonManager. This part is
+    // tricky, as we only want to detect when the set of active addons changes without
+    // getting double notifications.
+    //
+    // We identified the following cases:
+    //
+    // * onEnabled:   Gets called when a restartless addon is enabled. Doesn't get called
+    //                if the restartless addon is installed and directly enabled.
+    // * onDisabled:  Gets called when disabling a restartless addon or can get called when
+    //                uninstalling a restartless addon from the UI (see bug 612168).
+    // * onInstalled: Gets called for all addon installs.
+    // * onUninstalling: Called the moment before addon uninstall happens.
+
+    this._addonsListener = {
+      onEnabled: addon => {
+        this._log.trace("_addonsListener - onEnabled " + addon.id);
+        this._onActiveAddonsChanged(addon)
+      },
+      onDisabled: addon => {
+        this._log.trace("_addonsListener - onDisabled " + addon.id);
+        this._onActiveAddonsChanged(addon);
+      },
+      onInstalled: addon => {
+        this._log.trace("_addonsListener - onInstalled " + addon.id +
+                        ", isActive: " + addon.isActive);
+        if (addon.isActive) {
+          this._onActiveAddonsChanged(addon);
+        }
+      },
+      onUninstalling: (addon, requiresRestart) => {
+        this._log.trace("_addonsListener - onUninstalling " + addon.id +
+                        ", isActive: " + addon.isActive +
+                        ", requiresRestart: " + requiresRestart);
+        if (!addon.isActive || requiresRestart) {
+          return;
+        }
+        this._onActiveAddonsChanged(addon);
+      },
+    };
+
+    AddonManager.addAddonListener(this._addonsListener);
+
+    // Watch for experiment changes as well.
+    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
+  },
+
+  /**
+   * Stop watching addons for changes.
+   */
+  _stopWatchingAddons: function () {
+    if (this._addonsListener) {
+      AddonManager.removeAddonListener(this._addonsListener);
+      Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
+    }
+    this._addonsListener = null;
+  },
+
+  /**
+   * Triggered when an addon changes its state.
+   * @param aAddon The addon which triggered the change.
+   */
+  _onActiveAddonsChanged: function (aAddon) {
+    const INTERESTING_ADDONS = [ "extension", "plugin", "service", "theme" ];
+
+    this._log.trace("_onActiveAddonsChanged - id " + aAddon.id + ", type " + aAddon.type);
+
+    if (INTERESTING_ADDONS.find(addon => addon == aAddon.type)) {
+      this._onEnvironmentChange("addons-changed");
+    }
+  },
+
+  /**
+   * Handle experiment change notifications.
+   */
+  observe: function (aSubject, aTopic, aData) {
+    this._log.trace("observe - Topic " + aTopic);
+
+    if (aTopic == EXPERIMENTS_CHANGED_TOPIC) {
+      this._onEnvironmentChange("experiment-changed");
+    }
+  },
+
+  /**
+   * Get the environment data in object form.
+   * @return Promise<Object> Resolved with the data on success, otherwise rejected.
+   */
+  getEnvironmentData: function() {
+    if (this._shutdown) {
+      this._log.error("getEnvironmentData - Already shut down");
+      return Promise.reject("Already shutdown");
+    }
+
+    this._log.trace("getEnvironmentData");
+    if (this._collectTask) {
+      return this._collectTask;
+    }
+
+    this._collectTask = this._doGetEnvironmentData();
+    let clear = () => this._collectTask = null;
+    this._collectTask.then(clear, clear);
+    return this._collectTask;
+  },
+
+  _doGetEnvironmentData: Task.async(function* () {
+    this._log.trace("getEnvironmentData");
+
+    // Define the data collection function for each section.
+    let sections = {
+      "build" : () => this._getBuild(),
+      "settings": () => this._getSettings(),
+#ifndef MOZ_WIDGET_ANDROID
+      "profile": () => this._getProfile(),
+#endif
+      "partner": () => this._getPartner(),
+      "system": () => this._getSystem(),
+      "addons": () => this._getAddons(),
+    };
+
+    let data = {};
+    // Recover from exceptions in the collection functions and populate the data
+    // object. We want to recover so that, in the worst-case, we only lose some data
+    // sections instead of all.
+    for (let s in sections) {
+      try {
+        data[s] = yield sections[s]();
+      } catch (e) {
+        this._log.error("_doGetEnvironmentData - There was an exception collecting " + s, e);
+      }
+    }
+
+    return data;
+  }),
+
   _onEnvironmentChange: function (what) {
     this._log.trace("_onEnvironmentChange for " + what);
     if (this._shutdown) {
       this._log.trace("_onEnvironmentChange - Already shut down.");
       return;
     }
 
     for (let [name, listener] of this._changeListeners) {
       try {
         this._log.debug("_onEnvironmentChange - calling " + name);
-        listener(what, this.currentEnvironment);
+        listener();
       } catch (e) {
-        this._log.error("_onEnvironmentChange - listener " + name + " caught error", e);
+        this._log.warning("_onEnvironmentChange - listener " + name + " caught error", e);
       }
     }
   },
+
+  /**
+   * This blocks the AddonManager shutdown barrier, it caches addons we might need later.
+   * It also lets an active collect task finish first as it may still access the AM.
+   */
+  _blockAddonManagerShutdown: Task.async(function*() {
+    this._log.trace("_blockAddonManagerShutdown");
+
+    this._stopWatchingAddons();
+
+    this._cachedAddons = {
+      activeAddons: yield this._getActiveAddons(),
+      activeTheme: yield this._getActiveTheme(),
+      activeGMPlugins: yield this._getActiveGMPlugins(),
+    };
+
+    yield this._collectTask;
+  }),
+
+  /**
+   * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+   */
+  _getState: function() {
+    return {
+      shutdown: this._shutdown,
+      hasCollectTask: !!this._collectTask,
+      hasAddonsListener: !!this._addonsListener,
+      hasCachedAddons: !!this._cachedAddons,
+    };
+  },
 };
--- a/toolkit/components/telemetry/TelemetryPing.jsm
+++ b/toolkit/components/telemetry/TelemetryPing.jsm
@@ -342,20 +342,26 @@ let Impl = {
       payload: aPayload,
     };
 
     if (aOptions.addClientId) {
       pingData.clientId = this._clientID;
     }
 
     if (aOptions.addEnvironment) {
-      pingData.environment = TelemetryEnvironment.currentEnvironment;
+      return TelemetryEnvironment.getEnvironmentData().then(environment => {
+        pingData.environment = environment;
+        return pingData;
+      },
+      error => {
+        this._log.error("assemblePing - Rejection", error);
+      });
     }
 
-    return pingData;
+    return Promise.resolve(pingData);
   },
 
   popPayloads: function popPayloads() {
     this._log.trace("popPayloads");
     function payloadIter() {
       let iterator = TelemetryFile.popPendingPings();
       for (let data of iterator) {
         yield data;
@@ -413,26 +419,29 @@ let Impl = {
    *                  environment data.
    *
    * @returns {Promise} A promise that resolves when the ping is sent.
    */
   send: function send(aType, aPayload, aOptions) {
     this._log.trace("send - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    let pingData = this.assemblePing(aType, aPayload, aOptions);
-    // Once ping is assembled, send it along with the persisted pings in the backlog.
-    let p = [
-      // Persist the ping if sending it fails.
-      this.doPing(pingData, false)
-          .catch(() => TelemetryFile.savePing(pingData, true)),
-      this.sendPersistedPings(),
-    ];
+    let promise = this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => {
+          // Once ping is assembled, send it along with the persisted ping in the backlog.
+          let p = [
+            // Persist the ping if sending it fails.
+            this.doPing(pingData, false)
+                .catch(() => TelemetryFile.savePing(pingData, true)),
+            this.sendPersistedPings(),
+          ];
+          return Promise.all(p);
+        },
+        error => this._log.error("send - Rejection", error));
 
-    let promise = Promise.all(p);
     this._trackPendingPingTask(promise);
     return promise;
   },
 
   /**
    * Send the persisted pings to the server.
    *
    * @return Promise A promise that is resolved when all pings finished sending or failed.
@@ -464,18 +473,19 @@ let Impl = {
    *                  environment data.
    *
    * @returns {Promise} A promise that resolves when all the pings are saved to disk.
    */
   savePendingPings: function savePendingPings(aType, aPayload, aOptions) {
     this._log.trace("savePendingPings - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    let pingData = this.assemblePing(aType, aPayload, aOptions);
-    return TelemetryFile.savePendingPings(pingData);
+    return this.assemblePing(aType, aPayload, aOptions)
+        .then(pingData => TelemetryFile.savePendingPings(pingData),
+              error => this._log.error("savePendingPings - Rejection", error));
   },
 
   /**
    * Save a ping to disk.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} aOptions Options object.
@@ -491,24 +501,26 @@ let Impl = {
    *
    * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
    *                    disk.
    */
   savePing: function savePing(aType, aPayload, aOptions) {
     this._log.trace("savePing - Type " + aType + ", Server " + this._server +
                     ", aOptions " + JSON.stringify(aOptions));
 
-    let pingData = this.assemblePing(aType, aPayload, aOptions);
-    if ("filePath" in aOptions) {
-      return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
-                          .then(() => { return pingData.id; });
-    } else {
-      return TelemetryFile.savePing(pingData, aOptions.overwrite)
-                          .then(() => { return pingData.id; });
-    }
+    return this.assemblePing(aType, aPayload, aOptions)
+      .then(pingData => {
+        if ("filePath" in aOptions) {
+          return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
+                              .then(() => { return pingData.id; });
+        } else {
+          return TelemetryFile.savePing(pingData, aOptions.overwrite)
+                              .then(() => { return pingData.id; });
+        }
+      }, error => this._log.error("savePing - Rejection", error));
   },
 
   onPingRequestFinished: function(success, startTime, ping, isPersisted) {
     this._log.trace("onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
 
     let hping = Telemetry.getHistogramById("TELEMETRY_PING");
     let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
 
@@ -752,16 +764,18 @@ let Impl = {
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
     // footprint and other numbers would be too optimistic.
     this._delayedInitTaskDeferred = Promise.defer();
     this._delayedInitTask = new DeferredTask(function* () {
       try {
         this._initialized = true;
 
+        yield TelemetryEnvironment.init();
+
         yield TelemetryFile.loadSavedPings();
         // If we have any TelemetryPings lying around, we'll be aggressive
         // and try to send them all off ASAP.
         if (TelemetryFile.pingsOverdue > 0) {
           this._log.trace("setupChromeProcess - Sending " + TelemetryFile.pingsOverdue +
                           " overdue pings now.");
           // It doesn't really matter what we pass to this.send as a reason,
           // since it's never sent to the server. All that this.send does with
@@ -805,16 +819,23 @@ let Impl = {
       return;
     }
 
     try {
       // First wait for clients processing shutdown.
       yield this._shutdownBarrier.wait();
       // Then wait for any outstanding async ping activity.
       yield this._connectionsBarrier.wait();
+
+      // Should down dependent components.
+      try {
+        yield TelemetryEnvironment.shutdown();
+      } catch (e) {
+        this._log.error("shutdown - environment shutdown failure", e);
+      }
     } finally {
       // Reset state.
       this._initialized = false;
       this._initStarted = false;
     }
   }),
 
   shutdown: function() {
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1512,31 +1512,31 @@ let Impl = {
 
         Telemetry.asyncFetchTelemetryData(function () {});
 
 #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
         // Check for a previously written aborted session ping.
         yield this._checkAbortedSessionPing();
 
         TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
-                                                    (reason, data) => this._onEnvironmentChange(reason, data));
+                                                    () => this._onEnvironmentChange());
         // Write the first aborted-session ping as early as possible. Just do that
         // if we are not testing, since calling Telemetry.reset() will make a previous
         // aborted ping a pending ping.
         if (!testing) {
           yield this._saveAbortedSessionPing();
         }
 
         // Start the scheduler.
         TelemetryScheduler.init();
 #endif
 
         this._delayedInitTaskDeferred.resolve();
       } catch (e) {
-        this._delayedInitTaskDeferred.reject(e);
+        this._delayedInitTaskDeferred.reject();
       } finally {
         this._delayedInitTask = null;
         this._delayedInitTaskDeferred = null;
       }
     }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
 
     this._delayedInitTask.arm();
     return this._delayedInitTaskDeferred.promise;
@@ -1978,18 +1978,18 @@ let Impl = {
     let filePath = OS.Path.join(dataDir, SESSION_STATE_FILE_NAME);
     try {
       yield CommonUtils.writeJSON(sessionData, filePath);
     } catch(e) {
       this._log.error("_saveSessionData - Failed to write session data to " + filePath, e);
     }
   }),
 
-  _onEnvironmentChange: function(reason, data) {
-    this._log.trace("_onEnvironmentChange", reason);
+  _onEnvironmentChange: function() {
+    this._log.trace("_onEnvironmentChange");
     let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
 
     let clonedPayload = Cu.cloneInto(payload, myScope);
     TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
 
     let options = {
       retentionDays: RETENTION_DAYS,
       addClientId: true,
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -31,21 +31,18 @@ Structure::
         telemetryEnabled: <bool>, // false on failure
         locale: <string>, // e.g. "it", null on failure
         update: {
           channel: <string>, // e.g. "release", null on failure
           enabled: <bool>, // true on failure
           autoDownload: <bool>, // true on failure
         },
         userPrefs: {
-          // Only prefs which are changed from the default value are listed
-          // in this block
-          "pref.name.value": value // some prefs send the value
-          "pref.name.url": "<user-set>" // For some privacy-sensitive prefs
-            // only the fact that the value has been changed is recorded
+          // Two possible behaviours: values of the whitelisted prefs, or for some prefs we
+          // only record they are present with value being set to null.
         },
       },
       profile: { // This section is not available on Android.
         creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
         resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
       },
       partner: {
         distributionId: <string>, // pref "distribution.id", null on failure
@@ -177,13 +174,8 @@ Structure::
         ],
         activeExperiment: { // section is empty if there's no active experiment
             id: <string>, // id
             branch: <string>, // branch name
         },
         persona: <string>, // id of the current persona, null on GONK
       },
     }
-
-Some parts of the environment must be fetched asynchronously at startup. If a session is very short or terminates early, the following items may be missing from the environment:
-
-- profile
-- addons
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -608,180 +608,228 @@ function isRejected(promise) {
     promise.then(() => resolve(false), () => resolve(true));
   });
 }
 
 add_task(function* asyncSetup() {
   yield spoofProfileReset();
 });
 
+add_task(function* test_initAndShutdown() {
+  // Check that init and shutdown work properly.
+  TelemetryEnvironment.init();
+  yield TelemetryEnvironment.shutdown();
+  TelemetryEnvironment.init();
+  yield TelemetryEnvironment.shutdown();
+
+  // A double init should be silently handled.
+  TelemetryEnvironment.init();
+  TelemetryEnvironment.init();
+
+  // getEnvironmentData should return a sane result.
+  let data = yield TelemetryEnvironment.getEnvironmentData();
+  Assert.ok(!!data);
+
+  // The change listener registration should silently fail after shutdown.
+  yield TelemetryEnvironment.shutdown();
+  TelemetryEnvironment.registerChangeListener("foo", () => {});
+  TelemetryEnvironment.unregisterChangeListener("foo");
+
+  // Shutting down again should be ignored.
+  yield TelemetryEnvironment.shutdown();
+
+  // Getting the environment data should reject after shutdown.
+  Assert.ok(yield isRejected(TelemetryEnvironment.getEnvironmentData()));
+});
+
+add_task(function* test_changeNotify() {
+  TelemetryEnvironment.init();
+
+  // Register some listeners
+  let results = new Array(4).fill(false);
+  for (let i=0; i<results.length; ++i) {
+    let k = i;
+    TelemetryEnvironment.registerChangeListener("test"+k, () => results[k] = true);
+  }
+  // Trigger environment change notifications.
+  // TODO: test with proper environment changes, not directly.
+  TelemetryEnvironment._onEnvironmentChange("foo");
+  Assert.ok(results.every(val => val), "All change listeners should have been notified.");
+  results.fill(false);
+  TelemetryEnvironment._onEnvironmentChange("bar");
+  Assert.ok(results.every(val => val), "All change listeners should have been notified.");
+
+  // Unregister listeners
+  for (let i=0; i<4; ++i) {
+    TelemetryEnvironment.unregisterChangeListener("test"+i);
+  }
+});
+
 add_task(function* test_checkEnvironment() {
-  let environmentData = yield TelemetryEnvironment.onInitialized();
+  yield TelemetryEnvironment.init();
+  let environmentData = yield TelemetryEnvironment.getEnvironmentData();
+
   checkEnvironmentData(environmentData);
+
+  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_prefWatchPolicies() {
   const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
   const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
   const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
-  const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
 
   const expectedValue = "some-test-value";
 
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST_1] = TelemetryEnvironment.RECORD_PREF_VALUE;
   prefsToWatch[PREF_TEST_2] = TelemetryEnvironment.RECORD_PREF_STATE;
   prefsToWatch[PREF_TEST_3] = TelemetryEnvironment.RECORD_PREF_STATE;
-  prefsToWatch[PREF_TEST_4] = TelemetryEnvironment.RECORD_PREF_VALUE;
 
-  Preferences.set(PREF_TEST_4, expectedValue);
+  yield TelemetryEnvironment.init();
 
   // Set the Environment preferences to watch.
   TelemetryEnvironment._watchPreferences(prefsToWatch);
   let deferred = PromiseUtils.defer();
-
-  // Check that the pref values are missing or present as expected
-  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined);
-  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], expectedValue);
-
   TelemetryEnvironment.registerChangeListener("testWatchPrefs", deferred.resolve);
 
   // Trigger a change in the watched preferences.
   Preferences.set(PREF_TEST_1, expectedValue);
   Preferences.set(PREF_TEST_2, false);
   yield deferred.promise;
 
   // Unregister the listener.
   TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");
 
   // Check environment contains the correct data.
-  let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;
+  let environmentData = yield TelemetryEnvironment.getEnvironmentData();
+
+  let userPrefs = environmentData.settings.userPrefs;
 
   Assert.equal(userPrefs[PREF_TEST_1], expectedValue,
                "Environment contains the correct preference value.");
-  Assert.equal(userPrefs[PREF_TEST_2], "<user-set>",
-               "Report that the pref was user set but the value is not shown.");
+  Assert.equal(userPrefs[PREF_TEST_2], null,
+               "Report that the pref was user set and has no value.");
   Assert.ok(!(PREF_TEST_3 in userPrefs),
             "Do not report if preference not user set.");
+
+  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_prefWatch_prefReset() {
   const PREF_TEST = "toolkit.telemetry.test.pref1";
 
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_STATE;
   // Set the preference to a non-default value.
   Preferences.set(PREF_TEST, false);
 
+  yield TelemetryEnvironment.init();
+
   // Set the Environment preferences to watch.
   TelemetryEnvironment._watchPreferences(prefsToWatch);
   let deferred = PromiseUtils.defer();
   TelemetryEnvironment.registerChangeListener("testWatchPrefs_reset", deferred.resolve);
 
-  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "<user-set>");
-
   // Trigger a change in the watched preferences.
   Preferences.reset(PREF_TEST);
   yield deferred.promise;
 
-  Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], undefined);
-
   // Unregister the listener.
   TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset");
+  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_addonsWatch_InterestingChange() {
   const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
   const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
   // We only expect a single notification for each install, uninstall, enable, disable.
   const EXPECTED_NOTIFICATIONS = 4;
 
+  yield TelemetryEnvironment.init();
   let deferred = PromiseUtils.defer();
   let receivedNotifications = 0;
 
   let registerCheckpointPromise = (aExpected) => {
     return new Promise(resolve => TelemetryEnvironment.registerChangeListener(
-      "testWatchAddons_Changes" + aExpected, (reason, data) => {
-        Assert.equal(reason, "addons-changed");
+      "testWatchAddons_Changes" + aExpected, () => {
         receivedNotifications++;
         resolve();
       }));
   };
 
   let assertCheckpoint = (aExpected) => {
     Assert.equal(receivedNotifications, aExpected);
     TelemetryEnvironment.unregisterChangeListener("testWatchAddons_Changes" + aExpected);
   };
 
   // Test for receiving one notification after each change.
   let checkpointPromise = registerCheckpointPromise(1);
   yield AddonTestUtils.installXPIFromURL(ADDON_INSTALL_URL);
   yield checkpointPromise;
   assertCheckpoint(1);
-  Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons);
-
+  
   checkpointPromise = registerCheckpointPromise(2);
   let addon = yield AddonTestUtils.getAddonById(ADDON_ID);
   addon.userDisabled = true;
   yield checkpointPromise;
   assertCheckpoint(2);
-  Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
 
   checkpointPromise = registerCheckpointPromise(3);
   addon.userDisabled = false;
   yield checkpointPromise;
   assertCheckpoint(3);
-  Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons);
 
   checkpointPromise = registerCheckpointPromise(4);
   yield AddonTestUtils.uninstallAddonByID(ADDON_ID);
   yield checkpointPromise;
   assertCheckpoint(4);
-  Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
+
+  yield TelemetryEnvironment.shutdown();
 
   Assert.equal(receivedNotifications, EXPECTED_NOTIFICATIONS,
                "We must only receive the notifications we expect.");
 });
 
 add_task(function* test_pluginsWatch_Add() {
   if (gIsAndroid) {
     Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
     return;
   }
 
-  Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 1);
+  yield TelemetryEnvironment.init();
 
   let newPlugin = new PluginTag(PLUGIN2_NAME, PLUGIN2_DESC, PLUGIN2_VERSION, true);
   gInstalledPlugins.push(newPlugin);
 
   let deferred = PromiseUtils.defer();
   let receivedNotifications = 0;
-  let callback = (reason, data) => {
+  let callback = () => {
     receivedNotifications++;
-    Assert.equal(reason, "addons-changed");
     deferred.resolve();
   };
   TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback);
 
   Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null);
   yield deferred.promise;
 
-  Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 2);
-
   TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add");
+  yield TelemetryEnvironment.shutdown();
 
   Assert.equal(receivedNotifications, 1, "We must only receive one notification.");
 });
 
 add_task(function* test_pluginsWatch_Remove() {
   if (gIsAndroid) {
     Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
     return;
   }
 
+  yield TelemetryEnvironment.init();
+
   // Find the test plugin.
   let plugin = gInstalledPlugins.find(plugin => (plugin.name == PLUGIN2_NAME));
   Assert.ok(plugin, "The test plugin must exist.");
 
   // Remove it from the PluginHost.
   gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin);
 
   let deferred = PromiseUtils.defer();
@@ -791,43 +839,38 @@ add_task(function* test_pluginsWatch_Rem
     deferred.resolve();
   };
   TelemetryEnvironment.registerChangeListener("testWatchPlugins_Remove", callback);
 
   Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null);
   yield deferred.promise;
 
   TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove");
+  yield TelemetryEnvironment.shutdown();
 
   Assert.equal(receivedNotifications, 1, "We must only receive one notification.");
 });
 
 add_task(function* test_addonsWatch_NotInterestingChange() {
   // We are not interested to dictionary addons changes.
   const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi";
   const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+  yield TelemetryEnvironment.init();
 
-  let receivedNotification = false;
-  let deferred = PromiseUtils.defer();
+  let receivedNotifications = 0;
   TelemetryEnvironment.registerChangeListener("testNotInteresting",
-    () => {
-      Assert.ok(!receivedNotification, "Should not receive multiple notifications");
-      receivedNotification = true;
-      deferred.resolve();
-    });
+                                              () => receivedNotifications++);
 
   yield AddonTestUtils.installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL);
   yield AddonTestUtils.installXPIFromURL(INTERESTING_ADDON_INSTALL_URL);
 
-  yield deferred.promise;
-  Assert.ok(!("telemetry-dictionary@tests.mozilla.org" in
-              TelemetryEnvironment.currentEnvironment.addons.activeAddons),
-            "Dictionaries should not appear in active addons.");
+  Assert.equal(receivedNotifications, 1, "We must receive only one notification.");
 
   TelemetryEnvironment.unregisterChangeListener("testNotInteresting");
+  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function* test_addonsAndPlugins() {
   const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
   const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
   const ADDON_INSTALL_DATE = truncateToDays(Date.now());
   const EXPECTED_ADDON_DATA = {
     blocklisted: false,
@@ -848,20 +891,22 @@ add_task(function* test_addonsAndPlugins
     name: FLASH_PLUGIN_NAME,
     version: FLASH_PLUGIN_VERSION,
     description: FLASH_PLUGIN_DESC,
     blocklisted: false,
     disabled: false,
     clicktoplay: true,
   };
 
+  yield TelemetryEnvironment.init();
+
   // Install an addon so we have some data.
   yield AddonTestUtils.installXPIFromURL(ADDON_INSTALL_URL);
 
-  let data = TelemetryEnvironment.currentEnvironment;
+  let data = yield TelemetryEnvironment.getEnvironmentData();
   checkEnvironmentData(data);
 
   // Check addon data.
   Assert.ok(ADDON_ID in data.addons.activeAddons, "We must have one active addon.");
   let targetAddon = data.addons.activeAddons[ADDON_ID];
   for (let f in EXPECTED_ADDON_DATA) {
     Assert.equal(targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value.");
   }
@@ -881,13 +926,15 @@ add_task(function* test_addonsAndPlugins
 
   // Check plugin mime types.
   Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1));
   Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2));
   Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There."));
 
   let personaId = (gIsGonk) ? null : PERSONA_ID;
   Assert.equal(data.addons.persona, personaId, "The correct Persona Id must be reported.");
+
+  yield TelemetryEnvironment.shutdown();
 });
 
 add_task(function*() {
   do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -1230,30 +1230,28 @@ add_task(function* test_savedSessionData
 
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
   let prefsToWatch = {};
   prefsToWatch[PREF_TEST] = TelemetryEnvironment.RECORD_PREF_VALUE;
 
   // We expect one new subsession when starting TelemetrySession and one after triggering
   // an environment change.
-  const expectedSubsessions = sessionState.profileSubsessionCounter + 3;
+  const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
   const expectedUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
   fakeGenerateUUID(generateUUID, () => expectedUUID);
 
   if (gIsAndroid) {
     // We don't support subsessions yet on Android, so skip the next checks.
     return;
   }
 
   // Start TelemetrySession so that it loads the session data file.
   yield TelemetrySession.reset();
   // Watch a test preference, trigger and environment change and wait for it to propagate.
-
-  // _watchPreferences triggers a subsession notification
   TelemetryEnvironment._watchPreferences(prefsToWatch);
   let changePromise = new Promise(resolve =>
     TelemetryEnvironment.registerChangeListener("test_fake_change", resolve));
   Preferences.set(PREF_TEST, 1);
   yield changePromise;
   TelemetryEnvironment.unregisterChangeListener("test_fake_change");
 
   let payload = TelemetrySession.getPayload();