Bug 893098, record update starts, stops and errors in the health report, r=rnewman,rstrong
authorNeil Deakin <neil@mozilla.com>
Wed, 14 May 2014 08:01:45 -0400
changeset 183123 a55387bf473bad74ce711a358952b96db68e1f4e
parent 183122 17942ed40870d33e2265ee67afbe199c8ad98523
child 183124 83fd85b082d16a102f895a4c9a15f9768d6b9a38
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersrnewman, rstrong
bugs893098
milestone32.0a1
Bug 893098, record update starts, stops and errors in the health report, r=rnewman,rstrong
toolkit/mozapps/update/UpdaterHealthProvider.jsm
toolkit/mozapps/update/moz.build
toolkit/mozapps/update/nsUpdateService.js
toolkit/mozapps/update/nsUpdateService.manifest
toolkit/mozapps/update/tests/unit_aus_update/updateHealthReport.js
toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/UpdaterHealthProvider.jsm
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "UpdateProvider",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
+
+function UpdateMeasurement1() {
+  Metrics.Measurement.call(this);
+}
+
+UpdateMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "update",
+  version: 1,
+
+  fields: {
+    updateCheckStartCount: DAILY_COUNTER_FIELD,
+    updateCheckSuccessCount: DAILY_COUNTER_FIELD,
+    updateCheckFailedCount: DAILY_COUNTER_FIELD,
+    updateCheckFailedStatuses: DAILY_DISCRETE_NUMERIC_FIELD,
+    completeUpdateStartCount: DAILY_COUNTER_FIELD,
+    partialUpdateStartCount: DAILY_COUNTER_FIELD,
+    completeUpdateSuccessCount: DAILY_COUNTER_FIELD,
+    partialUpdateSuccessCount: DAILY_COUNTER_FIELD,
+    updateFailedCount: DAILY_COUNTER_FIELD,
+    updateFailedStatuses: DAILY_DISCRETE_NUMERIC_FIELD,
+  },
+});
+
+this.UpdateProvider = function () {
+  Metrics.Provider.call(this);
+};
+UpdateProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.update",
+
+  measurementTypes: [
+    UpdateMeasurement1,
+  ],
+
+  recordUpdate: function (field, status) {
+    let m = this.getMeasurement(UpdateMeasurement1.prototype.name,
+                                UpdateMeasurement1.prototype.version);
+
+    return this.enqueueStorageOperation(function recordUpdateFields() {
+      return Task.spawn(function recordUpdateFieldsTask() {
+        yield m.incrementDailyCounter(field + "Count");
+
+        if ((field == "updateFailed" || field == "updateCheckFailed") && status) {
+          yield m.addDailyDiscreteNumeric(field + "Statuses", status);
+        }
+      }.bind(this));
+    }.bind(this));
+  },
+});
--- a/toolkit/mozapps/update/moz.build
+++ b/toolkit/mozapps/update/moz.build
@@ -41,9 +41,13 @@ if CONFIG['MOZ_UPDATER']:
         'nsUpdateService.manifest',
     ]
 
     EXTRA_PP_COMPONENTS += [
         'nsUpdateService.js',
         'nsUpdateServiceStub.js',
     ]
 
-JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file
+    EXTRA_JS_MODULES += [
+      'UpdaterHealthProvider.jsm'
+    ]
+
+JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -7,16 +7,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/ctypes.jsm");
+Components.utils.import("resource://gre/modules/UpdaterHealthProvider.jsm");
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 const UPDATESERVICE_CID = Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}");
 const UPDATESERVICE_CONTRACTID = "@mozilla.org/updates/update-service;1";
@@ -253,16 +254,28 @@ const PING_BGUC_ADDON_PREF_DISABLED     
 const PING_BGUC_ADDON_SAME_APP_VER           = 27;
 // No incompatible add-ons found during incompatible check (background download)
 const PING_BGUC_CHECK_NO_INCOMPAT            = 28;
 // Incompatible add-ons found and all of them have updates (background download)
 const PING_BGUC_ADDON_UPDATES_FOR_INCOMPAT   = 29;
 // Incompatible add-ons found (update notification)
 const PING_BGUC_ADDON_HAVE_INCOMPAT          = 30;
 
+// Health report field names
+const UpdaterHealthReportFields = {
+  CHECK_START: "updateCheckStart",
+  CHECK_SUCCESS: "updateCheckSuccess",
+  CHECK_FAILED: "updateCheckFailed",
+  COMPLETE_START: "completeUpdateStart",
+  PARTIAL_START: "partialUpdateStart",
+  COMPLETE_SUCCESS: "completeUpdateSuccess",
+  PARTIAL_SUCCESS: "partialUpdateSuccess",
+  FAILED: "updateFailed"
+};
+
 var gLocale = null;
 var gUpdateMutexHandle = null;
 
 #ifdef MOZ_WIDGET_GONK
 var gSDCardMountLock = null;
 
 XPCOMUtils.defineLazyGetter(this, "gExtStorage", function aus_gExtStorage() {
     return Services.env.get("EXTERNAL_STORAGE");
@@ -931,16 +944,47 @@ function getStatusTextFromCode(code, def
     reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
     LOG("getStatusTextFromCode - transfer error: " + reason +
         ", default code: " + defaultCode);
   }
   return reason;
 }
 
 /**
+ * Record count in the health report.
+ * @param field
+ *        The field name to record
+ * @param status
+ *        Status code for errors, 0 otherwise
+ */
+function recordInHealthReport(field, status) {
+#ifdef MOZ_SERVICES_HEALTHREPORT
+  try {
+    LOG("recordInHealthReport - " + field + " - " + status);
+
+    let reporter = Cc["@mozilla.org/datareporting/service;1"]
+                      .getService().wrappedJSObject.healthReporter;
+
+    if (reporter) {
+      reporter.onInit().then(function recordUpdateInHealthReport() {
+        try {
+          reporter.getProvider("org.mozilla.update").recordUpdate(field, status);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      });
+    }
+  // If getting the heath reporter service fails, don't fail updating.
+  } catch (ex) {
+    LOG("recordInHealthReport - could not initialize health reporter");
+  }
+#endif
+}
+
+/**
  * Get the Active Updates directory
  * @return The active updates directory, as a nsIFile object
  */
 function getUpdatesDir() {
   // Right now, we only support downloading one patch at a time, so we always
   // use the same target directory.
   return getUpdateDirCreate([DIR_UPDATES, "0"]);
 }
@@ -3595,16 +3639,18 @@ Checker.prototype = {
       throw Cr.NS_ERROR_NULL_POINTER;
 
     Services.obs.notifyObservers(null, "update-check-start", null);
 
     var url = this.getUpdateURL(force);
     if (!url || (!this.enabled && !force))
       return;
 
+    recordInHealthReport(UpdaterHealthReportFields.CHECK_START, 0);
+
     this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
                     createInstance(Ci.nsISupports);
     // This is here to let unit test code override XHR
     if (this._request.wrappedJSObject) {
       this._request = this._request.wrappedJSObject;
     }
     this._request.open("GET", url, true);
     var allowNonBuiltIn = !getPref("getBoolPref",
@@ -3718,16 +3764,18 @@ Checker.prototype = {
       gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs);
 
       if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CERT_ERRORS))
         Services.prefs.clearUserPref(PREF_APP_UPDATE_CERT_ERRORS);
 
       if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS))
         Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
 
+      recordInHealthReport(UpdaterHealthReportFields.CHECK_SUCCESS, 0);
+
       // Tell the callback about the updates
       this._callback.onCheckComplete(event.target, updates, updates.length);
     }
     catch (e) {
       LOG("Checker:onLoad - there was a problem checking for updates. " +
           "Exception: " + e);
       var request = event.target;
       var status = this._getChannelStatus(request);
@@ -3738,16 +3786,19 @@ Checker.prototype = {
 
       if (this._isHttpStatusCode(status)) {
         update.errorCode = HTTP_ERROR_OFFSET + status;
       }
       if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
         update.errorCode = updates[0] ? CERT_ATTR_CHECK_FAILED_HAS_UPDATE
                                       : CERT_ATTR_CHECK_FAILED_NO_UPDATE;
       }
+
+      recordInHealthReport(UpdaterHealthReportFields.CHECK_FAILED, update.errorCode);
+
       this._callback.onError(request, update);
     }
 
     this._callback = null;
     this._request = null;
   },
 
   /**
@@ -3769,16 +3820,18 @@ Checker.prototype = {
 
     if (status == Cr.NS_ERROR_OFFLINE) {
       // We use a separate constant here because nsIUpdate.errorCode is signed
       update.errorCode = NETWORK_ERROR_OFFLINE;
     } else if (this._isHttpStatusCode(status)) {
       update.errorCode = HTTP_ERROR_OFFSET + status;
     }
 
+    recordInHealthReport(UpdaterHealthReportFields.CHECK_FAILED, update.errorCode);
+
     this._callback.onError(request, update);
 
     this._request = null;
   },
 
   /**
    * Whether or not we are allowed to do update checking.
    */
@@ -4083,16 +4136,20 @@ Downloader.prototype = {
     // to download.
     this._patch = this._selectPatch(update, updateDir);
     if (!this._patch) {
       LOG("Downloader:downloadUpdate - no patch to download");
       return readStatusFile(updateDir);
     }
     this.isCompleteUpdate = this._patch.type == "complete";
 
+    recordInHealthReport(
+      this.isCompleteUpdate ? UpdaterHealthReportFields.COMPLETE_START :
+                              UpdaterHealthReportFields.PARTIAL_START, 0);
+
     var patchFile = null;
 
 #ifdef MOZ_WIDGET_GONK
     let status = readStatusFile(updateDir);
     if (isInterruptedUpdate(status)) {
       LOG("Downloader:downloadUpdate - interruptted update");
       // The update was interrupted. Try to locate the existing patch file.
       // For an interrupted download, this allows a resume rather than a
@@ -4338,16 +4395,20 @@ Downloader.prototype = {
     var retryTimeout = getPref("getIntPref", PREF_APP_UPDATE_RETRY_TIMEOUT,
                                DEFAULT_UPDATE_RETRY_TIMEOUT);
     var maxFail = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_ERRORS,
                           DEFAULT_SOCKET_MAX_ERRORS);
     LOG("Downloader:onStopRequest - status: " + status + ", " +
         "current fail: " + this.updateService._consecutiveSocketErrors + ", " +
         "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout);
     if (Components.isSuccessCode(status)) {
+      recordInHealthReport(
+        this.isCompleteUpdate ? UpdaterHealthReportFields.COMPLETE_SUCCESS :
+                                UpdaterHealthReportFields.PARTIAL_SUCCESS, 0);
+
       if (this._verifyDownload()) {
         state = shouldUseService() ? STATE_PENDING_SVC : STATE_PENDING;
         if (this.background) {
           shouldShowPrompt = !getCanStageUpdates();
         }
 
         // Tell the updater.exe we're ready to apply.
         writeStatusFile(getUpdatesDir(), state);
@@ -4366,59 +4427,63 @@ Downloader.prototype = {
         this._update.statusText = message;
 
         if (this._update.isCompleteUpdate || this._update.patchCount != 2)
           deleteActiveUpdate = true;
 
         // Destroy the updates directory, since we're done with it.
         cleanUpUpdatesDir();
       }
-    } else if (status == Cr.NS_ERROR_OFFLINE) {
-      // Register an online observer to try again.
-      // The online observer will continue the incremental download by
-      // calling downloadUpdate on the active update which continues
-      // downloading the file from where it was.
-      LOG("Downloader:onStopRequest - offline, register online observer: true");
-      shouldRegisterOnlineObserver = true;
-      deleteActiveUpdate = false;
-    // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, and
-    // NS_ERROR_NET_RESET can be returned when disconnecting the internet while
-    // a download of a MAR is in progress.  There may be others but I have not
-    // encountered them during testing.
-    } else if ((status == Cr.NS_ERROR_NET_TIMEOUT ||
-                status == Cr.NS_ERROR_CONNECTION_REFUSED ||
-                status == Cr.NS_ERROR_NET_RESET) &&
-               this.updateService._consecutiveSocketErrors < maxFail) {
-      LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true");
-      shouldRetrySoon = true;
-      deleteActiveUpdate = false;
-    } else if (status != Cr.NS_BINDING_ABORTED &&
-               status != Cr.NS_ERROR_ABORT &&
-               status != Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
-      LOG("Downloader:onStopRequest - non-verification failure");
-      // Some sort of other failure, log this in the |statusText| property
-      state = STATE_DOWNLOAD_FAILED;
-
-      // XXXben - if |request| (The Incremental Download) provided a means
-      // for accessing the http channel we could do more here.
-
-      this._update.statusText = getStatusTextFromCode(status,
-                                                      Cr.NS_BINDING_FAILED);
+    } else {
+      recordInHealthReport(UpdaterHealthReportFields.FAILED, status);
+
+      if (status == Cr.NS_ERROR_OFFLINE) {
+        // Register an online observer to try again.
+        // The online observer will continue the incremental download by
+        // calling downloadUpdate on the active update which continues
+        // downloading the file from where it was.
+        LOG("Downloader:onStopRequest - offline, register online observer: true");
+        shouldRegisterOnlineObserver = true;
+        deleteActiveUpdate = false;
+      // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, and
+      // NS_ERROR_NET_RESET can be returned when disconnecting the internet while
+      // a download of a MAR is in progress.  There may be others but I have not
+      // encountered them during testing.
+      } else if ((status == Cr.NS_ERROR_NET_TIMEOUT ||
+                  status == Cr.NS_ERROR_CONNECTION_REFUSED ||
+                  status == Cr.NS_ERROR_NET_RESET) &&
+                 this.updateService._consecutiveSocketErrors < maxFail) {
+        LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true");
+        shouldRetrySoon = true;
+        deleteActiveUpdate = false;
+      } else if (status != Cr.NS_BINDING_ABORTED &&
+                 status != Cr.NS_ERROR_ABORT &&
+                 status != Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
+        LOG("Downloader:onStopRequest - non-verification failure");
+        // Some sort of other failure, log this in the |statusText| property
+        state = STATE_DOWNLOAD_FAILED;
+
+        // XXXben - if |request| (The Incremental Download) provided a means
+        // for accessing the http channel we could do more here.
+
+        this._update.statusText = getStatusTextFromCode(status,
+                                                        Cr.NS_BINDING_FAILED);
 
 #ifdef MOZ_WIDGET_GONK
-      // bug891009: On FirefoxOS, manaully retry OTA download will reuse
-      // the Update object. We need to remove selected patch so that download
-      // can be triggered again successfully.
-      this._update.selectedPatch.selected = false;
+        // bug891009: On FirefoxOS, manaully retry OTA download will reuse
+        // the Update object. We need to remove selected patch so that download
+        // can be triggered again successfully.
+        this._update.selectedPatch.selected = false;
 #endif
 
-      // Destroy the updates directory, since we're done with it.
-      cleanUpUpdatesDir();
-
-      deleteActiveUpdate = true;
+        // Destroy the updates directory, since we're done with it.
+        cleanUpUpdatesDir();
+
+        deleteActiveUpdate = true;
+      }
     }
     LOG("Downloader:onStopRequest - setting state to: " + state);
     this._patch.state = state;
     var um = Cc["@mozilla.org/updates/update-manager;1"].
              getService(Ci.nsIUpdateManager);
     if (deleteActiveUpdate) {
       this._update.installDate = (new Date()).getTime();
       um.activeUpdate = null;
--- a/toolkit/mozapps/update/nsUpdateService.manifest
+++ b/toolkit/mozapps/update/nsUpdateService.manifest
@@ -5,8 +5,11 @@ component {093C2356-4843-4C65-8709-D7DBC
 contract @mozilla.org/updates/update-manager;1 {093C2356-4843-4C65-8709-D7DBCBBE7DFB}
 component {898CDC9B-E43F-422F-9CC4-2F6291B415A3} nsUpdateService.js
 contract @mozilla.org/updates/update-checker;1 {898CDC9B-E43F-422F-9CC4-2F6291B415A3}
 component {27ABA825-35B5-4018-9FDD-F99250A0E722} nsUpdateService.js
 contract @mozilla.org/updates/update-prompt;1 {27ABA825-35B5-4018-9FDD-F99250A0E722}
 component {e43b0010-04ba-4da6-b523-1f92580bc150} nsUpdateServiceStub.js
 contract @mozilla.org/updates/update-service-stub;1 {e43b0010-04ba-4da6-b523-1f92580bc150}
 category profile-after-change nsUpdateServiceStub @mozilla.org/updates/update-service-stub;1
+#ifdef MOZ_SERVICES_HEALTHREPORT
+category healthreport-js-provider-default UpdateProvider resource://gre/modules/UpdaterHealthProvider.jsm
+#endif
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/updateHealthReport.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/UpdaterHealthProvider.jsm");
+
+// Create a profile
+let gProfile = do_get_profile();
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_constructor() {
+  let provider = new UpdateProvider();
+  run_next_test();
+});
+
+add_task(function test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new UpdateProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+
+  yield storage.close();
+});
+
+add_task(function test_collect() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new UpdateProvider();
+  yield provider.init(storage);
+
+  let now = new Date();
+
+  let m = provider.getMeasurement("update", 1);
+
+  let fieldcount = 0;
+  for (let field of ["updateCheckStart",
+                     "updateCheckSuccess",
+                     "completeUpdateStart",
+                     "partialUpdateStart",
+                     "completeUpdateSuccess",
+                     "partialUpdateSuccess"]) {
+    fieldcount++; // One new day per iteration
+
+    yield provider.recordUpdate(field, 0);
+
+    let data = yield m.getValues();
+    do_check_eq(data.days.size, 1);
+
+    let day = data.days.getDay(now);
+    do_check_eq(day.size, fieldcount);
+    do_check_eq(day.get(field + "Count"), 1);
+
+    yield provider.recordUpdate(field, 0);
+
+    data = yield m.getValues();
+    day = data.days.getDay(now);
+    do_check_eq(day.size, fieldcount);
+    do_check_eq(day.get(field + "Count"), 2);
+  }
+
+  for (let field of ["updateCheckFailed", "updateFailed"]) {
+    fieldcount += 2; // Two fields added per iteration
+
+    yield provider.recordUpdate(field, 500);
+
+    let data = yield m.getValues();
+    let day = data.days.getDay(now);
+    do_check_eq(day.size, fieldcount);
+
+    do_check_eq(day.get(field + "Statuses"), 500);
+
+    yield provider.recordUpdate(field, 800);
+
+    data = yield m.getValues();
+    day = data.days.getDay(now);
+    do_check_eq(day.size, fieldcount);
+    do_check_eq(day.get(field + "Statuses")[0], 500);
+    do_check_eq(day.get(field + "Statuses")[1], 800);
+  }
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
--- a/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini
+++ b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini
@@ -34,8 +34,10 @@ reason = custom nsIUpdatePrompt
 [uiOnlyAllowOneWindow.js]
 skip-if = toolkit == 'gonk'
 reason = custom nsIUpdatePrompt
 [uiUnsupportedAlreadyNotified.js]
 skip-if = toolkit == 'gonk'
 reason = custom nsIUpdatePrompt
 [updateRootDirMigration_win.js]
 run-if = os == 'win'
+[updateHealthReport.js]
+skip-if = ! healthreport