Bug 1124516 - Telemetry: Record the time in days each saved login was last used. r=MattN
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Sun, 01 Feb 2015 19:38:09 +0000
changeset 227000 e79f5902b0b1c7e39620949882689fb9fbfe0f5d
parent 226961 940118b1adcd83967fbd49c96217857a91a2b2d0
child 227001 b955d50b6ec34505f4f75c166fdc56825cea507c
child 227098 2f80566d11064e4c656cfc7726d94d827d9f9e94
push id54982
push usercbook@mozilla.com
push dateMon, 02 Feb 2015 12:25:45 +0000
treeherdermozilla-inbound@7e188718dea2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1124516
milestone38.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 1124516 - Telemetry: Record the time in days each saved login was last used. r=MattN
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
@@ -7263,17 +7263,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",