Bug 1122480 - Fx40 - Update about:telemetry to the unification changes. r=rvitillo,dexter a=sledru
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Wed, 13 May 2015 21:57:05 +0200
changeset 275115 0f8d873a4e5e1aa31e2594cf5fc0c35f1e5d0195
parent 275114 a7d5b64df128b3e0c1f961a10f2082a1bc00f290
child 275116 ccf0bdbac4eb9f0ecbbf89064c62e0f12df446ee
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrvitillo, dexter, sledru
bugs1122480
milestone40.0a2
Bug 1122480 - Fx40 - Update about:telemetry to the unification changes. r=rvitillo,dexter a=sledru
toolkit/components/telemetry/TelemetryArchive.jsm
toolkit/components/telemetry/TelemetryController.jsm
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/tests/unit/test_PingAPI.js
toolkit/components/telemetry/tests/unit/xpcshell.ini
toolkit/content/aboutTelemetry.css
toolkit/content/aboutTelemetry.js
toolkit/content/aboutTelemetry.xhtml
toolkit/content/jar.mn
--- a/toolkit/components/telemetry/TelemetryArchive.jsm
+++ b/toolkit/components/telemetry/TelemetryArchive.jsm
@@ -150,17 +150,16 @@ let TelemetryArchiveImpl = {
     if (this._scannedArchiveDirectory) {
       return Promise.resolve(this._buildArchivedPingList())
     }
 
     return TelemetryStorage.loadArchivedPingList().then((loadedInfo) => {
       // Add the ping info from scanning to the existing info.
       // We might have pings added before lazily loading this list.
       for (let [id, info] of loadedInfo) {
-        this._log.trace("promiseArchivedPingList - id: " + id + ", info: " + info);
         this._archivedPings.set(id, {
           timestampCreated: info.timestampCreated,
           type: info.type,
         });
       }
 
       this._scannedArchiveDirectory = true;
       return this._buildArchivedPingList();
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -39,17 +39,16 @@ const PREF_FHR_UPLOAD_ENABLED = "datarep
 const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
 const PREF_UNIFIED = PREF_BRANCH + "unified";
 
 // Whether the FHR/Telemetry unification features are enabled.
 // Changing this pref requires a restart.
 const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
 
 const PING_FORMAT_VERSION = 4;
-const PING_TYPE_MAIN = "main";
 
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 // The number of days to keep pings serialised on the disk in case of failures.
 const DEFAULT_RETENTION_DAYS = 14;
 // Timeout after which we consider a ping submission failed.
@@ -58,16 +57,23 @@ const PING_SUBMIT_TIMEOUT_MS = 2 * 60 * 
 // We treat pings before midnight as happening "at midnight" with this tolerance.
 const MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
 // For midnight fuzzing we want to affect pings around midnight with this tolerance.
 const MIDNIGHT_TOLERANCE_FUZZ_MS = 5 * 60 * 1000;
 // We try to spread "midnight" pings out over this interval.
 const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * 60 * 1000;
 const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS;
 
+// Ping types.
+const PING_TYPE_MAIN = "main";
+
+// Session ping reasons.
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
+
 XPCOMUtils.defineLazyModuleGetter(this, "ClientID",
                                   "resource://gre/modules/ClientID.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                    "@mozilla.org/base/telemetry;1",
                                    "nsITelemetry");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage",
@@ -77,16 +83,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
                                   "resource://gre/modules/TelemetryEnvironment.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionRecorder",
                                   "resource://gre/modules/SessionRecorder.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive",
                                   "resource://gre/modules/TelemetryArchive.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySession",
+                                  "resource://gre/modules/TelemetrySession.jsm");
 
 /**
  * Setup Telemetry logging. This function also gets called when loggin related
  * preferences change.
  */
 let gLogger = null;
 let gLogAppenderDump = null;
 function configureLogging() {
@@ -208,16 +216,26 @@ this.TelemetryController = Object.freeze
   submitExternalPing: function(aType, aPayload, aOptions = {}) {
     aOptions.addClientId = aOptions.addClientId || false;
     aOptions.addEnvironment = aOptions.addEnvironment || false;
 
     return Impl.submitExternalPing(aType, aPayload, aOptions);
   },
 
   /**
+   * Get the current session ping data as it would be sent out or stored.
+   *
+   * @param {bool} aSubsession Whether to get subsession data. Optional, defaults to false.
+   * @return {object} The current ping data in object form.
+   */
+  getCurrentPingData: function(aSubsession = false) {
+    return Impl.getCurrentPingData(aSubsession);
+  },
+
+  /**
    * Add the ping to the pending ping list and save all pending pings.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} [aOptions] Options object.
    * @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
    *                 if sending fails.
    * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
@@ -1184,9 +1202,21 @@ let Impl = {
   /**
    * Allows waiting for TelemetryControllers delayed initialization to complete.
    * This will complete before TelemetryController is shutting down.
    * @return {Promise} Resolved when delayed TelemetryController initialization completed.
    */
   promiseInitialized: function() {
     return this._delayedInitTaskDeferred.promise;
   },
+
+  getCurrentPingData: function(aSubsession) {
+    this._log.trace("getCurrentPingData - subsession: " + aSubsession)
+
+    const reason = aSubsession ? REASON_GATHER_SUBSESSION_PAYLOAD : REASON_GATHER_PAYLOAD;
+    const type = PING_TYPE_MAIN;
+    const payload = TelemetrySession.getPayload(reason);
+    const options = { addClientId: true, addEnvironment: true };
+    const ping = this.assemblePing(type, payload, options);
+
+    return ping;
+  },
 };
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -36,16 +36,17 @@ const PAYLOAD_VERSION = 4;
 const PING_TYPE_MAIN = "main";
 const PING_TYPE_SAVED_SESSION = "saved-session";
 const RETENTION_DAYS = 14;
 
 const REASON_ABORTED_SESSION = "aborted-session";
 const REASON_DAILY = "daily";
 const REASON_SAVED_SESSION = "saved-session";
 const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
 const REASON_TEST_PING = "test-ping";
 const REASON_ENVIRONMENT_CHANGE = "environment-change";
 const REASON_SHUTDOWN = "shutdown";
 
 const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange";
 
 const MS_IN_ONE_HOUR  = 60 * 60 * 1000;
 const MIN_SUBSESSION_LENGTH_MS = 10 * 60 * 1000;
--- a/toolkit/components/telemetry/tests/unit/test_PingAPI.js
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -2,29 +2,33 @@
    http://creativecommons.org/publicdomain/zero/1.0/
 */
 
 // This tests the public Telemetry API for submitting pings.
 
 "use strict";
 
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 
 XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
   return OS.Path.join(OS.Constants.Path.profileDir, "datareporting", "archived");
 });
 
+const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
 function run_test() {
   do_get_profile(true);
+  Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   run_next_test();
 }
 
 add_task(function* test_archivedPings() {
   // TelemetryController should not be fully initialized at this point.
   // Submitting pings should still work fine.
 
   const PINGS = [
@@ -160,8 +164,37 @@ add_task(function* test_clientId() {
   let promiseSetup = TelemetryController.reset();
   id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true});
   ping = yield TelemetryArchive.promiseArchivedPingById(id);
   Assert.equal(ping.clientId, clientId);
 
   // Finish setup.
   yield promiseSetup;
 });
+
+add_task(function* test_currentPingData() {
+  yield TelemetrySession.setup();
+
+  // Setup test data.
+  let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+  h.clear();
+  h.add(1);
+  let k = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+  k.clear();
+  k.add("a", 1);
+
+  // Get current ping data objects and check that their data is sane.
+  for (let subsession of [true, false]) {
+    let ping = TelemetryController.getCurrentPingData(subsession);
+
+    Assert.ok(!!ping, "Should have gotten a ping.");
+    Assert.equal(ping.type, "main", "Ping should have correct type.");
+    const expectedReason = subsession ? "gather-subsession-payload" : "gather-payload";
+    Assert.equal(ping.payload.info.reason, expectedReason, "Ping should have the correct reason.");
+
+    let id = "TELEMETRY_TEST_RELEASE_OPTOUT";
+    Assert.ok(id in ping.payload.histograms, "Payload should have test count histogram.");
+    Assert.equal(ping.payload.histograms[id].sum, 1, "Test count value should match.");
+    id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT";
+    Assert.ok(id in ping.payload.keyedHistograms, "Payload should have keyed test histogram.");
+    Assert.equal(ping.payload.keyedHistograms[id]["a"].sum, 1, "Keyed test value should match.");
+  }
+});
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -20,16 +20,17 @@ generated-files =
   theme.xpi
 
 [test_nsITelemetry.js]
 [test_SubsessionChaining.js]
 [test_TelemetryEnvironment.js]
 # Bug 1144395: crash on Android 4.3
 skip-if = android_version == "18"
 [test_PingAPI.js]
+skip-if = os == "android"
 [test_TelemetryFlagClear.js]
 [test_TelemetryLateWrites.js]
 [test_TelemetryLockCount.js]
 [test_TelemetryLog.js]
 [test_TelemetryController.js]
 # Bug 676989: test fails consistently on Android
 # fail-if = os == "android"
 # Bug 1144395: crash on Android 4.3
--- a/toolkit/content/aboutTelemetry.css
+++ b/toolkit/content/aboutTelemetry.css
@@ -22,24 +22,45 @@ h2 {
 }
 
 #page-description {
   border: 1px solid threedshadow;
   margin: 0px;
   padding: 10px;
 }
 
-#description-enabled > span {
+#settings {
+  border: 1px solid lightgrey;
+  padding: 5px;
+}
+
+.description-enabled,
+.description-disabled {
+  margin: 0px;
+}
+
+.description-enabled > span {
   color: green;
 }
 
-#description-disabled > span {
+.description-disabled > span {
   color: red;
 }
 
+#ping-picker {
+  margin-top: 10px;
+  border: 1px solid lightgrey;
+  padding: 5px;
+}
+
+#ping-source-picker {
+  margin-left: 5px;
+  margin-bottom: 10px;
+}
+
 .data-section {
   background-color: -moz-Field;
   color: -moz-FieldText;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
   margin: 0px;
   padding: 10px;
 }
--- a/toolkit/content/aboutTelemetry.js
+++ b/toolkit/content/aboutTelemetry.js
@@ -7,139 +7,469 @@
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm");
 Cu.import("resource://gre/modules/TelemetrySession.jsm");
+Cu.import("resource://gre/modules/TelemetryArchive.jsm");
+Cu.import("resource://gre/modules/TelemetryUtils.jsm");
 Cu.import("resource://gre/modules/TelemetryLog.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
 
 const Telemetry = Services.telemetry;
 const bundle = Services.strings.createBundle(
   "chrome://global/locale/aboutTelemetry.properties");
 const brandBundle = Services.strings.createBundle(
   "chrome://branding/locale/brand.properties");
 
 // Maximum height of a histogram bar (in em for html, in chars for text)
 const MAX_BAR_HEIGHT = 18;
 const MAX_BAR_CHARS = 25;
 const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
 const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
 const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
 const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
 const DEFAULT_SYMBOL_SERVER_URI = "http://symbolapi.mozilla.org";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 
 // ms idle before applying the filter (allow uninterrupted typing)
 const FILTER_IDLE_TIMEOUT = 500;
 
-#ifdef XP_WIN
-const EOL = "\r\n";
-#else
-const EOL = "\n";
-#endif
+const isWindows = (Services.appinfo.OS == "WINNT");
+const EOL = isWindows ? "\r\n" : "\n";
+
+// This is the ping object currently displayed in the page.
+let gPingData = null;
 
 // Cached value of document's RTL mode
 let documentRTLMode = "";
 
 /**
- * Helper function for fetching a config pref
- *
- * @param aPrefName Name of config pref to fetch.
- * @param aDefault Default value to return if pref isn't set.
- * @return Value of pref
- */
-function getPref(aPrefName, aDefault) {
-  let result = aDefault;
-
-  try {
-    let prefType = Services.prefs.getPrefType(aPrefName);
-    if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
-      result = Services.prefs.getBoolPref(aPrefName);
-    } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
-      result = Services.prefs.getCharPref(aPrefName);
-    }
-  } catch (e) {
-    // Return default if Prefs service throws exception
-  }
-
-  return result;
-}
-
-/**
  * Helper function for determining whether the document direction is RTL.
  * Caches result of check on first invocation.
  */
 function isRTL() {
   if (!documentRTLMode)
     documentRTLMode = window.getComputedStyle(document.body).direction;
   return (documentRTLMode == "rtl");
 }
 
-let observer = {
+function isArray(arg) {
+  return Object.prototype.toString.call(arg) === '[object Array]';
+}
+
+function isFlatArray(obj) {
+  if (!isArray(obj)) {
+    return false;
+  }
+  return !obj.some(e => typeof(e) == "object");
+}
+
+/**
+ * This is a helper function for explodeObject.
+ */
+function flattenObject(obj, map, path, array) {
+  for (let k of Object.keys(obj)) {
+    let newPath = [...path, array ? "[" + k + "]" : k];
+    let v = obj[k];
+    if (!v || (typeof(v) != "object")) {
+      map.set(newPath.join("."), v);
+    } else if (isFlatArray(v)) {
+      map.set(newPath.join("."), "[" + v.join(", ") + "]");
+    } else {
+      flattenObject(v, map, newPath, isArray(v));
+    }
+  }
+}
+
+/**
+ * This turns a JSON object into a "flat" stringified form.
+ *
+ * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the
+ * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]).
+ */
+function explodeObject(obj) {
+  let map = new Map();
+  flattenObject(obj, map, []);
+  return map;
+}
 
-  enableTelemetry: bundle.GetStringFromName("enableTelemetry"),
+function filterObject(obj, filterOut) {
+  let ret = {};
+  for (let k of Object.keys(obj)) {
+    if (filterOut.indexOf(k) == -1) {
+      ret[k] = obj[k];
+    }
+  }
+  return ret;
+}
+
 
-  disableTelemetry: bundle.GetStringFromName("disableTelemetry"),
+/**
+ * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
+ *
+ * For an object like:
+ *   {
+ *     a: {b: "1"},
+ *     c: {d: "2", e: {f: "3"}}
+ *   }
+ * it returns a Map of the form:
+ *   Map([
+ *     ["a", Map(["b","1"])],
+ *     ["c", Map([["d", "2"], ["e.f", "3"]])]
+ *   ])
+ */
+function sectionalizeObject(obj) {
+  let map = new Map();
+  for (let k of Object.keys(obj)) {
+    map.set(k, explodeObject(obj[k]));
+  }
+  return map;
+}
+
+/**
+ * Obtain the main DOMWindow for the current context.
+ */
+function getMainWindow() {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIWebNavigation)
+               .QueryInterface(Ci.nsIDocShellTreeItem)
+               .rootTreeItem
+               .QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindow);
+}
 
-  /**
-   * Observer is called whenever Telemetry is enabled or disabled
-   */
-  observe: function observe(aSubject, aTopic, aData) {
-    if (aData == PREF_TELEMETRY_ENABLED) {
-      this.updatePrefStatus();
+/**
+ * Obtain the DOMWindow that can open a preferences pane.
+ *
+ * This is essentially "get the browser chrome window" with the added check
+ * that the supposed browser chrome window is capable of opening a preferences
+ * pane.
+ *
+ * This may return null if we can't find the browser chrome window.
+ */
+function getMainWindowWithPreferencesPane() {
+  let mainWindow = getMainWindow();
+  if (mainWindow && "openAdvancedPreferences" in mainWindow) {
+    return mainWindow;
+  } else {
+    return null;
+  }
+}
+
+/**
+ * Remove all child nodes of a document node.
+ */
+function removeAllChildNodes(node) {
+  while (node.hasChildNodes()) {
+    node.removeChild(node.lastChild);
+  }
+}
+
+/**
+ * Pad a number to two digits with leading "0".
+ */
+function padToTwoDigits(n) {
+  return (n > 9) ? n: "0" + n;
+}
+
+/**
+ * Return yesterdays date with the same time.
+ */
+function yesterday(date) {
+  let d = new Date(date);
+  d.setDate(d.getDate() - 1);
+  return d;
+}
+
+/**
+ * This returns a short date string of the form YYYY/MM/DD.
+ */
+function shortDateString(date) {
+  return date.getFullYear()
+         + "/" + padToTwoDigits(date.getMonth() + 1)
+         + "/" + padToTwoDigits(date.getDate());
+}
+
+/**
+ * This returns a short time string of the form hh:mm:ss.
+ */
+function shortTimeString(date) {
+  return padToTwoDigits(date.getHours())
+         + ":" + padToTwoDigits(date.getMinutes())
+         + ":" + padToTwoDigits(date.getSeconds());
+}
+
+let Settings = {
+  SETTINGS: [
+    // data upload
+    {
+      pref: PREF_FHR_UPLOAD_ENABLED,
+      defaultPrefValue: false,
+      descriptionEnabledId: "description-upload-enabled",
+      descriptionDisabledId: "description-upload-disabled",
+    },
+    // extended "Telemetry" recording
+    {
+      pref: PREF_TELEMETRY_ENABLED,
+      defaultPrefValue: false,
+      descriptionEnabledId: "description-extended-recording-enabled",
+      descriptionDisabledId: "description-extended-recording-disabled",
+    },
+  ],
+
+  attachObservers: function() {
+    for (let s of this.SETTINGS) {
+      let setting = s;
+      Preferences.observe(setting.pref, this.render, this);
+    }
+
+    let elements = document.getElementsByClassName("change-data-choices-link");
+    for (let el of elements) {
+      el.addEventListener("click", function() {
+        let mainWindow = getMainWindowWithPreferencesPane();
+        mainWindow.openAdvancedPreferences("dataChoicesTab");
+      }, false);
+    }
+  },
+
+  detachObservers: function() {
+    for (let setting of this.SETTINGS) {
+      Preferences.ignore(setting.pref, this.render, this);
     }
   },
 
   /**
    * Updates the button & text at the top of the page to reflect Telemetry state.
    */
-  updatePrefStatus: function updatePrefStatus() {
-    // Notify user whether Telemetry is enabled
-    let enabledElement = document.getElementById("description-enabled");
-    let disabledElement = document.getElementById("description-disabled");
-    let toggleElement = document.getElementById("toggle-telemetry");
-    if (getPref(PREF_TELEMETRY_ENABLED, false)) {
-      enabledElement.classList.remove("hidden");
-      disabledElement.classList.add("hidden");
-      toggleElement.innerHTML = this.disableTelemetry;
-    } else {
-      enabledElement.classList.add("hidden");
-      disabledElement.classList.remove("hidden");
-      toggleElement.innerHTML = this.enableTelemetry;
+  render: function() {
+    for (let setting of this.SETTINGS) {
+      let enabledElement = document.getElementById(setting.descriptionEnabledId);
+      let disabledElement = document.getElementById(setting.descriptionDisabledId);
+
+      if (Preferences.get(setting.pref, setting.defaultPrefValue)) {
+        enabledElement.classList.remove("hidden");
+        disabledElement.classList.add("hidden");
+      } else {
+        enabledElement.classList.add("hidden");
+        disabledElement.classList.remove("hidden");
+      }
     }
   }
 };
 
+let PingPicker = {
+  viewCurrentPingData: true,
+  _archivedPings: null,
+
+  attachObservers: function() {
+    let elements = document.getElementsByName("choose-ping-source");
+    for (let el of elements) {
+      el.addEventListener("change", () => this.onPingSourceChanged(), false);
+    }
+
+    document.getElementById("show-subsession-data").addEventListener("change", () => {
+      this._updateCurrentPingData();
+    });
+
+    document.getElementById("choose-ping-week").addEventListener("change", () => {
+      this._renderPingList();
+      this._updateArchivedPingData();
+    }, false);
+    document.getElementById("choose-ping-id").addEventListener("change", () => {
+      this._updateArchivedPingData()
+    }, false);
+
+    document.getElementById("next-ping")
+            .addEventListener("click", () => this._movePingIndex(-1), false);
+    document.getElementById("previous-ping")
+            .addEventListener("click", () => this._movePingIndex(1), false);
+  },
+
+  onPingSourceChanged: function() {
+    this.update();
+  },
+
+  update: function() {
+    let el = document.getElementById("ping-source-current");
+    this.viewCurrentPingData = el.checked;
+
+    if (this.viewCurrentPingData) {
+      document.getElementById("current-ping-picker").classList.remove("hidden");
+      document.getElementById("archived-ping-picker").classList.add("hidden");
+      this._updateCurrentPingData();
+    } else {
+      document.getElementById("current-ping-picker").classList.add("hidden");
+      this._updateArchivedPingList().then(() =>
+        document.getElementById("archived-ping-picker").classList.remove("hidden"));
+    }
+  },
+
+  _updateCurrentPingData: function() {
+    const subsession = document.getElementById("show-subsession-data").checked;
+    const ping = TelemetryController.getCurrentPingData(subsession);
+    displayPingData(ping);
+  },
+
+  _updateArchivedPingData: function() {
+    let id = this._getSelectedPingId();
+    TelemetryArchive.promiseArchivedPingById(id)
+                    .then((ping) => displayPingData(ping));
+  },
+
+  _updateArchivedPingList: function() {
+    return TelemetryArchive.promiseArchivedPingList().then((pingList) => {
+      // The archived ping list is sorted in ascending timestamp order,
+      // but descending is more practical for the operations we do here.
+      pingList.reverse();
+
+      // Currently about:telemetry can only handle the Telemetry session pings,
+      // so we have to filter out everything else.
+      pingList = pingList.filter(
+        (p) => ["main", "saved-session"].indexOf(p.type) != -1);
+      this._archivedPings = pingList;
+
+      // Collect the start dates for all the weeks we have pings for.
+      let weekStart = (date) => {
+        let weekDay = (date.getDay() + 6) % 7;
+        let monday = new Date(date);
+        monday.setDate(date.getDate() - weekDay);
+        return TelemetryUtils.truncateToDays(monday);
+      };
+
+      let weekStartDates = new Set();
+      for (let p of pingList) {
+        weekStartDates.add(weekStart(new Date(p.timestampCreated)).getTime());
+      }
+
+      // Build a list of the week date ranges we have ping data for.
+      let plusOneWeek = (date) => {
+        let d = date;
+        d.setDate(d.getDate() + 7);
+        return d;
+      };
+
+      this._weeks = [for (startTime of weekStartDates.values()) {
+        startDate: new Date(startTime),
+        endDate: plusOneWeek(new Date(startTime)),
+      }];
+
+      // Render the archive data.
+      this._renderWeeks();
+      this._renderPingList();
+
+      // Update the displayed ping.
+      this._updateArchivedPingData();
+    });
+  },
+
+  _renderWeeks: function() {
+    let weekSelector = document.getElementById("choose-ping-week");
+    removeAllChildNodes(weekSelector);
+
+    let index = 0;
+    for (let week of this._weeks) {
+      let text = shortDateString(week.startDate)
+                 + " - " + shortDateString(yesterday(week.endDate));
+
+      let option = document.createElement("option");
+      let content = document.createTextNode(text);
+      option.appendChild(content);
+      weekSelector.appendChild(option);
+    }
+  },
+
+  _getSelectedWeek: function() {
+    let weekSelector = document.getElementById("choose-ping-week");
+    return this._weeks[weekSelector.selectedIndex];
+  },
+
+  _renderPingList: function(id = null) {
+    let pingSelector = document.getElementById("choose-ping-id");
+    removeAllChildNodes(pingSelector);
+
+    let weekRange = this._getSelectedWeek();
+    let pings = this._archivedPings.filter(
+      (p) => p.timestampCreated >= weekRange.startDate.getTime() &&
+             p.timestampCreated < weekRange.endDate.getTime());
+
+    for (let p of pings) {
+      let date = new Date(p.timestampCreated);
+      let text = shortDateString(date)
+                 + " " + shortTimeString(date)
+                 + " - " + p.type;
+
+      let option = document.createElement("option");
+      let content = document.createTextNode(text);
+      option.appendChild(content);
+      option.setAttribute("value", p.id);
+      if (id && p.id == id) {
+        option.selected = true;
+      }
+      pingSelector.appendChild(option);
+    }
+  },
+
+  _getSelectedPingId: function() {
+    let pingSelector = document.getElementById("choose-ping-id");
+    let selected = pingSelector.selectedOptions.item(0);
+    return selected.getAttribute("value");
+  },
+
+  _movePingIndex: function(offset) {
+    const id = this._getSelectedPingId();
+    const index = this._archivedPings.findIndex((p) => p.id == id);
+    const newIndex = Math.min(Math.max(index + offset, 0), this._archivedPings.length - 1);
+    const ping = this._archivedPings[newIndex];
+
+    const weekIndex = this._weeks.findIndex(
+      (week) => ping.timestampCreated >= week.startDate.getTime() &&
+                ping.timestampCreated < week.endDate.getTime());
+    const options = document.getElementById("choose-ping-week").options;
+    options.item(weekIndex).selected = true;
+
+    this._renderPingList(ping.id);
+    this._updateArchivedPingData();
+  },
+};
+
 let GeneralData = {
   /**
    * Renders the general data
    */
-  render: function() {
+  render: function(aPing) {
     setHasData("general-data-section", true);
-
     let table = document.createElement("table");
 
     let caption = document.createElement("caption");
     let captionString = bundle.GetStringFromName("generalDataTitle");
     caption.appendChild(document.createTextNode(captionString + "\n"));
     table.appendChild(caption);
 
     let headings = document.createElement("tr");
     this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingName") + "\t");
     this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingValue") + "\t");
     table.appendChild(headings);
 
-    let row = document.createElement("tr");
-    this.appendColumn(row, "td", "Client ID\t");
-    this.appendColumn(row, "td", TelemetryController.clientID + "\t");
-    table.appendChild(row);
+    // The payload & environment parts are handled by other renderers.
+    let ignoreSections = ["payload", "environment"];
+    let data = explodeObject(filterObject(aPing, ignoreSections));
+
+    for (let [path, value] of data) {
+        let row = document.createElement("tr");
+        this.appendColumn(row, "td", path + "\t");
+        this.appendColumn(row, "td", value + "\t");
+        table.appendChild(row);
+    }
 
     let dataDiv = document.getElementById("general-data");
+    removeAllChildNodes(dataDiv);
     dataDiv.appendChild(table);
   },
 
   /**
    * Helper function for appending a column to the data table.
    *
    * @param aRowElement Parent row element
    * @param aColType Column's tag name
@@ -148,22 +478,69 @@ let GeneralData = {
   appendColumn: function(aRowElement, aColType, aColText) {
     let colElement = document.createElement(aColType);
     let colTextElement = document.createTextNode(aColText);
     colElement.appendChild(colTextElement);
     aRowElement.appendChild(colElement);
   },
 };
 
+let EnvironmentData = {
+  /**
+   * Renders the environment data
+   */
+  render: function(ping) {
+    setHasData("environment-data-section", true);
+    let dataDiv = document.getElementById("environment-data");
+    removeAllChildNodes(dataDiv);
+    let data = sectionalizeObject(ping.environment);
+
+    for (let [section, sectionData] of data) {
+      let table = document.createElement("table");
+      let caption = document.createElement("caption");
+      caption.appendChild(document.createTextNode(section + "\n"));
+      table.appendChild(caption);
+
+      let headings = document.createElement("tr");
+      this.appendColumn(headings, "th", "Name" + "\t");
+      this.appendColumn(headings, "th", "Value" + "\t");
+      table.appendChild(headings);
+
+      for (let [path, value] of sectionData) {
+          let row = document.createElement("tr");
+          this.appendColumn(row, "td", path + "\t");
+          this.appendColumn(row, "td", value + "\t");
+          table.appendChild(row);
+      }
+
+      dataDiv.appendChild(table);
+    }
+  },
+
+  /**
+   * Helper function for appending a column to the data table.
+   *
+   * @param aRowElement Parent row element
+   * @param aColType Column's tag name
+   * @param aColText Column contents
+   */
+  appendColumn: function(aRowElement, aColType, aColText) {
+    let colElement = document.createElement(aColType);
+    let colTextElement = document.createTextNode(aColText);
+    colElement.appendChild(colTextElement);
+    aRowElement.appendChild(colElement);
+  },
+};
+
 let TelLog = {
   /**
    * Renders the telemetry log
    */
-  render: function() {
-    let entries =  TelemetryLog.entries();
+  render: function(aPing) {
+    let entries = aPing.payload.log;
 
     if(entries.length == 0) {
         return;
     }
     setHasData("telemetry-log-section", true);
     let table = document.createElement("table");
 
     let caption = document.createElement("caption");
@@ -181,16 +558,17 @@ let TelLog = {
         let row = document.createElement("tr");
         for (let elem of entry) {
             this.appendColumn(row, "td", elem + "\t");
         }
         table.appendChild(row);
     }
 
     let dataDiv = document.getElementById("telemetry-log");
+    removeAllChildNodes(dataDiv);
     dataDiv.appendChild(table);
   },
 
   /**
    * Helper function for appending a column to the data table.
    *
    * @param aRowElement Parent row element
    * @param aColType Column's tag name
@@ -214,34 +592,40 @@ let SlowSQL = {
 
   mainThreadTitle: bundle.GetStringFromName("slowSqlMain"),
 
   otherThreadTitle: bundle.GetStringFromName("slowSqlOther"),
 
   /**
    * Render slow SQL statistics
    */
-  render: function SlowSQL_render() {
-    let debugSlowSql = getPref(PREF_DEBUG_SLOW_SQL, false);
+  render: function SlowSQL_render(aPing) {
+    // We can add the debug SQL data to the current ping later.
+    // However, we need to be careful to never send that debug data
+    // out due to privacy concerns.
+    // We want to show the actual ping data for archived pings,
+    // so skip this there.
+    let debugSlowSql = PingPicker.viewCurrentPingData && Preferences.get(PREF_DEBUG_SLOW_SQL, false);
     let {mainThread, otherThreads} =
-      Telemetry[debugSlowSql ? "debugSlowSQL" : "slowSQL"];
+      debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
 
     let mainThreadCount = Object.keys(mainThread).length;
     let otherThreadCount = Object.keys(otherThreads).length;
     if (mainThreadCount == 0 && otherThreadCount == 0) {
       return;
     }
 
     setHasData("slow-sql-section", true);
 
     if (debugSlowSql) {
       document.getElementById("sql-warning").classList.remove("hidden");
     }
 
     let slowSqlDiv = document.getElementById("slow-sql-tables");
+    removeAllChildNodes(slowSqlDiv);
 
     // Main thread
     if (mainThreadCount > 0) {
       let table = document.createElement("table");
       this.renderTableHeader(table, this.mainThreadTitle);
       this.renderTable(table, mainThread);
 
       slowSqlDiv.appendChild(table);
@@ -309,27 +693,16 @@ let SlowSQL = {
   appendColumn: function SlowSQL_appendColumn(aRowElement, aColType, aColText) {
     let colElement = document.createElement(aColType);
     let colTextElement = document.createTextNode(aColText);
     colElement.appendChild(colTextElement);
     aRowElement.appendChild(colElement);
   }
 };
 
-/**
- * Removes child elements from the supplied div
- *
- * @param aDiv Element to be cleared
- */
-function clearDivData(aDiv) {
-  while (aDiv.hasChildNodes()) {
-    aDiv.removeChild(aDiv.lastChild);
-  }
-};
-
 let StackRenderer = {
 
   stackTitle: bundle.GetStringFromName("stackTitle"),
 
   memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"),
 
   /**
    * Outputs the memory map associated with this hang report
@@ -360,17 +733,17 @@ let StackRenderer = {
     aDiv.appendChild(document.createTextNode(stackText));
 
     aDiv.appendChild(document.createElement("br"));
     aDiv.appendChild(document.createElement("br"));
   },
   renderStacks: function StackRenderer_renderStacks(aPrefix, aStacks,
                                                     aMemoryMap, aRenderHeader) {
     let div = document.getElementById(aPrefix + '-data');
-    clearDivData(div);
+    removeAllChildNodes(div);
 
     let fetchE = document.getElementById(aPrefix + '-fetch-symbols');
     if (fetchE) {
       fetchE.classList.remove("hidden");
     }
     let hideE = document.getElementById(aPrefix + '-hide-symbols');
     if (hideE) {
       hideE.classList.add("hidden");
@@ -427,17 +800,17 @@ function SymbolicationRequest_handleSymb
   if (this.symbolRequest.readyState != 4)
     return;
 
   let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
   fetchElement.classList.add("hidden");
   let hideElement = document.getElementById(this.prefix + "-hide-symbols");
   hideElement.classList.remove("hidden");
   let div = document.getElementById(this.prefix + "-data");
-  clearDivData(div);
+  removeAllChildNodes(div);
   let errorMessage = bundle.GetStringFromName("errorFetchingSymbols");
 
   if (this.symbolRequest.status != 200) {
     div.appendChild(document.createTextNode(errorMessage));
     return;
   }
 
   let jsonResponse = {};
@@ -460,17 +833,17 @@ function SymbolicationRequest_handleSymb
   }
 };
 /**
  * Send a request to the symbolication server to symbolicate this stack.
  */
 SymbolicationRequest.prototype.fetchSymbols =
 function SymbolicationRequest_fetchSymbols() {
   let symbolServerURI =
-    getPref(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI);
+    Preferences.get(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI);
   let request = {"memoryMap" : this.memoryMap, "stacks" : this.stacks,
                  "version" : 3};
   let requestJSON = JSON.stringify(request);
 
   this.symbolRequest = new XMLHttpRequest();
   this.symbolRequest.open("POST", symbolServerURI, true);
   this.symbolRequest.setRequestHeader("Content-type", "application/json");
   this.symbolRequest.setRequestHeader("Content-length",
@@ -482,41 +855,41 @@ function SymbolicationRequest_fetchSymbo
 
 let ChromeHangs = {
 
   symbolRequest: null,
 
   /**
    * Renders raw chrome hang data
    */
-  render: function ChromeHangs_render() {
-    let hangs = Telemetry.chromeHangs;
+  render: function ChromeHangs_render(aPing) {
+    let hangs = aPing.payload.chromeHangs;
     let stacks = hangs.stacks;
     let memoryMap = hangs.memoryMap;
 
     StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap,
-			       this.renderHangHeader);
+			       (index) => this.renderHangHeader(aPing, index));
   },
 
-  renderHangHeader: function ChromeHangs_renderHangHeader(aIndex) {
-    let durations = Telemetry.chromeHangs.durations;
+  renderHangHeader: function ChromeHangs_renderHangHeader(aPing, aIndex) {
+    let durations = aPing.payload.chromeHangs.durations;
     StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, durations[aIndex]]);
   }
 };
 
 let ThreadHangStats = {
 
   /**
    * Renders raw thread hang stats data
    */
-  render: function() {
+  render: function(aPing) {
     let div = document.getElementById("thread-hang-stats");
-    clearDivData(div);
+    removeAllChildNodes(div);
 
-    let stats = Telemetry.threadHangStats;
+    let stats = aPing.payload.threadHangStats;
     stats.forEach((thread) => {
       div.appendChild(this.renderThread(thread));
     });
     if (stats.length) {
       setHasData("thread-hang-stats-section", true);
     }
   },
 
@@ -567,18 +940,18 @@ let Histogram = {
    *
    * @param aParent Parent element
    * @param aName Histogram name
    * @param aHgram Histogram information
    * @param aOptions Object with render options
    *                 * exponential: bars follow logarithmic scale
    */
   render: function Histogram_render(aParent, aName, aHgram, aOptions) {
-    let hgram = this.unpack(aHgram);
     let options = aOptions || {};
+    let hgram = this.processHistogram(aHgram, aName);
 
     let outerDiv = document.createElement("div");
     outerDiv.className = "histogram";
     outerDiv.id = aName;
 
     let divTitle = document.createElement("div");
     divTitle.className = "histogram-title";
     divTitle.appendChild(document.createTextNode(aName));
@@ -587,76 +960,60 @@ let Histogram = {
     let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " +
                 this.hgramAverageCaption + " = " + hgram.pretty_average + ", " +
                 this.hgramSumCaption + " = " + hgram.sum;
 
     let divStats = document.createElement("div");
     divStats.appendChild(document.createTextNode(stats));
     outerDiv.appendChild(divStats);
 
-    if (isRTL())
+    if (isRTL()) {
+      hgram.buckets.reverse();
       hgram.values.reverse();
+    }
 
-    let textData = this.renderValues(outerDiv, hgram.values, hgram.max,
-                                     hgram.sample_count, options);
+    let textData = this.renderValues(outerDiv, hgram, options);
 
     // The 'Copy' button contains the textual data, copied to clipboard on click
     let copyButton = document.createElement("button");
     copyButton.className = "copy-node";
     copyButton.appendChild(document.createTextNode(this.hgramCopyCaption));
     copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData;
     copyButton.addEventListener("click", function(){
       Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
                                                  .copyString(this.histogramText);
     });
     outerDiv.appendChild(copyButton);
 
     aParent.appendChild(outerDiv);
     return outerDiv;
   },
 
-  /**
-   * Unpacks histogram values
-   *
-   * @param aHgram Packed histogram
-   *
-   * @return Unpacked histogram representation
-   */
-  unpack: function Histogram_unpack(aHgram) {
-    let sample_count = aHgram.counts.reduceRight((a, b) => a + b);
-    let buckets = [0, 1];
-    if (aHgram.histogram_type != Telemetry.HISTOGRAM_BOOLEAN) {
-      buckets = aHgram.ranges;
+  processHistogram: function(aHgram, aName) {
+    const values = [for (k of Object.keys(aHgram.values)) aHgram.values[k]];
+    if (!values.length) {
+      // If we have no values collected for this histogram, just return
+      // zero values so we still render it.
+      return {
+        values: [],
+        pretty_average: 0,
+        max: 0,
+        sample_count: 0,
+        sum: 0
+      };
     }
 
-    let average =  Math.round(aHgram.sum * 10 / sample_count) / 10;
-    let max_value = Math.max.apply(Math, aHgram.counts);
+    const sample_count = values.reduceRight((a, b) => a + b);
+    const average = Math.round(aHgram.sum * 10 / sample_count) / 10;
+    const max_value = Math.max(...values);
 
-    let first = true;
-    let last = 0;
-    let values = [];
-    for (let i = 0; i < buckets.length; i++) {
-      let count = aHgram.counts[i];
-      if (!count)
-        continue;
-      if (first) {
-        first = false;
-        if (i) {
-          values.push([buckets[i - 1], 0]);
-        }
-      }
-      last = i + 1;
-      values.push([buckets[i], count]);
-    }
-    if (last && last < buckets.length) {
-      values.push([buckets[last], 0]);
-    }
+    const labelledValues = [for (k of Object.keys(aHgram.values)) [Number(k), aHgram.values[k]]];
 
     let result = {
-      values: values,
+      values: labelledValues,
       pretty_average: average,
       max: max_value,
       sample_count: sample_count,
       sum: aHgram.sum
     };
 
     return result;
   },
@@ -672,36 +1029,37 @@ let Histogram = {
   },
 
   /**
    * Create histogram HTML bars, also returns a textual representation
    * Both aMaxValue and aSumValues must be positive.
    * Values are assumed to use 0 as baseline.
    *
    * @param aDiv Outer parent div
-   * @param aValues Histogram values
-   * @param aMaxValue Value of the longest bar (length, not label)
-   * @param aSumValues Sum of all bar values
+   * @param aHgram The histogram data
    * @param aOptions Object with render options (@see #render)
    */
-  renderValues: function Histogram_renderValues(aDiv, aValues, aMaxValue, aSumValues, aOptions) {
+  renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
     let text = "";
     // If the last label is not the longest string, alignment will break a little
-    let labelPadTo = String(aValues[aValues.length -1][0]).length;
-    let maxBarValue = aOptions.exponential ? this.getLogValue(aMaxValue) : aMaxValue;
+    let labelPadTo = 0;
+    if (aHgram.values.length) {
+      labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
+    }
+    let maxBarValue = aOptions.exponential ? this.getLogValue(aHgram.max_value) : aHgram.max;
 
-    for (let [label, value] of aValues) {
+    for (let [label, value] of aHgram.values) {
       let barValue = aOptions.exponential ? this.getLogValue(value) : value;
 
       // Create a text representation: <right-aligned-label> |<bar-of-#><value>  <percentage>
       text += EOL
               + " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label
               + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
               + "  " + value // Value
-              + "  " + Math.round(100 * value / aSumValues) + "%"; // Percentage
+              + "  " + Math.round(100 * value / aHgram.sum) + "%"; // Percentage
 
       // Construct the HTML labels + bars
       let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
       let aboveEm = MAX_BAR_HEIGHT - belowEm;
 
       let barDiv = document.createElement("div");
       barDiv.className = "bar";
       barDiv.style.paddingTop = aboveEm + "em";
@@ -907,27 +1265,35 @@ let KeyedHistogram = {
 };
 
 let AddonDetails = {
   tableIDTitle: bundle.GetStringFromName("addonTableID"),
   tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
 
   /**
    * Render the addon details section as a series of headers followed by key/value tables
-   * @param aSections Object containing the details sections to render
+   * @param aPing A ping object to render the data from.
    */
-  render: function AddonDetails_render(aSections) {
+  render: function AddonDetails_render(aPing) {
     let addonSection = document.getElementById("addon-details");
-    for (let provider in aSections) {
+    removeAllChildNodes(addonSection);
+    let addonDetails = aPing.payload.addonDetails;
+    const hasData = Object.keys(addonDetails).length > 0;
+    setHasData("addon-details-section", hasData);
+    if (!hasData) {
+      return;
+    }
+
+    for (let provider in addonDetails) {
       let providerSection = document.createElement("h2");
       let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
       providerSection.appendChild(document.createTextNode(titleText));
       addonSection.appendChild(providerSection);
       addonSection.appendChild(
-        KeyValueTable.render(aSections[provider],
+        KeyValueTable.render(addonDetails[provider],
                              this.tableIDTitle, this.tableDetailsTitle));
     }
   }
 };
 
 /**
  * Helper function for showing either the toggle element or "No data collected" message for a section
  *
@@ -956,73 +1322,82 @@ function toggleSection(aEvent) {
   statebox.checked = parentElement.classList.contains("expanded");
 }
 
 /**
  * Sets the text of the page header based on a config pref + bundle strings
  */
 function setupPageHeader()
 {
-  let serverOwner = getPref(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
+  let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
   let brandName = brandBundle.GetStringFromName("brandFullName");
   let subtitleText = bundle.formatStringFromName(
     "pageSubtitle", [serverOwner, brandName], 2);
 
   let subtitleElement = document.getElementById("page-subtitle");
   subtitleElement.appendChild(document.createTextNode(subtitleText));
 }
 
 /**
  * Initializes load/unload, pref change and mouse-click listeners
  */
 function setupListeners() {
-  Services.prefs.addObserver(PREF_TELEMETRY_ENABLED, observer, false);
-  observer.updatePrefStatus();
+  Settings.attachObservers();
+  PingPicker.attachObservers();
 
   // Clean up observers when page is closed
   window.addEventListener("unload",
     function unloadHandler(aEvent) {
       window.removeEventListener("unload", unloadHandler);
-      Services.prefs.removeObserver(PREF_TELEMETRY_ENABLED, observer);
-  }, false);
-
-  document.getElementById("toggle-telemetry").addEventListener("click",
-    function () {
-      let value = getPref(PREF_TELEMETRY_ENABLED, false);
-      Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, !value);
+      Settings.detachObservers();
   }, false);
 
   document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click",
     function () {
-      let hangs = Telemetry.chromeHangs;
+      if (!gPingData) {
+        return;
+      }
+
+      let hangs = gPingData.payload.chromeHangs;
       let req = new SymbolicationRequest("chrome-hangs",
                                          ChromeHangs.renderHangHeader,
                                          hangs.memoryMap, hangs.stacks);
       req.fetchSymbols();
   }, false);
 
   document.getElementById("chrome-hangs-hide-symbols").addEventListener("click",
     function () {
-      ChromeHangs.render();
+      if (!gPingData) {
+        return;
+      }
+
+      ChromeHangs.render(gPingData);
   }, false);
 
   document.getElementById("late-writes-fetch-symbols").addEventListener("click",
     function () {
-      let lateWrites = TelemetrySession.getPayload().lateWrites;
+      if (!gPingData) {
+        return;
+      }
+
+      let lateWrites = gPingData.payload.lateWrites;
       let req = new SymbolicationRequest("late-writes",
                                          LateWritesSingleton.renderHeader,
                                          lateWrites.memoryMap,
                                          lateWrites.stacks);
       req.fetchSymbols();
   }, false);
 
   document.getElementById("late-writes-hide-symbols").addEventListener("click",
     function () {
-      let ping = TelemetrySession.getPayload();
-      LateWritesSingleton.renderLateWrites(ping.lateWrites);
+      if (!gPingData) {
+        return;
+      }
+
+      LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
   }, false);
 
   // Clicking on the section name will toggle its state
   let sectionHeaders = document.getElementsByClassName("section-name");
   for (let sectionHeader of sectionHeaders) {
     sectionHeader.addEventListener("click", toggleSection, false);
   }
 
@@ -1037,76 +1412,21 @@ function onLoad() {
   window.removeEventListener("load", onLoad);
 
   // Set the text in the page header
   setupPageHeader();
 
   // Set up event listeners
   setupListeners();
 
-  // Show general data.
-  GeneralData.render();
-
-  // Show telemetry log.
-  TelLog.render();
-
-  // Show slow SQL stats
-  SlowSQL.render();
-
-  // Show chrome hang stacks
-  ChromeHangs.render();
-
-  // Show thread hang stats
-  ThreadHangStats.render();
-
-  // Show histogram data
-  let histograms = Telemetry.histogramSnapshots;
-  if (Object.keys(histograms).length) {
-    let hgramDiv = document.getElementById("histograms");
-    for (let [name, hgram] of Iterator(histograms)) {
-      Histogram.render(hgramDiv, name, hgram);
-    }
-
-    let filterBox = document.getElementById("histograms-filter");
-    filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
-    if (filterBox.value.trim() != "") { // on load, no need to filter if empty
-      Histogram.filterHistograms(hgramDiv, filterBox.value);
-    }
+  // Render settings.
+  Settings.render();
 
-    setHasData("histograms-section", true);
-  }
-
-  // Show keyed histogram data
-  let keyedHistograms = Telemetry.keyedHistogramSnapshots;
-  if (Object.keys(keyedHistograms).length) {
-    let keyedDiv = document.getElementById("keyed-histograms");
-    for (let [id, keyed] of Iterator(keyedHistograms)) {
-      KeyedHistogram.render(keyedDiv, id, keyed);
-    }
-
-    setHasData("keyed-histograms-section", true);
-  }
-
-  // Show addon histogram data
-  let addonDiv = document.getElementById("addon-histograms");
-  let addonHistogramsRendered = false;
-  let addonData = Telemetry.addonHistogramSnapshots;
-  for (let [addon, histograms] of Iterator(addonData)) {
-    for (let [name, hgram] of Iterator(histograms)) {
-      addonHistogramsRendered = true;
-      Histogram.render(addonDiv, addon + ": " + name, hgram);
-    }
-  }
-
-  if (addonHistogramsRendered) {
-   setHasData("addon-histograms-section", true);
-  }
-
-  // Get the Telemetry Ping payload
-  Telemetry.asyncFetchTelemetryData(displayPingData);
+  // Update ping data when async Telemetry init is finished.
+  Telemetry.asyncFetchTelemetryData(() => PingPicker.update());
 
   // Restore sections states
   let stateboxes = document.getElementsByClassName("statebox");
   for (let box of stateboxes) {
     if (box.checked) { // Was open. Will still display as empty if not has-data
         box.parentElement.classList.add("expanded");
     }
   }
@@ -1165,41 +1485,116 @@ function sortStartupMilestones(aSimpleMe
   let result = {};
   for (let key of sortedKeys) {
     result[key] = aSimpleMeasurements[key];
   }
 
   return result;
 }
 
-function displayPingData() {
-  let ping = TelemetrySession.getPayload();
+function displayPingData(ping) {
+  gPingData = ping;
+
+  const keysHeader = bundle.GetStringFromName("keysHeader");
+  const valuesHeader = bundle.GetStringFromName("valuesHeader");
+
+  // Show general data.
+  GeneralData.render(ping);
+
+  // Show environment data.
+  EnvironmentData.render(ping);
 
-  let keysHeader = bundle.GetStringFromName("keysHeader");
-  let valuesHeader = bundle.GetStringFromName("valuesHeader");
+  // Show telemetry log.
+  TelLog.render(ping);
+
+  // Show slow SQL stats
+  SlowSQL.render(ping);
+
+  // Show chrome hang stacks
+  ChromeHangs.render(ping);
+
+  // Show thread hang stats
+  ThreadHangStats.render(ping);
+
+  // Render Addon details.
+  AddonDetails.render(ping);
 
   // Show simple measurements
-  let simpleMeasurements = sortStartupMilestones(ping.simpleMeasurements);
-  if (Object.keys(simpleMeasurements).length) {
-    let simpleSection = document.getElementById("simple-measurements");
+  let payload = ping.payload;
+  let simpleMeasurements = sortStartupMilestones(payload.simpleMeasurements);
+  let hasData = Object.keys(simpleMeasurements).length > 0;
+  setHasData("simple-measurements-section", true);
+  let simpleSection = document.getElementById("simple-measurements");
+  removeAllChildNodes(simpleSection);
+
+  if (hasData) {
     simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
                                                    keysHeader, valuesHeader));
-    setHasData("simple-measurements-section", true);
   }
 
-  LateWritesSingleton.renderLateWrites(ping.lateWrites);
+  LateWritesSingleton.renderLateWrites(payload.lateWrites);
 
   // Show basic system info gathered
-  if (Object.keys(ping.info).length) {
-    let infoSection = document.getElementById("system-info");
-    infoSection.appendChild(KeyValueTable.render(ping.info,
+  hasData = Object.keys(payload.info).length > 0;
+  setHasData("system-info-section", hasData);
+  let infoSection = document.getElementById("system-info");
+  removeAllChildNodes(infoSection);
+
+  if (hasData) {
+    infoSection.appendChild(KeyValueTable.render(payload.info,
                                                  keysHeader, valuesHeader));
-    setHasData("system-info-section", true);
   }
 
-  let addonDetails = ping.addonDetails;
-  if (Object.keys(addonDetails).length) {
-    AddonDetails.render(addonDetails);
-    setHasData("addon-details-section", true);
+  // Show histogram data
+  let hgramDiv = document.getElementById("histograms");
+  removeAllChildNodes(hgramDiv);
+
+  let histograms = payload.histograms;
+  hasData = Object.keys(histograms).length > 0;
+  setHasData("histograms-section", hasData);
+
+  if (hasData) {
+    for (let [name, hgram] of Iterator(histograms)) {
+      Histogram.render(hgramDiv, name, hgram, {unpacked: true});
+    }
+
+    let filterBox = document.getElementById("histograms-filter");
+    filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
+    if (filterBox.value.trim() != "") { // on load, no need to filter if empty
+      Histogram.filterHistograms(hgramDiv, filterBox.value);
+    }
+
+    setHasData("histograms-section", true);
+  }
+
+  // Show keyed histogram data
+  let keyedDiv = document.getElementById("keyed-histograms");
+  removeAllChildNodes(keyedDiv);
+
+  let keyedHistograms = payload.keyedHistograms;
+  hasData = Object.keys(keyedHistograms).length > 0;
+  setHasData("keyed-histograms-section", hasData);
+
+  if (hasData) {
+    for (let [id, keyed] of Iterator(keyedHistograms)) {
+      KeyedHistogram.render(keyedDiv, id, keyed, {unpacked: true});
+    }
+  }
+
+  // Show addon histogram data
+  let addonDiv = document.getElementById("addon-histograms");
+  removeAllChildNodes(addonDiv);
+
+  let addonHistogramsRendered = false;
+  let addonData = payload.addonHistograms;
+  for (let [addon, histograms] of Iterator(addonData)) {
+    for (let [name, hgram] of Iterator(histograms)) {
+      addonHistogramsRendered = true;
+      Histogram.render(addonDiv, addon + ": " + name, hgram, {unpacked: true});
+    }
+  }
+
+  if (addonHistogramsRendered) {
+   setHasData("addon-histograms-section", true);
   }
 }
 
 window.addEventListener("load", onLoad, false);
--- a/toolkit/content/aboutTelemetry.xhtml
+++ b/toolkit/content/aboutTelemetry.xhtml
@@ -24,31 +24,90 @@
 
   <body dir="&locale.dir;">
 
     <header id="page-description">
       <h1>&aboutTelemetry.pageTitle;</h1>
 
       <h2 id="page-subtitle"></h2>
 
-      <p id="description-enabled">&aboutTelemetry.telemetryEnabled;</p>
-      <p id="description-disabled">&aboutTelemetry.telemetryDisabled;</p>
+      <table id="settings">
+        <tr>
+          <td>
+            <p id="description-upload-enabled" class="description-enabled">FHR data upload is <span>enabled</span>.</p>
+            <p id="description-upload-disabled" class="description-disabled">FHR data upload is <span>disabled</span>.</p>
+          </td>
+          <td>
+            <a href="" class="change-data-choices-link">Change</a>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <p id="description-extended-recording-enabled" class="description-enabled">Extended Telemetry recording is <span>enabled</span>.</p>
+            <p id="description-extended-recording-disabled" class="description-disabled">Extended Telemetry recording is <span>disabled</span>.</p>
+          </td>
+          <td>
+            <a href="" class="change-data-choices-link">Change</a>
+          </td>
+        </tr>
+      </table>
 
-      <button id="toggle-telemetry" type="button"/>
+      <div id="ping-picker">
+        <div id="ping-source-picker">
+          Ping data source:<br/>
+          <input type="radio" id="ping-source-current" name="choose-ping-source" value="current" checked="checked" />
+          Current ping data<br />
+          <input type="radio" id="ping-source-archive" name="choose-ping-source" value="archive" />
+          Archived ping data<br />
+        </div>
+        <div id="current-ping-picker">
+          <input id="show-subsession-data" type="checkbox" />Show subsession data
+        </div>
+        <div id="archived-ping-picker" class="hidden">
+          Choose ping:<br />
+          <button id="next-ping" type="button">&lt;&lt; Previous ping</button>
+          <button id="previous-ping" type="button">Next ping &gt;&gt;</button><br />
+          <table>
+            <tr>
+                <th>Week</th>
+                <th>Ping</th>
+            </tr>
+            <tr>
+                <td>
+                    <select id="choose-ping-week">
+                    </select>
+                </td>
+                <td>
+                    <select id="choose-ping-id">
+                    </select>
+                </td>
+            </tr>
+          </table>
+        </div>
+      </div>
     </header>
 
     <section id="general-data-section" class="data-section">
       <input type="checkbox" class="statebox"/>
       <h1 class="section-name">&aboutTelemetry.generalDataSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggle;</span>
       <span class="empty-caption">&aboutTelemetry.emptySection;</span>
       <div id="general-data" class="data">
       </div>
     </section>
 
+    <section id="environment-data-section" class="data-section">
+      <input type="checkbox" class="statebox"/>
+      <h1 class="section-name">Environment Data</h1>
+      <span class="toggle-caption">&aboutTelemetry.toggle;</span>
+      <span class="empty-caption">&aboutTelemetry.emptySection;</span>
+      <div id="environment-data" class="data">
+      </div>
+    </section>
+
     <section id="telemetry-log-section" class="data-section">
       <input type="checkbox" class="statebox"/>
       <h1 class="section-name">&aboutTelemetry.telemetryLogSection;</h1>
       <span class="toggle-caption">&aboutTelemetry.toggle;</span>
       <span class="empty-caption">&aboutTelemetry.emptySection;</span>
       <div id="telemetry-log" class="data">
       </div>
     </section>
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -22,17 +22,17 @@ toolkit.jar:
    content/global/aboutNetworking.xhtml
    content/global/aboutServiceWorkers.js
    content/global/aboutServiceWorkers.xhtml
    content/global/aboutwebrtc/aboutWebrtc.css   (aboutwebrtc/aboutWebrtc.css)
    content/global/aboutwebrtc/aboutWebrtc.js    (aboutwebrtc/aboutWebrtc.js)
    content/global/aboutwebrtc/aboutWebrtc.xhtml (aboutwebrtc/aboutWebrtc.xhtml)
 *  content/global/aboutSupport.js
 *  content/global/aboutSupport.xhtml
-*  content/global/aboutTelemetry.js
+   content/global/aboutTelemetry.js
    content/global/aboutTelemetry.xhtml
    content/global/aboutTelemetry.css          (aboutTelemetry.css)
    content/global/directionDetector.html
    content/global/plugins.html
    content/global/plugins.css
    content/global/browser-child.js            (browser-child.js)
    content/global/browser-content.js          (browser-content.js)
 *+  content/global/buildconfig.html            (buildconfig.html)