From d862c81fab969e7443a651f2fe28b4feda461c94 Mon Sep 17 00:00:00 2001
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 04 Dec 2014 12:09:34 -0800
changeset 249601 47a93c546ccf24c25932d61e5ac5aa8c1a5180ec
parent 249600 728fd3c8cac4c5110609c3d3d2ae0bfb2fd55a43
child 249602 06736b6f84c533ba16ca2873ea3f4c26d4878cc7
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1097406, 100644
milestone37.0a2
From d862c81fab969e7443a651f2fe28b4feda461c94 Mon Sep 17 00:00:00 2001 Bug 1097406 - FHR data for sync migration. r=adw,gfritzsche a=lmandel From d862c81fab969e7443a651f2fe28b4feda461c94 Mon Sep 17 00:00:00 2001 --- browser/base/content/browser-fxaccounts.js | 26 +++- browser/components/preferences/in-content/sync.js | 1 + services/healthreport/docs/dataformat.rst | 39 +++++ services/sync/modules/FxaMigrator.jsm | 48 +++++- services/sync/modules/healthreport.jsm | 82 ++++++++++ .../sync/tests/unit/test_healthreport_migration.js | 165 +++++++++++++++++++++ services/sync/tests/unit/xpcshell.ini | 3 + 7 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 services/sync/tests/unit/test_healthreport_migration.js
browser/base/content/browser-fxaccounts.js
browser/components/preferences/in-content/sync.js
services/healthreport/docs/dataformat.rst
services/sync/modules/FxaMigrator.jsm
services/sync/modules/healthreport.jsm
services/sync/tests/unit/test_healthreport_migration.js
services/sync/tests/unit/xpcshell.ini
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -5,16 +5,21 @@
 let gFxAccounts = {
 
   PREF_SYNC_START_DOORHANGER: "services.sync.ui.showSyncStartDoorhanger",
   DOORHANGER_ACTIVATE_DELAY_MS: 5000,
   SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration",
 
   _initialized: false,
   _inCustomizationMode: false,
+  // _expectingNotifyClose is a hack that helps us determine if the
+  // migration notification was closed due to being "dismissed" vs closed
+  // due to one of the migration buttons being clicked.  It's ugly and somewhat
+  // fragile, so bug 1119020 exists to help us do this better.
+  _expectingNotifyClose: false,
 
   get weave() {
     delete this.weave;
     return this.weave = Cc["@mozilla.org/weave/service;1"]
                           .getService(Ci.nsISupports)
                           .wrappedJSObject;
   },
 
@@ -23,17 +28,18 @@ let gFxAccounts = {
     delete this.topics;
     return this.topics = [
       "weave:service:ready",
       "weave:service:sync:start",
       "weave:service:login:error",
       "weave:service:setup-complete",
       "fxa-migration:state-changed",
       this.FxAccountsCommon.ONVERIFIED_NOTIFICATION,
-      this.FxAccountsCommon.ONLOGOUT_NOTIFICATION
+      this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
+      "weave:notification:removed",
     ];
   },
 
   get button() {
     delete this.button;
     return this.button = document.getElementById("PanelUI-fxa-status");
   },
 
@@ -105,16 +111,26 @@ let gFxAccounts = {
         Services.prefs.setBoolPref(this.PREF_SYNC_START_DOORHANGER, true);
         break;
       case "weave:service:sync:start":
         this.onSyncStart();
         break;
       case "fxa-migration:state-changed":
         this.onMigrationStateChanged(data, subject);
         break;
+      case "weave:notification:removed":
+        // this exists just so we can tell the difference between "box was
+        // closed due to button press" vs "was closed due to click on [x]"
+        let notif = subject.wrappedJSObject.object;
+        if (notif.title == this.SYNC_MIGRATION_NOTIFICATION_TITLE &&
+            !this._expectingNotifyClose) {
+          // it's an [x] on our notification, so record telemetry.
+          this.fxaMigrator.recordTelemetry(this.fxaMigrator.TELEMETRY_DECLINED);
+        }
+        break;
       default:
         this.updateUI();
         break;
     }
   },
 
   onSyncStart: function () {
     if (!this.isActiveWindow) {
@@ -258,17 +274,23 @@ let gFxAccounts = {
     }
     this.button.label = label;
     this.button.hidden = false;
     this.button.setAttribute("fxastatus", status);
   }),
 
   updateMigrationNotification: Task.async(function* () {
     if (!this._migrationInfo) {
+      this._expectingNotifyClose = true;
       Weave.Notifications.removeAll(this.SYNC_MIGRATION_NOTIFICATION_TITLE);
+      // because this is called even when there is no such notification, we
+      // set _expectingNotifyClose back to false as we may yet create a new
+      // notification (but in general, once we've created a migration
+      // notification once in a session, we don't create one again)
+      this._expectingNotifyClose = false;
       return;
     }
     let note = null;
     switch (this._migrationInfo.state) {
       case this.fxaMigrator.STATE_USER_FXA: {
         // There are 2 cases here - no email address means it is an offer on
         // the first device (so the user is prompted to create an account).
         // If there is an email address it is the "join the party" flow, so the
@@ -283,16 +305,17 @@ let gFxAccounts = {
         } else {
           msg = this.strings.GetStringFromName("needUserLong");
           upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label");
           upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey");
         }
         note = new Weave.Notification(
           undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [
             new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => {
+              this._expectingNotifyClose = true;
               this.fxaMigrator.createFxAccount(window);
             }),
           ]
         );
         break;
       }
       case this.fxaMigrator.STATE_USER_FXA_VERIFIED: {
         let msg =
@@ -300,16 +323,17 @@ let gFxAccounts = {
                                             [this._migrationInfo.email], 1);
         let resendLabel =
           this.strings.GetStringFromName("resendVerificationEmail.label");
         let resendAccessKey =
           this.strings.GetStringFromName("resendVerificationEmail.accessKey");
         note = new Weave.Notification(
           undefined, msg, undefined, Weave.Notifications.PRIORITY_INFO, [
             new Weave.NotificationButton(resendLabel, resendAccessKey, () => {
+              this._expectingNotifyClose = true;
               this.fxaMigrator.resendVerificationMail();
             }),
           ]
         );
         break;
       }
     }
     note.title = this.SYNC_MIGRATION_NOTIFICATION_TITLE;
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -398,16 +398,17 @@ let gSyncPane = {
                                 flags,
                                 sb.GetStringFromName("unlinkVerificationConfirm"),
                                 null, null, null, {});
 
     // If the user selects cancel, just bail
     if (buttonChoice == 1)
       return;
 
+    fxaMigrator.recordTelemetry(fxaMigrator.TELEMETRY_UNLINKED);
     Weave.Service.startOver();
     this.updateWeavePrefs();
   },
 
   updatePass: function () {
     if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED)
       gSyncUtils.changePassword();
     else
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1633,16 +1633,55 @@ 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.sync.migration
+--------------------------
+
+This daily measurement contains information about sync migration (that is, the
+semi-automated process of migrating a legacy sync account to an FxA account.)
+
+Measurements will start being recorded after a migration is offered by the
+sync server and stop after migration is complete or the user elects to "unlink"
+their sync account.  In other words, it is expected that users with Sync setup
+for FxA or with sync unconfigured will not collect data, and that for users
+where data is collected, the collection will only be for a relatively short
+period.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 37 and includes the following properties:
+
+state
+   Corresponds to either a STATE_USER_* string or a STATE_INTERNAL_* string in
+   FxaMigration.jsm.  This reflects a state where we are waiting for the user,
+   or waiting for some internal process to complete on the way to completing
+   the migration.
+
+declined
+    Corresponds to the number of times the user closed the migration infobar.
+
+unlinked
+    Set if the user declined to migrate and instead "unlinked" Sync from the
+    browser.
+
+accepted
+    Corresponds to the number of times the user explicitly elected to start or
+    continue the migration - it counts how often the user clicked on any UI
+    created specifically for migration. The "ideal" UX for migration would see
+    this at exactly 1, some known edge-cases (eg, browser restart required to
+    finish) could expect this to be 2, and anything more means we are doing
+    something wrong.
+
 org.mozilla.sysinfo.sysinfo
 ---------------------------
 
 This measurement contains basic information about the system the application
 is running on.
 
 Version 2
 ^^^^^^^^^
--- a/services/sync/modules/FxaMigrator.jsm
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -22,23 +22,31 @@ XPCOMUtils.defineLazyGetter(this, "Weave
 
 XPCOMUtils.defineLazyModuleGetter(this, "Weave",
   "resource://services-sync/main.js");
 
 // FxAccountsCommon.js doesn't use a "namespace", so create one here.
 let fxAccountsCommon = {};
 Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
 
-// We send this notification whenever the migration state changes.
+// We send this notification whenever the "user" migration state changes.
 const OBSERVER_STATE_CHANGE_TOPIC = "fxa-migration:state-changed";
 // We also send the state notification when we *receive* this.  This allows
 // consumers to avoid loading this module until it receives a notification
 // from us (which may never happen if there's no migration to do)
 const OBSERVER_STATE_REQUEST_TOPIC = "fxa-migration:state-request";
 
+// We send this notification whenever the migration is paused waiting for
+// something internal to complete.
+const OBSERVER_INTERNAL_STATE_CHANGE_TOPIC = "fxa-migration:internal-state-changed";
+
+// We use this notification so Sync's healthreport module can record telemetry
+// (actually via "health report") for us.
+const OBSERVER_INTERNAL_TELEMETRY_TOPIC = "fxa-migration:internal-telemetry";
+
 const OBSERVER_TOPICS = [
   "xpcom-shutdown",
   "weave:service:sync:start",
   "weave:service:sync:finish",
   "weave:service:sync:error",
   "weave:eol",
   OBSERVER_STATE_REQUEST_TOPIC,
   fxAccountsCommon.ONLOGIN_NOTIFICATION,
@@ -81,16 +89,29 @@ Migrator.prototype = {
   // either. (a) no migration is necessary or (b) that the migrator module is
   // waiting for something outside of the user's control - eg, sync to complete,
   // the migration sentinel to be uploaded, etc.  In most cases the wait will be
   // short, but edge cases (eg, no network, sync bugs that prevent it stopping
   // until shutdown) may require a significantly longer wait.
   STATE_USER_FXA: "waiting for user to be signed in to FxA",
   STATE_USER_FXA_VERIFIED: "waiting for a verified FxA user",
 
+  // What internal state are we at?  This is primarily used for FHR reporting so
+  // we can determine why exactly we might be stalled.
+  STATE_INTERNAL_WAITING_SYNC_COMPLETE: "waiting for sync to complete",
+  STATE_INTERNAL_WAITING_WRITE_SENTINEL: "waiting for sentinel to be written",
+  STATE_INTERNAL_WAITING_START_OVER: "waiting for sync to reset itself",
+  STATE_INTERNAL_COMPLETE: "migration complete",
+
+  // Flags for the telemetry we record.  The UI will call a helper to record
+  // the fact some UI was interacted with.
+  TELEMETRY_ACCEPTED: "accepted",
+  TELEMETRY_DECLINED: "declined",
+  TELEMETRY_UNLINKED: "unlinked",
+
   finalize() {
     for (let topic of OBSERVER_TOPICS) {
       Services.obs.removeObserver(this, topic);
     }
   },
 
   observe(subject, topic, data) {
     this.log.debug("observed " + topic);
@@ -194,20 +215,24 @@ Migrator.prototype = {
     // We need to disable sync from automatically starting,
     // and if we are currently syncing wait for it to complete.
     this._blockSync();
 
     // Are we currently syncing?
     if (Weave.Service._locked) {
       // our observers will kick us further along when complete.
       this.log.info("waiting for sync to complete")
+      Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+                                   this.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
       return null;
     }
 
     // Write the migration sentinel if necessary.
+    Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+                                 this.STATE_INTERNAL_WAITING_WRITE_SENTINEL);
     yield this._setMigrationSentinelIfNecessary();
 
     // Get the list of enabled engines to we can restore that state.
     let enginePrefs = this._getEngineEnabledPrefs();
 
     // Must be ready to perform the actual migration.
     this.log.info("Performing final sync migration steps");
     // Do the actual migration.  We setup one observer for when the new identity
@@ -244,30 +269,34 @@ Migrator.prototype = {
         this.log.info("observed that startOver is complete");
         Services.obs.removeObserver(observe, "weave:service:start-over:finish");
         resolve();
       }, "weave:service:start-over:finish", false);
     });
 
     Weave.Service.startOver();
     // need to wait for an observer.
+    Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+                                 this.STATE_INTERNAL_WAITING_START_OVER);
     yield startOverComplete;
     // observer fired, now kick things off with the FxA user.
     this.log.info("scheduling initial FxA sync.");
     // Note we technically don't need to unblockSync as by now all sync prefs
     // have been reset - but it doesn't hurt.
     this._unblockSync();
     Weave.Service.scheduler.scheduleNextSync(0);
 
     // Tell the front end that migration is now complete -- Sync is now
     // configured with an FxA user.
     forceObserver = true;
     this.log.info("Migration complete");
     update(null);
 
+    Services.obs.notifyObservers(null, OBSERVER_INTERNAL_STATE_CHANGE_TOPIC,
+                                 this.STATE_INTERNAL_COMPLETE);
     return null;
   }),
 
   /* Return an object with the preferences we care about */
   _getSentinelPrefs() {
     let result = {};
     for (let pref of FXA_SENTINEL_PREFS) {
       if (Services.prefs.prefHasUserValue(pref)) {
@@ -420,16 +449,19 @@ Migrator.prototype = {
     let tail = email ? "&email=" + encodeURIComponent(email) : "";
     // A special flag so server-side metrics can tell this is part of migration.
     tail += "&migration=sync11";
     // We want to ask FxA to offer a "Customize Sync" checkbox iff any engines
     // are disabled.
     let customize = !this._allEnginesEnabled();
     tail += "&customizeSync=" + customize;
 
+    // We assume the caller of this is going to actually use it, so record
+    // telemetry now.
+    this.recordTelemetry(this.TELEMETRY_ACCEPTED);
     return {
       url: "about:accounts?action=" + action + tail,
       options: {ignoreFragment: true, replaceQueryString: true}
     };
   }),
 
   // Ask the FxA servers to re-send a verification mail for the currently
   // logged in user. This should only be called while we are in the
@@ -443,16 +475,17 @@ Migrator.prototype = {
     }
     let ok = true;
     try {
       yield fxAccounts.resendVerificationEmail();
     } catch (ex) {
       this.log.error("Failed to resend verification mail: ${}", ex);
       ok = false;
     }
+    this.recordTelemetry(this.TELEMETRY_ACCEPTED);
     let fxauser = yield fxAccounts.getSignedInUser();
     let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
 
     let heading = ok ?
                   sb.formatStringFromName("verificationSentHeading", [fxauser.email], 1) :
                   sb.GetStringFromName("verificationNotSentHeading");
     let title = sb.GetStringFromName(ok ? "verificationSentTitle" : "verificationNotSentTitle");
     let description = sb.GetStringFromName(ok ? "verificationSentDescription"
@@ -474,13 +507,26 @@ Migrator.prototype = {
   forgetFxAccount: Task.async(function * () {
     // warn if we aren't in the expected state - but go ahead anyway!
     if (this._state != this.STATE_USER_FXA_VERIFIED) {
       this.log.warn("forgetFxAccount called in an unexpected state: ${}", this._state);
     }
     return fxAccounts.signOut();
   }),
 
+  recordTelemetry(flag) {
+    // Note the value is the telemetry field name - but this is an
+    // implementation detail which could be changed later.
+    switch (flag) {
+      case this.TELEMETRY_ACCEPTED:
+      case this.TELEMETRY_UNLINKED:
+      case this.TELEMETRY_DECLINED:
+        Services.obs.notifyObservers(null, OBSERVER_INTERNAL_TELEMETRY_TOPIC, flag);
+        break;
+      default:
+        throw new Error("Unexpected telemetry flag: " + flag);
+    }
+  }
 }
 
 // We expose a singleton
 this.EXPORTED_SYMBOLS = ["fxaMigrator"];
 let fxaMigrator = new Migrator();
--- a/services/sync/modules/healthreport.jsm
+++ b/services/sync/modules/healthreport.jsm
@@ -58,33 +58,55 @@ SyncDevicesMeasurement1.prototype = Obje
     return true;
   },
 
   fieldType: function (name) {
     return Metrics.Storage.FIELD_DAILY_COUNTER;
   },
 });
 
+function SyncMigrationMeasurement1() {
+  Metrics.Measurement.call(this);
+}
+
+SyncMigrationMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "migration",
+  version: 1,
+
+  fields: {
+    state: DAILY_LAST_TEXT_FIELD, // last "user" or "internal" state we saw for the day
+    accepted: DAILY_COUNTER_FIELD, // number of times user tried to start migration
+    declined: DAILY_COUNTER_FIELD, // number of times user closed nagging infobar
+    unlinked: DAILY_LAST_NUMERIC_FIELD, // did the user decline and unlink
+  },
+});
+
 this.SyncProvider = function () {
   Metrics.Provider.call(this);
 };
 SyncProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.sync",
 
   measurementTypes: [
     SyncDevicesMeasurement1,
     SyncMeasurement1,
+    SyncMigrationMeasurement1,
   ],
 
   _OBSERVERS: [
     "weave:service:sync:start",
     "weave:service:sync:finish",
     "weave:service:sync:error",
+    "fxa-migration:state-changed",
+    "fxa-migration:internal-state-changed",
+    "fxa-migration:internal-telemetry",
   ],
 
   postInit: function () {
     for (let o of this._OBSERVERS) {
       Services.obs.addObserver(this, o, false);
     }
 
     return Promise.resolve();
@@ -94,38 +116,98 @@ SyncProvider.prototype = Object.freeze({
     for (let o of this._OBSERVERS) {
       Services.obs.removeObserver(this, o);
     }
 
     return Promise.resolve();
   },
 
   observe: function (subject, topic, data) {
+    switch (topic) {
+      case "weave:service:sync:start":
+      case "weave:service:sync:finish":
+      case "weave:service:sync:error":
+        return this._observeSync(subject, topic, data);
+
+      case "fxa-migration:state-changed":
+      case "fxa-migration:internal-state-changed":
+      case "fxa-migration:internal-telemetry":
+        return this._observeMigration(subject, topic, data);
+    }
+    Cu.reportError("unexpected topic in sync healthreport provider: " + topic);
+  },
+
+  _observeSync: 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;
+
+      default:
+        Cu.reportError("unexpected sync topic in sync healthreport provider: " + topic);
+        return;
     }
 
     let m = this.getMeasurement(SyncMeasurement1.prototype.name,
                                 SyncMeasurement1.prototype.version);
     return this.enqueueStorageOperation(function recordSyncEvent() {
       return m.incrementDailyCounter(field);
     });
   },
 
+  _observeMigration: function(subject, topic, data) {
+    switch (topic) {
+      case "fxa-migration:state-changed":
+      case "fxa-migration:internal-state-changed": {
+        // We record both "user" and "internal" states in the same field.  This
+        // works for us as user state is always null when there is an internal
+        // state.
+        if (!data) {
+          return; // we don't count the |null| state
+        }
+        let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name,
+                                    SyncMigrationMeasurement1.prototype.version);
+        return this.enqueueStorageOperation(function() {
+          return m.setDailyLastText("state", data);
+        });
+      }
+
+      case "fxa-migration:internal-telemetry": {
+        // |data| is our field name.
+        let m = this.getMeasurement(SyncMigrationMeasurement1.prototype.name,
+                                    SyncMigrationMeasurement1.prototype.version);
+        return this.enqueueStorageOperation(function() {
+          switch (data) {
+            case "accepted":
+            case "declined":
+              return m.incrementDailyCounter(data);
+            case "unlinked":
+              return m.setDailyLastNumeric(data, 1);
+            default:
+              Cu.reportError("Unexpected migration field in sync healthreport provider: " + data);
+              return Promise.resolve();
+          }
+        });
+      }
+
+      default:
+        Cu.reportError("unexpected migration topic in sync healthreport provider: " + topic);
+        return;
+    }
+  },
+
   collectDailyData: function () {
     return this.storage.enqueueTransaction(this._populateDailyData.bind(this));
   },
 
   _populateDailyData: function* () {
     let m = this.getMeasurement(SyncMeasurement1.prototype.name,
                                 SyncMeasurement1.prototype.version);
 
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_healthreport_migration.js
@@ -0,0 +1,165 @@
+/* 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/healthreport.jsm", this);
+Cu.import("resource://services-sync/FxaMigrator.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();
+
+  run_next_test();
+}
+
+add_task(function* test_no_data() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  try {
+    // Initially nothing should be configured.
+    let now = new Date();
+    yield provider.collectDailyData();
+
+    let m = provider.getMeasurement("migration", 1);
+    let values = yield m.getValues();
+    Assert.equal(values.days.size, 0);
+    Assert.ok(!values.days.hasDay(now));
+  } finally {
+    yield provider.shutdown();
+    yield storage.close();
+  }
+});
+
+
+add_task(function* test_state() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  try {
+    // Initially nothing should be configured.
+    let now = new Date();
+
+    let m = provider.getMeasurement("migration", 1);
+
+    // We record both a "user" and "internal" state in the same field.
+    // So simulate a "user" state first.
+    Services.obs.notifyObservers(null, "fxa-migration:state-changed",
+                                 fxaMigrator.STATE_USER_FXA_VERIFIED);
+
+    // Wait for storage to complete.
+    yield m.storage.enqueueOperation(() => {
+      return Promise.resolve();
+    });
+
+    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("state"));
+    Assert.equal(day.get("state"), fxaMigrator.STATE_USER_FXA_VERIFIED);
+
+    // And an  internal state.
+    Services.obs.notifyObservers(null, "fxa-migration:internal-state-changed",
+                                 fxaMigrator.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
+
+    // Wait for storage to complete.
+    yield m.storage.enqueueOperation(() => {
+      return Promise.resolve();
+    });
+
+    values = yield m.getValues();
+    Assert.equal(values.days.size, 1);
+    Assert.ok(values.days.hasDay(now));
+    day = values.days.getDay(now);
+    Assert.ok(day.has("state"));
+    Assert.equal(day.get("state"), fxaMigrator.STATE_INTERNAL_WAITING_SYNC_COMPLETE);
+  } finally {
+    yield provider.shutdown();
+    yield storage.close();
+  }
+});
+
+add_task(function* test_flags() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  try {
+    // Initially nothing should be configured.
+    let now = new Date();
+
+    let m = provider.getMeasurement("migration", 1);
+
+    let record = function*(what) {
+      Services.obs.notifyObservers(null, "fxa-migration:internal-telemetry", what);
+      // Wait for storage to complete.
+      yield m.storage.enqueueOperation(Promise.resolve);
+      let values = yield m.getValues();
+      Assert.equal(values.days.size, 1);
+      return values.days.getDay(now);
+    }
+
+    let values = yield m.getValues();
+    Assert.equal(values.days.size, 1);
+    let day = values.days.getDay(now);
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_DECLINED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+
+    // let's send an unknown value to ensure our error mitigation works.
+    day = yield record("unknown");
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_DECLINED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+
+    // record an fxaMigrator.TELEMETRY_ACCEPTED state.
+    day = yield record(fxaMigrator.TELEMETRY_ACCEPTED);
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_DECLINED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 1);
+
+    // and again - it should get 2.
+    day = yield record(fxaMigrator.TELEMETRY_ACCEPTED);
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 2);
+
+    // record fxaMigrator.TELEMETRY_DECLINED - also a counter.
+    day = yield record(fxaMigrator.TELEMETRY_DECLINED);
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_DECLINED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 2);
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_DECLINED), 1);
+
+    day = yield record(fxaMigrator.TELEMETRY_DECLINED);
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_DECLINED));
+    Assert.ok(!day.has(fxaMigrator.TELEMETRY_UNLINKED));
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_ACCEPTED), 2);
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_DECLINED), 2);
+
+    // and fxaMigrator.TELEMETRY_UNLINKED - this is conceptually a "daily bool".
+    // (ie, it's DAILY_LAST_NUMERIC_FIELD and only ever has |1| written to it)
+    day = yield record(fxaMigrator.TELEMETRY_UNLINKED);
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_ACCEPTED));
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_DECLINED));
+    Assert.ok(day.has(fxaMigrator.TELEMETRY_UNLINKED));
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_UNLINKED), 1);
+    // and doing it again still leaves us with |1|
+    day = yield record(fxaMigrator.TELEMETRY_UNLINKED);
+    Assert.equal(day.get(fxaMigrator.TELEMETRY_UNLINKED), 1);
+  } finally {
+    yield provider.shutdown();
+    yield storage.close();
+  }
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -166,14 +166,17 @@ skip-if = debug
 [test_prefs_tracker.js]
 [test_tab_engine.js]
 [test_tab_store.js]
 [test_tab_tracker.js]
 
 [test_healthreport.js]
 skip-if = ! healthreport
 
+[test_healthreport_migration.js]
+skip-if = ! healthreport
+
 [test_warn_on_truncated_response.js]
 
 # FxA migration
 [test_block_sync.js]
 [test_fxa_migration.js]
 [test_fxa_migration_sentinel.js]