Bug 958561 - Measure Sync with Firefox Health Report; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Wed, 05 Feb 2014 08:08:14 -0800
changeset 168499 dc26e8d484f8739c00afbaaf9795563959b6f774
parent 168498 881a08901c0db0ef5fd05bd47ace0d35f0b6ee9b
child 168500 7920df861c8a8ea89a0fda63f6dbfcfb3b1cdc22
child 168504 5f3546636bcc88f558435fe150381e40ee9e03dc
push id39736
push userryanvm@gmail.com
push dateThu, 13 Feb 2014 15:31:43 +0000
treeherdermozilla-inbound@11fcd667723d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs958561
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 958561 - Measure Sync with Firefox Health Report; r=rnewman Metrics for Sync have been added to Firefox Health Report. If Sync is not configured, we'll report that fact and the supported and preferred Sync protocols (1.1 or 1.5). If Sync is configured, we report the daily counts of sync attempts and how many are successful vs errored. We also report daily counts of the device types attached to the account.
build/docs/mozinfo.rst
python/mozbuild/mozbuild/mozinfo.py
services/healthreport/docs/dataformat.rst
services/metrics/providermanager.jsm
services/metrics/tests/xpcshell/test_metrics_provider_manager.js
services/sync/Makefile.in
services/sync/SyncComponents.manifest
services/sync/Weave.js
services/sync/modules/engines/clients.js
services/sync/modules/healthreport.jsm
services/sync/moz.build
services/sync/tests/unit/test_healthreport.js
services/sync/tests/unit/test_service_startup.js
services/sync/tests/unit/xpcshell.ini
--- a/build/docs/mozinfo.rst
+++ b/build/docs/mozinfo.rst
@@ -68,29 +68,36 @@ buildapp
 crashreporter
    Whether the crash reporter is enabled for this build.
 
    Values are ``true`` and ``false``.
 
    Always defined.
 
 datareporting
-  Whether data reporting (MOZ_DATA_REPORTING) is enabled for this build.
+   Whether data reporting (MOZ_DATA_REPORTING) is enabled for this build.
 
    Values are ``true`` and ``false``.
 
    Always defined.
 
 debug
    Whether this is a debug build.
 
    Values are ``true`` and ``false``.
 
    Always defined.
 
+healthreport
+   Whether the Health Report feature is enabled.
+
+   Values are ``true`` and ``false``.
+
+   Always defined.
+
 mozconfig
    The path of the :ref:`mozconfig file <mozconfig>` used to produce this build.
 
    Optional.
 
 os
    The operating system the build is produced for. Values for tier-1
    supported platforms are ``linux``, ``win``, ``mac``, ``b2g``, and
@@ -128,8 +135,21 @@ toolkit
 
    Always defined.
 
 topsrcdir
    The path to the source directory the build came from.
 
    Always defined.
 
+wave
+   Whether Wave audio support is enabled.
+
+   Values are ``true`` and ``false``.
+
+   Always defined.
+
+webm
+   Whether WebM support is enabled.
+
+   Values are ``true`` and ``false``.
+
+   Always defined.
--- a/python/mozbuild/mozbuild/mozinfo.py
+++ b/python/mozbuild/mozbuild/mozinfo.py
@@ -73,16 +73,17 @@ def build_dict(config, env=os.environ):
     # hardcoded list of known 32-bit CPUs
     elif p in ["x86", "arm", "ppc"]:
         d["bits"] = 32
     # other CPUs will wind up with unknown bits
 
     d['debug'] = substs.get('MOZ_DEBUG') == '1'
     d['crashreporter'] = bool(substs.get('MOZ_CRASHREPORTER'))
     d['datareporting'] = bool(substs.get('MOZ_DATA_REPORTING'))
+    d['healthreport'] = substs.get('MOZ_SERVICES_HEALTHREPORT') == '1'
     d['asan'] = substs.get('MOZ_ASAN') == '1'
     d['tests_enabled'] = substs.get('ENABLE_TESTS') == "1"
     d['bin_suffix'] = substs.get('BIN_SUFFIX', '')
 
     d['ogg'] = bool(substs.get('MOZ_OGG'))
     d['webm'] = bool(substs.get('MOZ_WEBM'))
     d['wave'] = bool(substs.get('MOZ_WAVE'))
 
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1308,16 +1308,84 @@ Example
 ::
 
     "org.mozilla.searches.counts": {
       "_v": 1,
       "google.searchbar": 3,
       "google.urlbar": 7
     },
 
+org.mozilla.sync.sync
+---------------------
+
+This daily measurement contains information about the Sync service.
+
+Values should be recorded for every day FHR measurements occurred.
+
+Version 1
+^^^^^^^^^
+
+This version debuted with Firefox 30 on desktop. It contains the following
+properties:
+
+enabled
+   Daily numeric indicating whether Sync is configured and enabled. 1 if so,
+   0 otherwise.
+
+preferredProtocol
+   String version of the maximum Sync protocol version the client supports.
+   This will be ``1.1`` for for legacy Sync and ``1.5`` for clients that
+   speak the Firefox Accounts protocol.
+
+actualProtocol
+   The actual Sync protocol version the client is configured to use.
+
+   This will be ``1.1`` if the client is configured with the legacy Sync
+   service or if the client only supports ``1.1``.
+
+   It will be ``1.5`` if the client supports ``1.5`` and either a) the
+   client is not configured b) the client is using Firefox Accounts Sync.
+
+syncStart
+   Count of sync operations performed.
+
+syncSuccess
+   Count of sync operations that completed successfully.
+
+syncError
+   Count of sync operations that did not complete successfully.
+
+   This is a measure of overall sync success. This does *not* reflect
+   recoverable errors (such as record conflict) that can occur during
+   sync. This is thus a rough proxy of whether the sync service is
+   operating without error.
+
+org.mozilla.sync.devices
+------------------------
+
+This daily measurement contains information about the device type composition
+for the configured Sync account.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 30.
+
+Field names are dynamic according to the client-reported device types from
+Sync records. All fields are daily last seen integer values corresponding to
+the number of devices of that type.
+
+Common values include:
+
+desktop
+   Corresponds to a Firefox desktop client.
+
+mobile
+   Corresponds to a Fennec client.
+
 org.mozilla.sysinfo.sysinfo
 ---------------------------
 
 This measurement contains basic information about the system the application
 is running on.
 
 Version 2
 ^^^^^^^^^
--- a/services/metrics/providermanager.jsm
+++ b/services/metrics/providermanager.jsm
@@ -155,20 +155,24 @@ this.ProviderManager.prototype = Object.
    * provider.init().
    *
    * @param provider
    *        (Metrics.Provider) The provider instance to register.
    *
    * @return Promise<null>
    */
   registerProvider: function (provider) {
-    if (!(provider instanceof Provider)) {
-      throw new Error("Argument must be a Provider instance.");
+    // We should perform an instanceof check here. However, due to merged
+    // compartments, the Provider type may belong to one of two JSMs
+    // isinstance gets confused depending on which module Provider comes
+    // from. Some code references Provider from dataprovider.jsm; others from
+    // Metrics.jsm.
+    if (!provider.name) {
+      throw new Error("Provider is not valid: does not have a name.");
     }
-
     if (this._providers.has(provider.name)) {
       return CommonUtils.laterTickResolvingPromise();
     }
 
     let deferred = Promise.defer();
     this._providerInitQueue.push([provider, deferred]);
 
     if (this._providerInitQueue.length == 1) {
--- a/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
+++ b/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
@@ -42,17 +42,17 @@ add_task(function test_register_provider
   yield manager.registerProvider(dummy);
   do_check_eq(manager._providers.size, 1);
   do_check_eq(manager.getProvider(dummy.name), dummy);
 
   let failed = false;
   try {
     manager.registerProvider({});
   } catch (ex) {
-    do_check_true(ex.message.startsWith("Argument must be a Provider"));
+    do_check_true(ex.message.startsWith("Provider is not valid"));
     failed = true;
   } finally {
     do_check_true(failed);
     failed = false;
   }
 
   manager.unregisterProvider(dummy.name);
   do_check_eq(manager._providers.size, 0);
--- a/services/sync/Makefile.in
+++ b/services/sync/Makefile.in
@@ -15,16 +15,17 @@ SYNC_PP_PATH = $(FINAL_TARGET)/modules/s
 PP_TARGETS += SYNC_PP
 
 # The set of core JavaScript modules for Sync. These are copied as-is.
 sync_modules := \
   addonsreconciler.js \
   addonutils.js \
   browserid_identity.js \
   engines.js \
+  healthreport.jsm \
   identity.js \
   jpakeclient.js \
   keys.js \
   main.js \
   notifications.js \
   policies.js \
   record.js \
   resource.js \
--- a/services/sync/SyncComponents.manifest
+++ b/services/sync/SyncComponents.manifest
@@ -18,8 +18,12 @@ component {74b89fb0-f200-4ae8-a3ec-dd164
 contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de}
 category app-startup WeaveService service,@mozilla.org/weave/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
 component {d28f8a0b-95da-48f4-b712-caf37097be41} Weave.js
 contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41}
 
 # Register resource aliases
 # (Note, for tests these are also set up in addResourceAlias)
 resource services-sync resource://gre/modules/services-sync/
+
+#ifdef MOZ_SERVICES_HEALTHREPORT
+category healthreport-js-provider-default SyncProvider resource://services-sync/healthreport.jsm
+#endif
--- a/services/sync/Weave.js
+++ b/services/sync/Weave.js
@@ -87,16 +87,21 @@ WeaveService.prototype = {
     Services.obs.addObserver(function onReady() {
       Services.obs.removeObserver(onReady, "weave:service:ready");
       deferred.resolve();
     }, "weave:service:ready", false);
     this.ensureLoaded();
     return deferred.promise;
   },
 
+  /**
+   * Whether Firefox Accounts is enabled.
+   *
+   * @return bool
+   */
   get fxAccountsEnabled() {
     // work out what identity manager to use.  This is stored in a preference;
     // if the preference exists, we trust it.
     let fxAccountsEnabled;
     try {
       fxAccountsEnabled = Services.prefs.getBoolPref("services.sync.fxaccounts.enabled");
     } catch (_) {
       // That pref doesn't exist - so let's assume this is a first-run.
@@ -107,16 +112,31 @@ WeaveService.prototype = {
       Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", fxAccountsEnabled);
     }
     // Currently we don't support toggling this pref after initialization -
     // except when sync is reset - but this 1 exception is enough that we can't
     // cache the value.
     return fxAccountsEnabled;
   },
 
+  /**
+   * Whether Sync appears to be enabled.
+   *
+   * This returns true if all the Sync preferences for storing account
+   * and server configuration are populated.
+   *
+   * It does *not* perform a robust check to see if the client is working.
+   * For that, you'll want to check Weave.Status.checkSetup().
+   */
+  get enabled() {
+    let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
+    return prefs.prefHasUserValue("username") &&
+           prefs.prefHasUserValue("clusterURL");
+  },
+
   observe: function (subject, topic, data) {
     switch (topic) {
     case "app-startup":
       let os = Cc["@mozilla.org/observer-service;1"].
                getService(Ci.nsIObserverService);
       os.addObserver(this, "final-ui-startup", true);
       break;
 
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -66,16 +66,38 @@ ClientEngine.prototype = {
       stats.hasMobile = stats.hasMobile || type == "mobile";
       stats.names.push(name);
       stats.numClients++;
     }
 
     return stats;
   },
 
+  /**
+   * Obtain information about device types.
+   *
+   * Returns a Map of device types to integer counts.
+   */
+  get deviceTypes() {
+    let counts = new Map();
+
+    counts.set(this.localType, 1);
+
+    for each (let record in this._store._remoteClients) {
+      let type = record.type;
+      if (!counts.has(type)) {
+        counts.set(type, 0);
+      }
+
+      counts.set(type, counts.get(type) + 1);
+    }
+
+    return counts;
+  },
+
   get localID() {
     // Generate a random GUID id we don't have one
     let localID = Svc.Prefs.get("client.GUID", "");
     return localID == "" ? this.localID = Utils.makeGUID() : localID;
   },
   set localID(value) Svc.Prefs.set("client.GUID", value),
 
   get localName() {
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/healthreport.jsm
@@ -0,0 +1,180 @@
+/* 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 = [
+  "SyncProvider",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
+const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+                                  "resource://services-sync/main.js");
+
+function SyncMeasurement1() {
+  Metrics.Measurement.call(this);
+}
+
+SyncMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "sync",
+  version: 1,
+
+  fields: {
+    enabled: DAILY_LAST_NUMERIC_FIELD,
+    preferredProtocol: DAILY_LAST_TEXT_FIELD,
+    activeProtocol: DAILY_LAST_TEXT_FIELD,
+    syncStart: DAILY_COUNTER_FIELD,
+    syncSuccess: DAILY_COUNTER_FIELD,
+    syncError: DAILY_COUNTER_FIELD,
+  },
+});
+
+function SyncDevicesMeasurement1() {
+  Metrics.Measurement.call(this);
+}
+
+SyncDevicesMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "devices",
+  version: 1,
+
+  fields: {},
+
+  shouldIncludeField: function (name) {
+    return true;
+  },
+
+  fieldType: function (name) {
+    return Metrics.Storage.FIELD_DAILY_COUNTER;
+  },
+});
+
+this.SyncProvider = function () {
+  Metrics.Provider.call(this);
+};
+SyncProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.sync",
+
+  measurementTypes: [
+    SyncDevicesMeasurement1,
+    SyncMeasurement1,
+  ],
+
+  _OBSERVERS: [
+    "weave:service:sync:start",
+    "weave:service:sync:finish",
+    "weave:service:sync:error",
+  ],
+
+  postInit: function () {
+    for (let o of this._OBSERVERS) {
+      Services.obs.addObserver(this, o, false);
+    }
+
+    return Promise.resolve();
+  },
+
+  onShutdown: function () {
+    for (let o of this._OBSERVERS) {
+      Services.obs.removeObserver(this, o);
+    }
+
+    return Promise.resolve();
+  },
+
+  observe: function (subject, topic, data) {
+    let field;
+    switch (topic) {
+      case "weave:service:sync:start":
+        field = "syncStart";
+        break;
+
+      case "weave:service:sync:finish":
+        field = "syncSuccess";
+        break;
+
+      case "weave:service:sync:error":
+        field = "syncError";
+        break;
+    }
+
+    let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+                                SyncMeasurement1.prototype.version);
+    return this.enqueueStorageOperation(function recordSyncEvent() {
+      return m.incrementDailyCounter(field);
+    });
+  },
+
+  collectDailyData: function () {
+    return this.storage.enqueueTransaction(this._populateDailyData.bind(this));
+  },
+
+  _populateDailyData: function* () {
+    let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+                                SyncMeasurement1.prototype.version);
+
+    let svc = Cc["@mozilla.org/weave/service;1"]
+                .getService(Ci.nsISupports)
+                .wrappedJSObject;
+
+    let enabled = svc.enabled;
+    yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
+
+    // preferredProtocol is constant and only changes as the client
+    // evolves.
+    yield m.setDailyLastText("preferredProtocol", "1.5");
+
+    let protocol = svc.fxAccountsEnabled ? "1.5" : "1.1";
+    yield m.setDailyLastText("activeProtocol", protocol);
+
+    if (!enabled) {
+      return;
+    }
+
+    // Before grabbing more information, be sure the Sync service
+    // is fully initialized. This has the potential to initialize
+    // Sync on the spot. This may be undesired if Sync appears to
+    // be enabled but it really isn't. That responsibility should
+    // be up to svc.enabled to not return false positives, however.
+    yield svc.whenLoaded();
+
+    if (Weave.Status.service != Weave.STATUS_OK) {
+      return;
+    }
+
+    // Device types are dynamic. So we need to dynamically create fields if
+    // they don't exist.
+    let dm = this.getMeasurement(SyncDevicesMeasurement1.prototype.name,
+                                 SyncDevicesMeasurement1.prototype.version);
+    let devices = Weave.Service.clientsEngine.deviceTypes;
+    for (let [field, count] of devices) {
+      let hasField = this.storage.hasFieldFromMeasurement(dm.id, field,
+                                    this.storage.FIELD_DAILY_LAST_NUMERIC);
+      let fieldID;
+      if (hasField) {
+        fieldID = this.storage.fieldIDFromMeasurement(dm.id, field);
+      } else {
+        fieldID = yield this.storage.registerField(dm.id, field,
+                                       this.storage.FIELD_DAILY_LAST_NUMERIC);
+      }
+
+      yield this.storage.setDailyLastNumericFromFieldID(fieldID, count);
+    }
+  },
+});
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -3,11 +3,14 @@
 # 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/.
 
 DIRS += ['locales']
 TEST_DIRS += ['tests']
 
 EXTRA_COMPONENTS += [
-    'SyncComponents.manifest',
     'Weave.js',
 ]
+
+EXTRA_PP_COMPONENTS += [
+    'SyncComponents.manifest',
+]
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_healthreport.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://services-sync/main.js", this);
+Cu.import("resource://services-sync/healthreport.jsm", this);
+Cu.import("resource://testing-common/services-common/logging.js", this);
+Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
+
+function run_test() {
+  initTestLogging();
+
+  // A head JS file always sets the
+  // services.sync.fxaccounts.enabled pref. This prevents us from testing
+  // pristine profile conditions and likely indicates there isn't test
+  // coverage of the Sync service's fxAccountsEnabled property. Check
+  // that pre-condition and hack around it.
+  let branch = new Preferences("services.sync.");
+  Assert.ok(branch.isSet("fxaccounts.enabled"), "Check precondition");
+  branch.reset("fxaccounts.enabled");
+
+  run_next_test();
+}
+
+add_task(function test_constructor() {
+  let provider = new SyncProvider();
+});
+
+// Provider can initialize and de-initialize properly.
+add_task(function* test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function* test_collect() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  // Initially nothing should be configured.
+  let now = new Date();
+  yield provider.collectDailyData();
+
+  let m = provider.getMeasurement("sync", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("enabled"));
+  Assert.ok(day.has("activeProtocol"));
+  Assert.ok(day.has("preferredProtocol"));
+  Assert.equal(day.get("enabled"), 0);
+  Assert.equal(day.get("preferredProtocol"), "1.5");
+  Assert.equal(day.get("activeProtocol"), "1.5",
+               "Protocol without setup should be FX Accounts version.");
+
+  // Now check for old Sync setup.
+  let branch = new Preferences("services.sync.");
+  branch.set("username", "foo");
+  branch.reset("fxaccounts.enabled");
+  yield provider.collectDailyData();
+  values = yield m.getValues();
+  Assert.equal(values.days.getDay(now).get("activeProtocol"), "1.1",
+               "Protocol with old Sync setup is correct.");
+
+  Assert.equal(Weave.Status.__authManager, undefined, "Detect code changes");
+
+  // Let's enable Sync so we can get more useful data.
+  // We need to do this because the FHR probe only records more info if Sync
+  // is configured properly.
+  Weave.Service.identity.account = "johndoe";
+  Weave.Service.identity.basicPassword = "ilovejane";
+  Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
+  Weave.Service.clusterURL = "http://localhost/";
+  Assert.equal(Weave.Status.checkSetup(), Weave.STATUS_OK);
+
+  yield provider.collectDailyData();
+  values = yield m.getValues();
+  day = values.days.getDay(now);
+  Assert.equal(day.get("enabled"), 1);
+
+  // An empty account should have 1 device: us.
+  let dm = provider.getMeasurement("devices", 1);
+  values = yield dm.getValues();
+  Assert.ok(values.days.hasDay(now));
+  day = values.days.getDay(now);
+  Assert.equal(day.size, 1);
+  let engine = Weave.Service.clientsEngine;
+  Assert.ok(engine);
+  Assert.ok(day.has(engine.localType));
+  Assert.equal(day.get(engine.localType), 1);
+
+  // Add some devices and ensure they show up.
+  engine._store._remoteClients["id1"] = {type: "mobile"};
+  engine._store._remoteClients["id2"] = {type: "tablet"};
+  engine._store._remoteClients["id3"] = {type: "mobile"};
+
+  yield provider.collectDailyData();
+  values = yield dm.getValues();
+  day = values.days.getDay(now);
+
+  let expected = {
+    "foobar": 0,
+    "tablet": 1,
+    "mobile": 2,
+    "desktop": 0,
+  };
+
+  for (let type in expected) {
+    let count = expected[type];
+
+    if (engine.localType == type) {
+      count++;
+    }
+
+    if (!count) {
+      Assert.ok(!day.has(type));
+    } else {
+      Assert.ok(day.has(type));
+      Assert.equal(day.get(type), count);
+    }
+  }
+
+  engine._store._remoteClients = {};
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function* test_sync_events() {
+  let storage = yield Metrics.Storage("sync_events");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  let m = provider.getMeasurement("sync", 1);
+
+  for (let i = 0; i < 5; i++) {
+    Services.obs.notifyObservers(null, "weave:service:sync:start", null);
+  }
+
+  for (let i = 0; i < 3; i++) {
+    Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
+  }
+
+  for (let i = 0; i < 2; i++) {
+    Services.obs.notifyObservers(null, "weave:service:sync:error", null);
+  }
+
+  // Wait for storage to complete.
+  yield m.storage.enqueueOperation(() => {
+    return Promise.resolve();
+  });
+
+  let values = yield m.getValues();
+  let now = new Date();
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+
+  Assert.ok(day.has("syncStart"));
+  Assert.ok(day.has("syncSuccess"));
+  Assert.ok(day.has("syncError"));
+  Assert.equal(day.get("syncStart"), 5);
+  Assert.equal(day.get("syncSuccess"), 3);
+  Assert.equal(day.get("syncError"), 2);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function* test_healthreporter_json() {
+  let reporter = yield getHealthReporter("healthreporter_json");
+  yield reporter.init();
+  try {
+    yield reporter._providerManager.registerProvider(new SyncProvider());
+    yield reporter.collectMeasurements();
+    let payload = yield reporter.getJSONPayload(true);
+    let now = new Date();
+    let today = reporter._formatDate(now);
+
+    Assert.ok(today in payload.data.days);
+    let day = payload.data.days[today];
+
+    Assert.ok("org.mozilla.sync.sync" in day);
+    Assert.ok("org.mozilla.sync.devices" in day);
+
+    let devices = day["org.mozilla.sync.devices"];
+    let engine = Weave.Service.clientsEngine;
+    Assert.ok(engine);
+    let type = engine.localType;
+    Assert.ok(type);
+    Assert.ok(type in devices);
+    Assert.equal(devices[type], 1);
+  } finally {
+    reporter._shutdown();
+  }
+});
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -8,39 +8,47 @@ Cu.import("resource://testing-common/ser
 
 Svc.Prefs.set("registerEngines", "Tab,Bookmarks,Form,History");
 Cu.import("resource://services-sync/service.js");
 
 function run_test() {
   _("When imported, Service.onStartup is called");
   initTestLogging("Trace");
 
+  let xps = Cc["@mozilla.org/weave/service;1"]
+              .getService(Ci.nsISupports)
+              .wrappedJSObject;
+  do_check_false(xps.enabled);
+
   // Test fixtures
   Service.identity.username = "johndoe";
+  do_check_false(xps.enabled);
 
   Cu.import("resource://services-sync/service.js");
 
   _("Service is enabled.");
   do_check_eq(Service.enabled, true);
 
   _("Engines are registered.");
   let engines = Service.engineManager.getAll();
   do_check_true(Utils.deepEquals([engine.name for each (engine in engines)],
                                  ['tabs', 'bookmarks', 'forms', 'history']));
 
   _("Observers are notified of startup");
   do_test_pending();
 
-  let xps = Cc["@mozilla.org/weave/service;1"]
-              .getService(Ci.nsISupports)
-              .wrappedJSObject;
-
   do_check_false(Service.status.ready);
   do_check_false(xps.ready);
   Observers.add("weave:service:ready", function (subject, data) {
     do_check_true(Service.status.ready);
     do_check_true(xps.ready);
 
     // Clean up.
     Svc.Prefs.resetBranch("");
     do_test_finished();
   });
+
+  do_check_false(xps.enabled);
+
+  Service.identity.account = "johndoe";
+  Service.clusterURL = "http://localhost/";
+  do_check_true(xps.enabled);
 }
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -159,8 +159,11 @@ skip-if = debug
 [test_password_tracker.js]
 # Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
 skip-if = debug
 [test_prefs_store.js]
 [test_prefs_tracker.js]
 [test_tab_engine.js]
 [test_tab_store.js]
 [test_tab_tracker.js]
+
+[test_healthreport.js]
+skip-if = ! healthreport