Bug 1064333 - Migrate the FHR client id to the datareporting service. r=gps, a=lmandel
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Fri, 17 Oct 2014 17:24:04 +0200
changeset 225900 8fbc0d8bb83d
parent 225899 d9b49c7ee7fe
child 225901 ad6d502a38c9
push id4063
push usergeorg.fritzsche@googlemail.com
push date2014-11-02 23:54 +0000
treeherdermozilla-beta@1ca39da5df9d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps, lmandel
bugs1064333
milestone34.0
Bug 1064333 - Migrate the FHR client id to the datareporting service. r=gps, a=lmandel
services/common/utils.js
services/datareporting/DataReportingService.js
services/datareporting/policy.jsm
services/datareporting/tests/xpcshell/test_client_id.js
services/datareporting/tests/xpcshell/xpcshell.ini
services/healthreport/healthreporter.jsm
services/healthreport/modules-testing/utils.jsm
services/healthreport/tests/xpcshell/test_healthreporter.js
services/healthreport/tests/xpcshell/test_provider_appinfo.js
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -393,19 +393,18 @@ this.CommonUtils = {
   /**
    * Write a JSON object to the named file using OS.File and promises.
    *
    * @param contents a JS object. Will be serialized.
    * @param path the path of the file to write.
    * @return a promise, as produced by OS.File.writeAtomic.
    */
   writeJSON: function(contents, path) {
-    let encoder = new TextEncoder();
-    let array = encoder.encode(JSON.stringify(contents));
-    return OS.File.writeAtomic(path, array, {tmpPath: path + ".tmp"});
+    let data = JSON.stringify(contents);
+    return OS.File.writeAtomic(path, data, {encoding: "utf-8", tmpPath: path + ".tmp"});
   },
 
 
   /**
    * Ensure that the specified value is defined in integer milliseconds since
    * UNIX epoch.
    *
    * This throws an error if the value is not an integer, is negative, or looks
--- a/services/datareporting/DataReportingService.js
+++ b/services/datareporting/DataReportingService.js
@@ -4,16 +4,19 @@
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
 
 
 const ROOT_BRANCH = "datareporting.";
 const POLICY_BRANCH = ROOT_BRANCH + "policy.";
 const SESSIONS_BRANCH = ROOT_BRANCH + "sessions.";
 const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport.";
 const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging.";
 const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
@@ -56,16 +59,23 @@ const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC 
  */
 this.DataReportingService = function () {
   this.wrappedJSObject = this;
 
   this._quitting = false;
 
   this._os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
+
+  this._clientID = null;
+  this._loadClientIdTask = null;
+  this._saveClientIdTask = null;
+
+  this._stateDir = null;
+  this._stateFilePath = null;
 }
 
 DataReportingService.prototype = Object.freeze({
   classID: Components.ID("{41f6ae36-a79f-4613-9ac3-915e70f83789}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
@@ -121,16 +131,19 @@ DataReportingService.prototype = Object.
             this.sessionRecorder = new SessionRecorder(SESSIONS_BRANCH);
             this.sessionRecorder.onStartup();
           }
 
           // We can't interact with prefs until after the profile is present.
           let policyPrefs = new Preferences(POLICY_BRANCH);
           this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this);
 
+          this._stateDir = OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
+          this._stateFilePath = OS.Path.join(this._stateDir, "state.json");
+
           this._os.addObserver(this, "sessionstore-windows-restored", true);
         } catch (ex) {
           Cu.reportError("Exception when initializing data reporting service: " +
                          CommonUtils.exceptionStr(ex));
         }
         break;
 
       case "sessionstore-windows-restored":
@@ -278,16 +291,119 @@ DataReportingService.prototype = Object.
                                                  this.sessionRecorder);
 
     // Wait for initialization to finish so if a shutdown occurs before init
     // has finished we don't adversely affect app startup on next run.
     this._healthReporter.init().then(function onInit() {
       this._prefs.set("service.firstRun", true);
     }.bind(this));
   },
+
+  _loadClientID: Task.async(function* () {
+    if (this._loadClientIdTask) {
+      return this._loadClientIdTask;
+    }
+
+    // Previously we had the stable client ID managed in FHR.
+    // As we want to start correlating FHR and telemetry data (and moving towards
+    // unifying the two), we moved the ID management to the datareporting
+    // service. Consequently, we try to import the FHR ID first, so we can keep
+    // using it.
+
+    // Try to load the client id from the DRS state file first.
+    try {
+      let state = yield CommonUtils.readJSON(this._stateFilePath);
+      if (state && 'clientID' in state && typeof(state.clientID) == 'string') {
+        this._clientID = state.clientID;
+        this._loadClientIdTask = null;
+        return this._clientID;
+      }
+    } catch (e) {
+      // fall through to next option
+    }
+
+    // If we dont have DRS state yet, try to import from the FHR state.
+    try {
+      let fhrStatePath = OS.Path.join(OS.Constants.Path.profileDir, "healthreport", "state.json");
+      let state = yield CommonUtils.readJSON(fhrStatePath);
+      if (state && 'clientID' in state && typeof(state.clientID) == 'string') {
+        this._clientID = state.clientID;
+        this._loadClientIdTask = null;
+        this._saveClientID();
+        return this._clientID;
+      }
+    } catch (e) {
+      // fall through to next option
+    }
+
+    // We dont have an id from FHR yet, generate a new ID.
+    this._clientID = CommonUtils.generateUUID();
+    this._loadClientIdTask = null;
+    this._saveClientIdTask = this._saveClientID();
+
+    // Wait on persisting the id. Otherwise failure to save the ID would result in
+    // the client creating and subsequently sending multiple IDs to the server.
+    // This would appear as multiple clients submitting similar data, which would
+    // result in orphaning.
+    yield this._saveClientIdTask;
+
+    return this._clientID;
+  }),
+
+  _saveClientID: Task.async(function* () {
+    let obj = { clientID: this._clientID };
+    yield OS.File.makeDir(this._stateDir);
+    yield CommonUtils.writeJSON(obj, this._stateFilePath);
+    this._saveClientIdTask = null;
+  }),
+
+  /**
+   * This returns a promise resolving to the the stable client ID we use for
+   * data reporting (FHR & Telemetry). Previously exising FHR client IDs are
+   * migrated to this.
+   *
+   * @return Promise<string> The stable client ID.
+   */
+  getClientID: function() {
+    if (this._loadClientIdTask) {
+      return this._loadClientIdTask;
+    }
+
+    if (!this._clientID) {
+      this._loadClientIdTask = this._loadClientID();
+      return this._loadClientIdTask;
+    }
+
+    return Promise.resolve(this._clientID);
+  },
+
+  /**
+   * Reset the stable client id.
+   *
+   * @return Promise<string> The new client ID.
+   */
+  resetClientID: Task.async(function* () {
+    yield this._loadClientIdTask;
+    yield this._saveClientIdTask;
+
+    this._clientID = CommonUtils.generateUUID();
+    this._saveClientIdTask = this._saveClientID();
+    yield this._saveClientIdTask;
+
+    return this._clientID;
+  }),
+
+  /*
+   * Simulate a restart of the service. This is for testing only.
+   */
+  _reset: Task.async(function* () {
+    yield this._loadClientIdTask;
+    yield this._saveClientIdTask;
+    this._clientID = null;
+  }),
 });
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]);
 
 #define MERGED_COMPARTMENT
 
 #include ../common/observers.js
 ;
--- a/services/datareporting/policy.jsm
+++ b/services/datareporting/policy.jsm
@@ -812,17 +812,18 @@ this.DataReportingPolicy.prototype = Obj
       this._handleSubmissionFailure();
     }.bind(this);
 
     let chained = deferred.promise.then(onSuccess, onError);
 
     this._log.info("Requesting data submission. Will expire at " +
                    requestExpiresDate);
     try {
-      this._listener[handler](this._inProgressSubmissionRequest);
+      let promise = this._listener[handler](this._inProgressSubmissionRequest);
+      chained = chained.then(() => promise, null);
     } catch (ex) {
       this._log.warn("Exception when calling " + handler + ": " +
                      CommonUtils.exceptionStr(ex));
       this._inProgressSubmissionRequest = null;
       this._handleSubmissionFailure();
       return;
     }
 
new file mode 100644
--- /dev/null
+++ b/services/datareporting/tests/xpcshell/test_client_id.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
+  () => Cc["@mozilla.org/datareporting/service;1"]
+          .getService(Ci.nsISupports)
+          .wrappedJSObject);
+
+function run_test() {
+  do_get_profile();
+
+  // Send the needed startup notifications to the datareporting service
+  // to ensure that it has been initialized.
+  gDatareportingService.observe(null, "app-startup", null);
+  gDatareportingService.observe(null, "profile-after-change", null);
+
+  run_next_test();
+}
+
+add_task(function* () {
+  const drsPath = gDatareportingService._stateFilePath;
+  const fhrDir  = OS.Path.join(OS.Constants.Path.profileDir, "healthreport");
+  const fhrPath = OS.Path.join(fhrDir, "state.json");
+  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+  yield OS.File.makeDir(fhrDir);
+
+  // Check that we are importing the FHR client ID.
+  let clientID = CommonUtils.generateUUID();
+  yield CommonUtils.writeJSON({clientID: clientID}, fhrPath);
+  Assert.equal(clientID, yield gDatareportingService.getClientID());
+
+  // We should persist the ID in DRS now and not pick up a differing ID from FHR.
+  yield gDatareportingService._reset();
+  yield CommonUtils.writeJSON({clientID: CommonUtils.generateUUID()}, fhrPath);
+  Assert.equal(clientID, yield gDatareportingService.getClientID());
+
+  // We should be guarded against broken FHR data.
+  yield gDatareportingService._reset();
+  yield OS.File.remove(drsPath);
+  yield CommonUtils.writeJSON({clientID: -1}, fhrPath);
+  clientID = yield gDatareportingService.getClientID();
+  Assert.equal(typeof(clientID), 'string');
+  Assert.ok(uuidRegex.test(clientID));
+
+  // We should be guarded against invalid FHR json.
+  yield gDatareportingService._reset();
+  yield OS.File.remove(drsPath);
+  yield OS.File.writeAtomic(fhrPath, "abcd", {encoding: "utf-8", tmpPath: fhrPath + ".tmp"});
+  clientID = yield gDatareportingService.getClientID();
+  Assert.equal(typeof(clientID), 'string');
+  Assert.ok(uuidRegex.test(clientID));
+
+  // We should be guarded against broken DRS data too and fall back to loading
+  // the FHR ID.
+  yield gDatareportingService._reset();
+  clientID = CommonUtils.generateUUID();
+  yield CommonUtils.writeJSON({clientID: clientID}, fhrPath);
+  yield CommonUtils.writeJSON({clientID: -1}, drsPath);
+  Assert.equal(clientID, yield gDatareportingService.getClientID());
+
+  // We should be guarded against invalid DRS json too.
+  yield gDatareportingService._reset();
+  yield OS.File.remove(fhrPath);
+  yield OS.File.writeAtomic(drsPath, "abcd", {encoding: "utf-8", tmpPath: drsPath + ".tmp"});
+  clientID = yield gDatareportingService.getClientID();
+  Assert.equal(typeof(clientID), 'string');
+  Assert.ok(uuidRegex.test(clientID));
+
+  // If both the FHR and DSR data are broken, we should end up with a new client ID.
+  yield gDatareportingService._reset();
+  yield CommonUtils.writeJSON({clientID: -1}, fhrPath);
+  yield CommonUtils.writeJSON({clientID: -1}, drsPath);
+  clientID = yield gDatareportingService.getClientID();
+  Assert.equal(typeof(clientID), 'string');
+  Assert.ok(uuidRegex.test(clientID));
+});
--- a/services/datareporting/tests/xpcshell/xpcshell.ini
+++ b/services/datareporting/tests/xpcshell/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head = head.js
 tail =
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_policy.js]
 [test_session_recorder.js]
+[test_client_id.js]
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -125,23 +125,28 @@ HealthReporterState.prototype = Object.f
   get _lastPayloadPath() {
     return OS.Path.join(this._stateDir, "lastpayload.json");
   },
 
   init: function () {
     return Task.spawn(function* init() {
       yield OS.File.makeDir(this._stateDir);
 
+      let drs = Cc["@mozilla.org/datareporting/service;1"]
+                  .getService(Ci.nsISupports)
+                  .wrappedJSObject;
+      let drsClientID = yield drs.getClientID();
+
       let resetObjectState = function () {
         this._s = {
           // The payload version. This is bumped whenever there is a
           // backwards-incompatible change.
           v: 1,
           // The persistent client identifier.
-          clientID: CommonUtils.generateUUID(),
+          clientID: drsClientID,
           // Denotes the mechanism used to generate the client identifier.
           // 1: Random UUID.
           clientIDVersion: 1,
           // Upload IDs that might be on the server.
           remoteIDs: [],
           // When we last performed an uploaded.
           lastPingTime: 0,
           // Tracks whether we removed an outdated payload.
@@ -171,32 +176,17 @@ HealthReporterState.prototype = Object.f
 
       if (this._s.v != 1) {
         this._log.warn("Unknown version in state file: " + this._s.v);
         resetObjectState();
         // We explicitly don't save here in the hopes an application re-upgrade
         // comes along and fixes us.
       }
 
-      let regen = false;
-      if (!this._s.clientID) {
-        this._log.warn("No client ID stored. Generating random ID.");
-        regen = true;
-      }
-
-      if (typeof(this._s.clientID) != "string") {
-        this._log.warn("Client ID is not a string. Regenerating.");
-        regen = true;
-      }
-
-      if (regen) {
-        this._s.clientID = CommonUtils.generateUUID();
-        this._s.clientIDVersion = 1;
-        yield this.save();
-      }
+      this._s.clientID = drsClientID;
 
       // Always look for preferences. This ensures that downgrades followed
       // by reupgrades don't result in excessive data loss.
       for (let promise of this._migratePrefs()) {
         yield promise;
       }
     }.bind(this));
   },
@@ -250,31 +240,28 @@ HealthReporterState.prototype = Object.f
 
     this._log.info("Recording last ping time and deleted remote document.");
     this._s.lastPingTime = date.getTime();
     return this.removeRemoteIDs(ids);
   },
 
   /**
    * Reset the client ID to something else.
-   *
-   * This fails if remote IDs are stored because changing the client ID
-   * while there is remote data will create orphaned records.
+   * Returns a promise that is resolved when completed.
    */
-  resetClientID: function () {
-    if (this.remoteIDs.length) {
-      throw new Error("Cannot reset client ID while remote IDs are stored.");
-    }
+  resetClientID: Task.async(function* () {
+    let drs = Cc["@mozilla.org/datareporting/service;1"]
+                .getService(Ci.nsISupports)
+                .wrappedJSObject;
+    yield drs.resetClientID();
+    this._s.clientID = yield drs.getClientID();
+    this._log.info("Reset client id to " + this._s.clientID + ".");
 
-    this._log.warn("Resetting client ID.");
-    this._s.clientID = CommonUtils.generateUUID();
-    this._s.clientIDVersion = 1;
-
-    return this.save();
-  },
+    yield this.save();
+  }),
 
   _migratePrefs: function () {
     let prefs = this._reporter._prefs;
 
     let lastID = prefs.get("lastSubmitID", null);
     let lastPingDate = CommonUtils.getDatePref(prefs, "lastPingTime",
                                                0, this._log, OLDEST_ALLOWED_YEAR);
 
--- a/services/healthreport/modules-testing/utils.jsm
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -177,33 +177,43 @@ InspectedHealthReporter.prototype = {
 
     return HealthReporter.prototype._onStorageClose.call(this);
   },
 };
 
 const DUMMY_URI="http://localhost:62013/";
 
 this.getHealthReporter = function (name, uri=DUMMY_URI, inspected=false) {
+  // The healthreporters use the client id from the datareporting service,
+  // so we need to ensure it is initialized.
+  let drs = Cc["@mozilla.org/datareporting/service;1"]
+              .getService(Ci.nsISupports)
+              .wrappedJSObject;
+  drs.observe(null, "app-startup", null);
+  drs.observe(null, "profile-after-change", null);
+
   let branch = "healthreport.testing." + name + ".";
 
   let prefs = new Preferences(branch + "healthreport.");
   prefs.set("documentServerURI", uri);
   prefs.set("dbName", name);
 
   let reporter;
 
   let policyPrefs = new Preferences(branch + "policy.");
   let listener = new MockPolicyListener();
   listener.onRequestDataUpload = function (request) {
-    reporter.requestDataUpload(request);
+    let promise = reporter.requestDataUpload(request);
     MockPolicyListener.prototype.onRequestDataUpload.call(this, request);
+    return promise;
   }
   listener.onRequestRemoteDelete = function (request) {
-    reporter.deleteRemoteData(request);
+    let promise = reporter.deleteRemoteData(request);
     MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
+    return promise;
   }
   let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
   let type = inspected ? InspectedHealthReporter : HealthReporter;
   reporter = new type(branch + "healthreport.", policy, null,
                       "state-" + name + ".json");
 
   return reporter;
 };
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -16,16 +16,22 @@ Cu.import("resource://gre/modules/servic
 Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://testing-common/services/common/bagheeraserver.js");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 Cu.import("resource://testing-common/services/healthreport/utils.jsm");
 Cu.import("resource://testing-common/AppData.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
+  () => Cc["@mozilla.org/datareporting/service;1"]
+          .getService(Ci.nsISupports)
+          .wrappedJSObject);
 
 
 const DUMMY_URI = "http://localhost:62013/";
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 const HealthReporterState = bsp.HealthReporterState;
 
 
@@ -103,16 +109,23 @@ function ensureUserNotified (reporter) {
   return Task.spawn(function* ensureUserNotified () {
     reporter._policy.ensureUserNotified();
     yield reporter._policy._listener.lastNotifyRequest.deferred.promise;
     do_check_true(reporter._policy.userNotifiedOfCurrentPolicy);
   });
 }
 
 function run_test() {
+  do_get_profile();
+
+  // Send the needed startup notifications to the datareporting service
+  // to ensure that it has been initialized.
+  gDatareportingService.observe(null, "app-startup", null);
+  gDatareportingService.observe(null, "profile-after-change", null);
+
   run_next_test();
 }
 
 // run_test() needs to finish synchronously, so we do async init here.
 add_task(function test_init() {
   yield makeFakeAppDir();
 });
 
@@ -740,17 +753,17 @@ add_task(function test_request_remote_da
 
     let promise = reporter.requestDeleteRemoteData();
     do_check_neq(promise, null);
     yield promise;
     do_check_null(reporter.lastSubmitID);
     do_check_false(reporter.haveRemoteData());
     do_check_false(server.hasDocument(reporter.serverNamespace, id));
 
-    // Client ID should be updated.
+     // Client ID should be updated.
     do_check_neq(reporter._state.clientID, null);
     do_check_neq(reporter._state.clientID, clientID);
     do_check_eq(reporter._state.clientIDVersion, 1);
 
     // And it should be persisted to disk.
     let o = yield CommonUtils.readJSON(reporter._state._filename);
     do_check_eq(o.clientID, reporter._state.clientID);
     do_check_eq(o.clientIDVersion, 1);
@@ -1166,48 +1179,16 @@ add_task(function test_state_downgrade_u
     let o = yield CommonUtils.readJSON(reporter._state._filename);
     do_check_eq(o.remoteIDs.length, 3);
     do_check_eq(o.lastPingTime, now.getTime() + 1000);
   } finally {
     yield reporter._shutdown();
   }
 });
 
-// Missing client ID in state should be created on state load.
-add_task(function* test_state_create_client_id() {
-  let reporter = getHealthReporter("state_create_client_id");
-
-  yield CommonUtils.writeJSON({
-    v: 1,
-    remoteIDs: ["id1", "id2"],
-    lastPingTime: Date.now(),
-    removeOutdatedLastPayload: true,
-  }, reporter._state._filename);
-
-  try {
-    yield reporter.init();
-
-    do_check_eq(reporter.lastSubmitID, "id1");
-    do_check_neq(reporter._state.clientID, null);
-    do_check_eq(reporter._state.clientID.length, 36);
-    do_check_eq(reporter._state.clientIDVersion, 1);
-
-    let clientID = reporter._state.clientID;
-
-    // The client ID should be persisted as soon as it is created.
-    reporter._shutdown();
-
-    reporter = getHealthReporter("state_create_client_id");
-    yield reporter.init();
-    do_check_eq(reporter._state.clientID, clientID);
-  } finally {
-    reporter._shutdown();
-  }
-});
-
 // Invalid stored client ID is reset automatically.
 add_task(function* test_empty_client_id() {
   let reporter = getHealthReporter("state_empty_client_id");
 
   yield CommonUtils.writeJSON({
     v: 1,
     clientID: "",
     remoteIDs: ["id1", "id2"],
--- a/services/healthreport/tests/xpcshell/test_provider_appinfo.js
+++ b/services/healthreport/tests/xpcshell/test_provider_appinfo.js
@@ -1,23 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const {interfaces: Ci, results: Cr, utils: Cu} = Components;
+const {interfaces: Ci, results: Cr, utils: Cu, classes: Cc} = Components;
 
 Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
 Cu.import("resource://testing-common/services/healthreport/utils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
+  () => Cc["@mozilla.org/datareporting/service;1"]
+          .getService(Ci.nsISupports)
+          .wrappedJSObject);
 
 
 function run_test() {
+  do_get_profile();
+
+  // Send the needed startup notifications to the datareporting service
+  // to ensure that it has been initialized.
+  gDatareportingService.observe(null, "app-startup", null);
+  gDatareportingService.observe(null, "profile-after-change", null);
+
   run_next_test();
 }
 
 add_test(function test_constructor() {
   let provider = new AppInfoProvider();
 
   run_next_test();
 });