Bug 1124516 - Telemetry: Record the time in days each saved login was last used. r=MattN, a=lmandel
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Sun, 01 Feb 2015 19:38:09 +0000
changeset 249670 cc5e2253319997d3d0aa1bc5c3899efc34dcdc08
parent 249669 396eb0f2d14463affb28ff2adb5001d2a7e0f53f
child 249671 af0810842345bacd6e3f5d84a5008f403438e6b3
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)
reviewersMattN, lmandel
bugs1124516
milestone37.0a2
Bug 1124516 - Telemetry: Record the time in days each saved login was last used. r=MattN, a=lmandel
toolkit/components/passwordmgr/nsLoginManager.js
toolkit/components/passwordmgr/test/unit/test_telemetry.js
toolkit/components/passwordmgr/test/unit/xpcshell.ini
toolkit/components/telemetry/Histograms.json
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -15,16 +15,18 @@ Cu.import("resource://gre/modules/LoginM
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
 var debug = false;
 function log(...pieces) {
   function generateLogMessage(args) {
     let strings = ['Login Manager:'];
 
     args.forEach(function(arg) {
       if (typeof arg === 'string') {
         strings.push(arg);
@@ -196,44 +198,75 @@ LoginManager.prototype = {
         Task.spawn(function () {
           yield this._pwmgr._storage.terminate();
           this._pwmgr._initStorage();
           yield this._pwmgr.initializationPromise;
           Services.obs.notifyObservers(null,
                        "passwordmgr-storage-replace-complete", null);
         }.bind(this));
       } else if (topic == "gather-telemetry") {
-        this._pwmgr._gatherTelemetry();
+        // When testing, the "data" parameter is a string containing the
+        // reference time in milliseconds for time-based statistics.
+        this._pwmgr._gatherTelemetry(data ? parseInt(data)
+                                          : new Date().getTime());
       } else {
         log("Oops! Unexpected notification:", topic);
       }
     }
   },
 
-  _gatherTelemetry : function() {
-    let numPasswordsBlocklist = Services.telemetry.getHistogramById("PWMGR_BLOCKLIST_NUM_SITES");
-    numPasswordsBlocklist.clear();
-    numPasswordsBlocklist.add(this.getAllDisabledHosts({}).length);
+  /**
+   * Collects statistics about the current logins and settings. The telemetry
+   * histograms used here are not accumulated, but are reset each time this
+   * function is called, since it can be called multiple times in a session.
+   *
+   * This function might also not be called at all in the current session.
+   *
+   * @param referenceTimeMs
+   *        Current time used to calculate time-based statistics, expressed as
+   *        the number of milliseconds since January 1, 1970, 00:00:00 UTC.
+   *        This is set to a fake value during unit testing.
+   */
+  _gatherTelemetry : function (referenceTimeMs) {
+    function clearAndGetHistogram(histogramId) {
+      let histogram = Services.telemetry.getHistogramById(histogramId);
+      histogram.clear();
+      return histogram;
+    }
 
-    let numPasswordsHist = Services.telemetry.getHistogramById("PWMGR_NUM_SAVED_PASSWORDS");
-    numPasswordsHist.clear();
-    numPasswordsHist.add(this.countLogins("", "", ""));
+    clearAndGetHistogram("PWMGR_BLOCKLIST_NUM_SITES").add(
+      this.getAllDisabledHosts({}).length
+    );
+    clearAndGetHistogram("PWMGR_NUM_SAVED_PASSWORDS").add(
+      this.countLogins("", "", "")
+    );
 
-    let isPwdSavedEnabledHist = Services.telemetry.getHistogramById("PWMGR_SAVING_ENABLED");
-    isPwdSavedEnabledHist.clear();
-    isPwdSavedEnabledHist.add(this._remember);
+    // This is a boolean histogram, and not a flag, because we don't want to
+    // record any value if _gatherTelemetry is not called.
+    clearAndGetHistogram("PWMGR_SAVING_ENABLED").add(this._remember);
 
     // Don't try to get logins if MP is enabled, since we don't want to show a MP prompt.
-    if (this.isLoggedIn) {
-      let logins = this.getAllLogins({});
+    if (!this.isLoggedIn) {
+      return;
+    }
+
+    let logins = this.getAllLogins({});
+
+    let usernamePresentHistogram = clearAndGetHistogram("PWMGR_USERNAME_PRESENT");
+    let loginLastUsedDaysHistogram = clearAndGetHistogram("PWMGR_LOGIN_LAST_USED_DAYS");
 
-      let usernameHist = Services.telemetry.getHistogramById("PWMGR_USERNAME_PRESENT");
-      usernameHist.clear();
-      for (let login of logins) {
-        usernameHist.add(!!login.username);
+    for (let login of logins) {
+      usernamePresentHistogram.add(!!login.username);
+
+      login.QueryInterface(Ci.nsILoginMetaInfo);
+      let timeLastUsedAgeMs = referenceTimeMs - login.timeLastUsed;
+      if (timeLastUsedAgeMs > 0) {
+        loginLastUsedDaysHistogram.add(
+          Math.floor(timeLastUsedAgeMs / MS_PER_DAY)
+        );
       }
     }
   },
 
 
 
 
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_telemetry.js
@@ -0,0 +1,183 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the statistics and other counters reported through telemetry.
+ */
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+// To prevent intermittent failures when the test is executed at a time that is
+// very close to a day boundary, we make it deterministic by using a static
+// reference date for all the time-based statistics.
+const gReferenceTimeMs = new Date("2000-01-01T00:00:00").getTime();
+
+// Returns a milliseconds value to use with nsILoginMetaInfo properties, falling
+// approximately in the middle of the specified number of days before the
+// reference time, where zero days indicates a time within the past 24 hours.
+let daysBeforeMs = days => gReferenceTimeMs - (days + 0.5) * MS_PER_DAY;
+
+/**
+ * Contains metadata that will be attached to test logins in order to verify
+ * that the statistics collection is working properly. Most properties of the
+ * logins are initialized to the default test values already.
+ *
+ * If you update this data or any of the telemetry histograms it checks, you'll
+ * probably need to update the expected statistics in the test below.
+ */
+const StatisticsTestData = [
+  {
+    timeLastUsed: daysBeforeMs(0),
+  },
+  {
+    timeLastUsed: daysBeforeMs(1),
+  },
+  {
+    timeLastUsed: daysBeforeMs(7),
+  },
+  {
+    username: "",
+    timeLastUsed: daysBeforeMs(7),
+  },
+  {
+    username: "",
+    timeLastUsed: daysBeforeMs(30),
+  },
+  {
+    username: "",
+    timeLastUsed: daysBeforeMs(31),
+  },
+  {
+    timeLastUsed: daysBeforeMs(365),
+  },
+  {
+    username: "",
+    timeLastUsed: daysBeforeMs(366),
+  },
+  {
+    // If the login was saved in the future, it is ignored for statistiscs.
+    timeLastUsed: daysBeforeMs(-1),
+  },
+  {
+    timeLastUsed: daysBeforeMs(1000),
+  },
+];
+
+/**
+ * Triggers the collection of those statistics that are not accumulated each
+ * time an action is taken, but are a static snapshot of the current state.
+ */
+function triggerStatisticsCollection() {
+  Services.obs.notifyObservers(null, "gather-telemetry", "" + gReferenceTimeMs);
+}
+
+/**
+ * Tests the telemetry histogram with the given ID contains only the specified
+ * non-zero ranges, expressed in the format { range1: value1, range2: value2 }.
+ */
+function testHistogram(histogramId, expectedNonZeroRanges) {
+  let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+
+  // Compute the actual ranges in the format { range1: value1, range2: value2 }.
+  let actualNonZeroRanges = {};
+  for (let [index, range] of snapshot.ranges.entries()) {
+    let value = snapshot.counts[index];
+    if (value > 0) {
+      actualNonZeroRanges[range] = value;
+    }
+  }
+
+  // These are stringified to visualize the differences between the values.
+  do_print("Testing histogram: " + histogramId);
+  do_check_eq(JSON.stringify(actualNonZeroRanges),
+              JSON.stringify(expectedNonZeroRanges));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+/**
+ * Enable local telemetry recording for the duration of the tests, and prepare
+ * the test data that will be used by the following tests.
+ */
+add_task(function test_initialize() {
+  let oldCanRecord = Services.telemetry.canRecord;
+  Services.telemetry.canRecord = true;
+  do_register_cleanup(function () {
+    Services.telemetry.canRecord = oldCanRecord;
+  });
+
+  let uniqueNumber = 1;
+  for (let loginModifications of StatisticsTestData) {
+    loginModifications.hostname = `http://${uniqueNumber++}.example.com`;
+    Services.logins.addLogin(TestData.formLogin(loginModifications));
+  }
+});
+
+/**
+ * Tests the collection of statistics related to login metadata.
+ */
+add_task(function test_logins_statistics() {
+  // Repeat the operation twice to test that histograms are not accumulated.
+  for (let repeating of [false, true]) {
+    triggerStatisticsCollection();
+
+    // Should record 1 in the bucket corresponding to the number of passwords.
+    testHistogram("PWMGR_NUM_SAVED_PASSWORDS",
+                  { 10: 1 });
+
+    // For each saved login, should record 1 in the bucket corresponding to the
+    // age in days since the login was last used.
+    testHistogram("PWMGR_LOGIN_LAST_USED_DAYS",
+                  { 0: 1, 1: 1, 7: 2, 29: 2, 356: 2, 750: 1 });
+
+    // Should record the number of logins without a username in bucket 0, and
+    // the number of logins with a username in bucket 1.
+    testHistogram("PWMGR_USERNAME_PRESENT",
+                  { 0: 4, 1: 6 });
+  }
+});
+
+/**
+ * Tests the collection of statistics related to hosts for which passowrd saving
+ * has been explicitly disabled.
+ */
+add_task(function test_disabledHosts_statistics() {
+  // Should record 1 in the bucket corresponding to the number of sites for
+  // which password saving is disabled.
+  Services.logins.setLoginSavingEnabled("http://www.example.com", false);
+  triggerStatisticsCollection();
+  testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 1: 1 });
+
+  Services.logins.setLoginSavingEnabled("http://www.example.com", true);
+  triggerStatisticsCollection();
+  testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 0: 1 });
+});
+
+/**
+ * Tests the collection of statistics related to general settings.
+ */
+add_task(function test_settings_statistics() {
+  let oldRememberSignons = Services.prefs.getBoolPref("signon.rememberSignons");
+  do_register_cleanup(function () {
+    Services.prefs.setBoolPref("signon.rememberSignons", oldRememberSignons);
+  });
+
+  // Repeat the operation twice per value to test that histograms are reset.
+  for (let remember of [false, true, false, true]) {
+    // This change should be observed immediately by the login service.
+    Services.prefs.setBoolPref("signon.rememberSignons", remember);
+
+    triggerStatisticsCollection();
+
+    // Should record 1 in either bucket 0 or bucket 1 based on the preference.
+    testHistogram("PWMGR_SAVING_ENABLED", remember ? { 1: 1 } : { 0: 1 });
+  }
+});
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -19,8 +19,9 @@ skip-if = os != "android"
 [test_legacy_empty_formSubmitURL.js]
 [test_legacy_validation.js]
 [test_logins_change.js]
 [test_logins_decrypt_failure.js]
 [test_logins_metainfo.js]
 [test_logins_search.js]
 [test_notifications.js]
 [test_storage.js]
+[test_telemetry.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -7167,17 +7167,24 @@
     "description": "The number of sites for which the user has explicitly rejected saving logins"
   },
   "PWMGR_NUM_SAVED_PASSWORDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 750,
     "n_buckets" : 50,
     "extended_statistics_ok": true,
-    "description": "The number of saved signons in storage"
+    "description": "Total number of saved logins, including those that cannot be decrypted"
+  },
+  "PWMGR_LOGIN_LAST_USED_DAYS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": 750,
+    "n_buckets" : 40,
+    "description": "Time in days each saved login was last used"
   },
   "PWMGR_SAVING_ENABLED": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Number of users who have password saving on globally"
   },
   "PWMGR_USERNAME_PRESENT": {
     "expires_in_version": "never",