Bug 1150115 - Update the SelfSupport API for the unification changes. r=bsmedberg,smaug a=sledru
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Tue, 02 Jun 2015 13:35:06 +0700
changeset 275142 a40529984065c032e915ff74608991114f81f610
parent 275141 b86ca4f51a3920ae88b9783a7b40034397ea7384
child 275143 ea083e8a04a22e582ec3bb93eba50739f04e8e9e
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)
reviewersbsmedberg, smaug, sledru
bugs1150115
milestone40.0a2
Bug 1150115 - Update the SelfSupport API for the unification changes. r=bsmedberg,smaug a=sledru
browser/base/content/abouthealthreport/abouthealth.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_aboutHealthReport.js
browser/base/content/test/general/healthreport_pingData.js
browser/base/content/test/general/healthreport_testRemoteCommands.html
browser/components/selfsupport/SelfSupportService.js
dom/webidl/MozSelfSupport.webidl
--- a/browser/base/content/abouthealthreport/abouthealth.js
+++ b/browser/base/content/abouthealthreport/abouthealth.js
@@ -44,28 +44,73 @@ let healthReportWrapper = {
       };
       healthReportWrapper.injectData("prefs", prefs);
     }
     catch (ex) {
       healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PREFS_FAILED);
     }
   },
 
+  sendTelemetryPingList: function () {
+    console.log("AboutHealthReport: Collecting Telemetry ping list.");
+    MozSelfSupport.getTelemetryPingList().then((list) => {
+      console.log("AboutHealthReport: Sending Telemetry ping list.");
+      this.injectData("telemetry-ping-list", list);
+    }).catch((ex) => {
+      console.log("AboutHealthReport: Collecting ping list failed: " + ex);
+    });
+  },
+
+  sendTelemetryPingData: function (pingId) {
+    console.log("AboutHealthReport: Collecting Telemetry ping data.");
+    MozSelfSupport.getTelemetryPing(pingId).then((ping) => {
+      console.log("AboutHealthReport: Sending Telemetry ping data.");
+      this.injectData("telemetry-ping-data", {
+        id: pingId,
+        pingData: ping,
+      });
+    }).catch((ex) => {
+      console.log("AboutHealthReport: Loading ping data failed: " + ex);
+      this.injectData("telemetry-ping-data", {
+        id: pingId,
+        error: "error-generic",
+      });
+    });
+  },
+
+  sendCurrentEnvironment: function () {
+    console.log("AboutHealthReport: Sending Telemetry environment data.");
+    MozSelfSupport.getCurrentTelemetryEnvironment().then((environment) => {
+      this.injectData("telemetry-current-environment-data", environment);
+    }).catch((ex) => {
+      console.log("AboutHealthReport: Collecting current environment data failed: " + ex);
+    });
+  },
+
+  sendCurrentPingData: function () {
+    console.log("AboutHealthReport: Sending current Telemetry ping data.");
+    MozSelfSupport.getCurrentTelemetrySubsessionPing().then((ping) => {
+      this.injectData("telemetry-current-ping-data", ping);
+    }).catch((ex) => {
+      console.log("AboutHealthReport: Collecting current ping data failed: " + ex);
+    });
+  },
+
   refreshPayload: function () {
     MozSelfSupport.getHealthReportPayload().then(this.updatePayload,
                                                  this.handlePayloadFailure);
   },
 
   updatePayload: function (payload) {
     healthReportWrapper.injectData("payload", JSON.stringify(payload));
   },
 
   injectData: function (type, content) {
     let report = this._getReportURI();
-    
+
     // file URIs can't be used for targetOrigin, so we use "*" for this special case
     // in all other cases, pass in the URL to the report so we properly restrict the message dispatch
     let reportUrl = report.scheme == "file" ? "*" : report.spec;
 
     let data = {
       type: type,
       content: content
     }
@@ -83,16 +128,28 @@ let healthReportWrapper = {
         this.setDataSubmission(true);
         break;
       case "RequestCurrentPrefs":
         this.updatePrefState();
         break;
       case "RequestCurrentPayload":
         this.refreshPayload();
         break;
+      case "RequestTelemetryPingList":
+        this.sendTelemetryPingList();
+        break;
+      case "RequestTelemetryPingData":
+        this.sendTelemetryPingData(evt.detail.id);
+        break;
+      case "RequestCurrentEnvironment":
+        this.sendCurrentEnvironment();
+        break;
+      case "RequestCurrentPingData":
+        this.sendCurrentPingData();
+        break;
       default:
         Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
         break;
     }
   },
 
   initRemotePage: function () {
     let iframe = document.getElementById("remote-report").contentDocument;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -59,16 +59,17 @@ support-files =
   file_bug970276_favicon2.ico
   file_dom_notifications.html
   file_double_close_tab.html
   file_favicon_change.html
   file_favicon_change_not_in_document.html
   file_fullscreen-window-open.html
   get_user_media.html
   head.js
+  healthreport_pingData.js
   healthreport_testRemoteCommands.html
   moz.png
   navigating_window_with_download.html
   offlineQuotaNotification.cacheManifest
   offlineQuotaNotification.html
   page_style_sample.html
   parsingTestHelpers.jsm
   pinning_headers.sjs
--- a/browser/base/content/test/general/browser_aboutHealthReport.js
+++ b/browser/base/content/test/general/browser_aboutHealthReport.js
@@ -2,38 +2,80 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
+const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
+
+const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
+const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
+
 registerCleanupFunction(function() {
   // Ensure we don't pollute prefs for next tests.
+  if (telemetryOriginalLogPref) {
+    Preferences.set(TELEMETRY_LOG_PREF, telemetryOriginalLogPref);
+  } else {
+    Preferences.reset(TELEMETRY_LOG_PREF);
+  }
+
   try {
     Services.prefs.clearUserPref("datareporting.healthreport.about.reportUrl");
     let policy = Cc["@mozilla.org/datareporting/service;1"]
                  .getService(Ci.nsISupports)
                  .wrappedJSObject
                  .policy;
-        policy.recordHealthReportUploadEnabled(true,
+    policy.recordHealthReportUploadEnabled(true,
                                            "Resetting after tests.");
   } catch (ex) {}
 });
 
+function fakeTelemetryNow(...args) {
+  let date = new Date(...args);
+  let scope = {};
+  const modules = [
+    Cu.import("resource://gre/modules/TelemetrySession.jsm", scope),
+    Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", scope),
+    Cu.import("resource://gre/modules/TelemetryController.jsm", scope),
+  ];
+
+  for (let m of modules) {
+    m.Policy.now = () => new Date(date);
+  }
+
+  return date;
+}
+
+function setupPingArchive() {
+  let scope = {};
+  Cu.import("resource://gre/modules/TelemetryController.jsm", scope);
+  Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+    .loadSubScript(CHROME_BASE + "healthreport_pingData.js", scope);
+
+  for (let p of scope.TEST_PINGS) {
+    fakeTelemetryNow(p.date);
+    p.id = yield scope.TelemetryController.submitExternalPing(p.type, p.payload);
+  }
+}
+
 let gTests = [
 
 {
   desc: "Test the remote commands",
-  setup: function ()
+  setup: Task.async(function*()
   {
-    Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl",
-                               "https://example.com/browser/browser/base/content/test/general/healthreport_testRemoteCommands.html");
-  },
+    Preferences.set(TELEMETRY_LOG_PREF, "Trace");
+    yield setupPingArchive();
+    Preferences.set("datareporting.healthreport.about.reportUrl",
+                    HTTPS_BASE + "healthreport_testRemoteCommands.html");
+  }),
   run: function (iframe)
   {
     let deferred = Promise.defer();
 
     let policy = Cc["@mozilla.org/datareporting/service;1"]
                  .getService(Ci.nsISupports)
                  .wrappedJSObject
                  .policy;
@@ -56,30 +98,29 @@ let gTests = [
     } catch(e) {
       ok(false, "Failed to get all commands");
       deferred.reject();
     }
     return deferred.promise;
   }
 },
 
-
 ]; // gTests
 
 function test()
 {
   waitForExplicitFinish();
 
   // xxxmpc leaving this here until we resolve bug 854038 and bug 854060
   requestLongerTimeout(10);
 
   Task.spawn(function () {
     for (let test of gTests) {
       info(test.desc);
-      test.setup();
+      yield test.setup();
 
       let iframe = yield promiseNewTabLoadEvent("about:healthreport");
 
       yield test.run(iframe);
 
       gBrowser.removeCurrentTab();
     }
 
@@ -100,9 +141,8 @@ function promiseNewTabLoadEvent(aUrl, aE
           return;
         }
         iframe.removeEventListener("load", frameLoad, false);
         deferred.resolve(iframe);
       }, false);
     }, true);
   return deferred.promise;
 }
-
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/healthreport_pingData.js
@@ -0,0 +1,17 @@
+const TEST_PINGS = [
+  {
+    type: "test-telemetryArchive-1",
+    payload: { foo: "bar" },
+    date: new Date(2010, 1, 1, 10, 0, 0),
+  },
+  {
+    type: "test-telemetryArchive-2",
+    payload: { x: { y: "z"} },
+    date: new Date(2010, 1, 1, 11, 0, 0),
+  },
+  {
+    type: "test-telemetryArchive-3",
+    payload: { moo: "meh" },
+    date: new Date(2010, 1, 1, 12, 0, 0),
+  },
+];
--- a/browser/base/content/test/general/healthreport_testRemoteCommands.html
+++ b/browser/base/content/test/general/healthreport_testRemoteCommands.html
@@ -1,13 +1,15 @@
 <html>
   <head>
     <meta charset="utf-8">
-
-<script>
+<script type="application/javascript;version=1.7"
+            src="healthreport_pingData.js">
+</script>
+<script type="application/javascript;version=1.7">
 
 function init() {
   window.addEventListener("message", function process(e) {
     // The init function of abouthealth.js schedules an initial payload event,
     // which will be sent after the payload data has been collected. This extra
     // event can cause unexpected successes/failures in this test, so we wait
     // for the extra event to arrive here before progressing with the actual
     // test.
@@ -29,16 +31,90 @@ function validatePayload(payload) {
 
   // xxxmpc - this is some pretty low-bar validation, but we have plenty of tests of that API elsewhere
   if (!payload.thisPingDate)
     return false;
 
   return true;
 }
 
+function isArray(arg) {
+  return Object.prototype.toString.call(arg) === '[object Array]';
+}
+
+function writeDiagnostic(text) {
+  let node = document.createTextNode(text);
+  let br = document.createElement("br");
+  document.body.appendChild(node);
+  document.body.appendChild(br);
+}
+
+function validateCurrentTelemetryEnvironment(data) {
+  // Simple check for now: check that the received object has the expected
+  // top-level properties.
+  const expectedKeys = ["profile", "settings", "system", "build", "partner", "addons"];
+  return expectedKeys.every(key => (key in data));
+}
+
+function validateCurrentTelemetryPingData(ping) {
+  // Simple check for now: check that the received object has the expected
+  // top-level properties and that the type and reason match.
+  const expectedKeys = ["environment", "clientId", "payload", "application",
+                        "version", "type", "id"];
+  return expectedKeys.every(key => (key in ping)) &&
+         (ping.type == "main") &&
+         ("info" in ping.payload) &&
+         ("reason" in ping.payload.info) &&
+         (ping.payload.info.reason == "gather-subsession-payload");
+}
+
+function validateTelemetryPingList(list) {
+  if (!isArray(list)) {
+    console.log("Telemetry ping list is not an array.");
+    return false;
+  }
+
+  if (list.length != TEST_PINGS.length) {
+    console.log("Telemetry ping length is not correct.");
+    return false;
+  }
+
+  let valid = true;
+  for (let i=0; i<list.length; ++i) {
+    let received = list[i];
+    let expected = TEST_PINGS[i];
+    if (received.type != expected.type ||
+        received.timestampCreated != expected.date.getTime()) {
+      writeDiagnostic("Telemetry ping " + i + " does not match.");
+      writeDiagnostic("Expected: " + JSON.stringify(expected));
+      writeDiagnostic("Received: " + JSON.stringify(received));
+      valid = false;
+    } else {
+      writeDiagnostic("Telemetry ping " + i + " matches.");
+    }
+  }
+
+  return true;
+}
+
+function validateTelemetryPingData(expected, received) {
+  const receivedDate = new Date(received.creationDate);
+  if (received.id != expected.id ||
+      received.type != expected.type ||
+      receivedDate.getTime() != expected.date.getTime()) {
+    writeDiagnostic("Telemetry ping data for " + expected.id + " doesn't match.");
+    writeDiagnostic("Expected: " + JSON.stringify(expected));
+    writeDiagnostic("Received: " + JSON.stringify(received));
+    return false;
+  }
+
+  writeDiagnostic("Telemetry ping data for " + expected.id + " matched.");
+  return true;
+}
+
 var tests = [
 {
   info: "Checking initial value is enabled",
   event: "RequestCurrentPrefs",
   payloadType: "prefs",
   validateResponse: function(payload) {
     return checkSubmissionValue(payload, true);
   },
@@ -86,16 +162,60 @@ var tests = [
 {
   info: "Verifying we can get a payload after re-enabling",
   event: "RequestCurrentPayload",
   payloadType: "payload",
   validateResponse: function(payload) {
     return validatePayload(payload);
   },
 },
+{
+  info: "Verifying that we can get the current Telemetry environment data",
+  event: "RequestCurrentEnvironment",
+  payloadType: "telemetry-current-environment-data",
+  validateResponse: function(payload) {
+    return validateCurrentTelemetryEnvironment(payload);
+  },
+},
+{
+  info: "Verifying that we can get the current Telemetry ping data",
+  event: "RequestCurrentPingData",
+  payloadType: "telemetry-current-ping-data",
+  validateResponse: function(payload) {
+    return validateCurrentTelemetryPingData(payload);
+  },
+},
+{
+  info: "Verifying that we get the proper Telemetry ping list",
+  event: "RequestTelemetryPingList",
+  payloadType: "telemetry-ping-list",
+  validateResponse: function(payload) {
+    // Validate the ping list
+    if (!validateTelemetryPingList(payload)) {
+      return false;
+    }
+
+    // Now that we received the ping ids, set up additional test tasks
+    // that check loading the individual pings.
+    for (let i=0; i<TEST_PINGS.length; ++i) {
+      TEST_PINGS[i].id = payload[i].id;
+      tests.push({
+        info: "Verifying that we can get the proper Telemetry ping data #" + (i + 1),
+        event: "RequestTelemetryPingData",
+        eventData: { id: TEST_PINGS[i].id },
+        payloadType: "telemetry-ping-data",
+        validateResponse: function(payload) {
+          return validateTelemetryPingData(TEST_PINGS[i], payload.pingData);
+        },
+      });
+    }
+
+    return true;
+  },
+},
 ];
 
 var currentTest = -1;
 function doTest(evt) {
   if (evt) {
     if (currentTest < 0 || !evt.data.content)
       return; // not yet testing
 
@@ -107,35 +227,41 @@ function doTest(evt) {
     var pass = false;
     try {
       pass = test.validateResponse(evt.data.content)
     } catch (e) {}
     reportResult(test.info, pass, error);
   }
   // start the next test if there are any left
   if (tests[++currentTest])
-    sendToBrowser(tests[currentTest].event);
+    sendToBrowser(tests[currentTest].event, tests[currentTest].eventData);
   else
     reportFinished();
 }
 
 function reportResult(info, pass, error) {
   var data = {type: "testResult", info: info, pass: pass, error: error};
   var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
   document.dispatchEvent(event);
 }
 
 function reportFinished(cmd) {
   var data = {type: "testsComplete", count: tests.length};
   var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
   document.dispatchEvent(event);
 }
 
-function sendToBrowser(type) {
-  var event = new CustomEvent("RemoteHealthReportCommand", {detail: {command: type}, bubbles: true});
+function sendToBrowser(type, eventData) {
+  eventData = eventData || {};
+  let detail = {command: type};
+  for (let key of Object.keys(eventData)) {
+    detail[key] = eventData[key];
+  }
+
+  var event = new CustomEvent("RemoteHealthReportCommand", {detail: detail, bubbles: true});
   document.dispatchEvent(event);
 }
 
 </script>
   </head>
   <body onload="init()">
   </body>
 </html>
--- a/browser/components/selfsupport/SelfSupportService.js
+++ b/browser/components/selfsupport/SelfSupportService.js
@@ -16,16 +16,23 @@ const policy = Cc["@mozilla.org/datarepo
 
 XPCOMUtils.defineLazyGetter(this, "reporter", () => {
   return Cc["@mozilla.org/datareporting/service;1"]
            .getService(Ci.nsISupports)
            .wrappedJSObject
            .healthReporter;
 });
 
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive",
+                                  "resource://gre/modules/TelemetryArchive.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
+                                  "resource://gre/modules/TelemetryEnvironment.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController",
+                                  "resource://gre/modules/TelemetryController.jsm");
+
 function MozSelfSupportInterface() {
 }
 
 MozSelfSupportInterface.prototype = {
   classDescription: "MozSelfSupport",
   classID: Components.ID("{d30aae8b-f352-4de3-b936-bb9d875df0bb}"),
   contractID: "@mozilla.org/mozselfsupport;1",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
@@ -68,11 +75,34 @@ MozSelfSupportInterface.prototype = {
   resetPref: function(name) {
     Services.prefs.clearUserPref(name);
   },
 
   resetSearchEngines: function() {
     Services.search.restoreDefaultEngines();
     Services.search.resetToOriginalDefaultEngine();
   },
+
+  getTelemetryPingList: function() {
+    return this._wrapPromise(TelemetryArchive.promiseArchivedPingList());
+  },
+
+  getTelemetryPing: function(pingId) {
+    return this._wrapPromise(TelemetryArchive.promiseArchivedPingById(pingId));
+  },
+
+  getCurrentTelemetryEnvironment: function() {
+    const current = TelemetryEnvironment.currentEnvironment;
+    return new this._window.Promise(resolve => resolve(current));
+  },
+
+  getCurrentTelemetrySubsessionPing: function() {
+    const current = TelemetryController.getCurrentPingData(true);
+    return new this._window.Promise(resolve => resolve(current));
+  },
+
+  _wrapPromise: function(promise) {
+    return new this._window.Promise(
+      (resolve, reject) => promise.then(resolve, reject));
+  },
 }
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozSelfSupportInterface]);
--- a/dom/webidl/MozSelfSupport.webidl
+++ b/dom/webidl/MozSelfSupport.webidl
@@ -36,16 +36,55 @@ interface MozSelfSupport
    * information.
    *
    * @return Promise<Object>
    *         Resolved when the FHR payload data has been collected.
    */
   Promise<object> getHealthReportPayload();
 
   /**
+   * Retrieve a list of the archived Telemetry pings.
+   * This contains objects with ping info, which are of the form:
+   * {
+   *   type: <string>, // The pings type, e.g. "main", "environment-change", ...
+   *   timestampCreated: <number>, // The time the ping was created (ms since unix epoch).
+   *   id: <string>, // The pings UUID.
+   * }
+   *
+   * @return Promise<sequence<Object>>
+   *         Resolved with the ping infos when the archived ping list has been built.
+   */
+  Promise<sequence<object>> getTelemetryPingList();
+
+  /**
+   * Retrieve an archived Telemetry ping by it's id.
+   * This will load the ping data async from the archive, possibly hitting the disk.
+   *
+   * @return Promise<Object>
+   *         Resolved with the ping data, see the Telemetry "main" ping documentation for the format.
+   */
+  Promise<object> getTelemetryPing(DOMString pingID);
+
+  /**
+   * Get the current Telemetry environment - see the Telemetry documentation for details on the format.
+   *
+   * @return Promise<Object>
+   *         Resolved with an object containing the Telemetry environment data.
+   */
+  Promise<object> getCurrentTelemetryEnvironment();
+
+  /**
+   * Get a Telemetry "main" ping containing the current session measurements.
+   *
+   * @return Promise<Object>
+   *         Resolved with the ping data, see the Telemetry "main" ping documentation for the format.
+   */
+  Promise<object> getCurrentTelemetrySubsessionPing();
+
+  /**
    * Resets a named pref:
    * - if there is a default value, then change the value back to default,
    * - if there's no default value, then delete the pref,
    * - no-op otherwise.
    *
    * @param DOMString
    *        The name of the pref to reset.
    */