Bug 974086 - [australis-measuring] UITour.seenPageIDs should persist across sessions. r=MattN sr=taras
authorBlair McBride <bmcbride@mozilla.com>
Thu, 27 Feb 2014 22:25:44 +1300
changeset 171321 ca204e499906ee58bf6dabe4c4b934c86e416d4c
parent 171320 117af4f944c8b2a858950dacdff46ad2b9449645
child 171322 f5b3819834fecc2791177466c60f0dbb4d5cffa9
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersMattN, taras
bugs974086
milestone30.0a1
Bug 974086 - [australis-measuring] UITour.seenPageIDs should persist across sessions. r=MattN sr=taras
browser/modules/UITour.jsm
browser/modules/test/browser_UITour_registerPageID.js
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -21,30 +21,33 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
 
 
 const UITOUR_PERMISSION   = "uitour";
 const PREF_PERM_BRANCH    = "browser.uitour.";
+const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
 const MAX_BUTTONS         = 4;
 
 const BUCKET_NAME         = "UITour";
 const BUCKET_TIMESTEPS    = [
   1 * 60 * 1000, // Until 1 minute after tab is closed/inactive.
   3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
   10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
   60 * 60 * 1000, // Until 1 hour after tab is closed/inactive.
 ];
 
+// Time after which seen Page IDs expire.
+const SEENPAGEID_EXPIRY  = 2 * 7 * 24 * 60 * 60 * 1000; // 2 weeks.
 
 
 this.UITour = {
-  seenPageIDs: new Set(),
+  seenPageIDs: null,
   pageIDSourceTabs: new WeakMap(),
   pageIDSourceWindows: new WeakMap(),
   /* Map from browser windows to a set of tabs in which a tour is open */
   originTabs: new WeakMap(),
   /* Map from browser windows to a set of pinned tabs opened by (a) tour(s) */
   pinnedTabs: new WeakMap(),
   urlbarCapture: new WeakMap(),
   appMenuOpenForAnnotation: new Set(),
@@ -110,20 +113,82 @@ this.UITour = {
     }],
     ["urlbar",      {
       query: "#urlbar",
       widgetName: "urlbar-container",
     }],
   ]),
 
   init: function() {
+    // Lazy getter is initialized here so it can be replicated any time
+    // in a test.
+    delete this.seenPageIDs;
+    Object.defineProperty(this, "seenPageIDs", {
+      get: this.restoreSeenPageIDs.bind(this),
+      configurable: true,
+    });
+
     UITelemetry.addSimpleMeasureFunction("UITour",
                                          this.getTelemetry.bind(this));
   },
 
+  restoreSeenPageIDs: function() {
+    delete this.seenPageIDs;
+
+    if (UITelemetry.enabled) {
+      let dateThreshold = Date.now() - SEENPAGEID_EXPIRY;
+
+      try {
+        let data = Services.prefs.getCharPref(PREF_SEENPAGEIDS);
+        data = new Map(JSON.parse(data));
+
+        for (let [pageID, details] of data) {
+
+          if (typeof pageID != "string" ||
+              typeof details != "object" ||
+              typeof details.lastSeen != "number" ||
+              details.lastSeen < dateThreshold) {
+
+            data.delete(pageID);
+          }
+        }
+
+        this.seenPageIDs = data;
+      } catch (e) {}
+    }
+
+    if (!this.seenPageIDs)
+      this.seenPageIDs = new Map();
+
+    this.persistSeenIDs();
+
+    return this.seenPageIDs;
+  },
+
+  addSeenPageID: function(aPageID) {
+    if (!UITelemetry.enabled)
+      return;
+
+    this.seenPageIDs.set(aPageID, {
+      lastSeen: Date.now(),
+    });
+
+    this.persistSeenIDs();
+  },
+
+  persistSeenIDs: function() {
+    if (this.seenPageIDs.size === 0) {
+      Services.prefs.clearUserPref(PREF_SEENPAGEIDS);
+      return;
+    }
+
+    Services.prefs.setCharPref(PREF_SEENPAGEIDS,
+                               JSON.stringify([...this.seenPageIDs]));
+  },
+
   onPageEvent: function(aEvent) {
     let contentDocument = null;
     if (aEvent.target instanceof Ci.nsIDOMHTMLDocument)
       contentDocument = aEvent.target;
     else if (aEvent.target instanceof Ci.nsIDOMHTMLElement)
       contentDocument = aEvent.target.ownerDocument;
     else
       return false;
@@ -156,21 +221,25 @@ this.UITour = {
       }
       Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
                      "This shouldn't happen!");
       return;
     }
 
     switch (action) {
       case "registerPageID": {
+        // This is only relevant if Telemtry is enabled.
+        if (!UITelemetry.enabled)
+          break;
+
         // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the
         // pageID, as it could make parsing the telemetry bucket name difficult.
         if (typeof data.pageID == "string" &&
             !data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
-          this.seenPageIDs.add(data.pageID);
+          this.addSeenPageID(data.pageID);
 
           // Store tabs and windows separately so we don't need to loop over all
           // tabs when a window is closed.
           this.pageIDSourceTabs.set(tab, data.pageID);
           this.pageIDSourceWindows.set(window, data.pageID);
 
           this.setTelemetryBucket(data.pageID);
         }
@@ -439,17 +508,17 @@ this.UITour = {
                  BrowserUITelemetry.BUCKET_SEPARATOR + aType;
 
     BrowserUITelemetry.setExpiringBucket(bucket,
                                          BUCKET_TIMESTEPS);
   },
 
   getTelemetry: function() {
     return {
-      seenPageIDs: [...this.seenPageIDs],
+      seenPageIDs: [...this.seenPageIDs.keys()],
     };
   },
 
   teardownTour: function(aWindow, aWindowClosing = false) {
     aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
     aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
     aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
     aWindow.removeEventListener("SSWindowClosing", this);
--- a/browser/modules/test/browser_UITour_registerPageID.js
+++ b/browser/modules/test/browser_UITour_registerPageID.js
@@ -3,33 +3,78 @@
 
 "use strict";
 
 let gTestTab;
 let gContentAPI;
 let gContentWindow;
 
 Components.utils.import("resource:///modules/UITour.jsm");
+Components.utils.import("resource://gre/modules/UITelemetry.jsm");
 Components.utils.import("resource:///modules/BrowserUITelemetry.jsm");
 
 function test() {
+  UITelemetry._enabled = true;
+
   registerCleanupFunction(function() {
-    UITour.seenPageIDs.clear();
+    Services.prefs.clearUserPref("browser.uitour.seenPageIDs");
+    resetSeenPageIDsLazyGetter();
+    UITelemetry._enabled = undefined;
     BrowserUITelemetry.setBucket(null);
+    delete window.UITelemetry;
     delete window.BrowserUITelemetry;
   });
   UITourTest();
 }
 
+function resetSeenPageIDsLazyGetter() {
+  delete UITour.seenPageIDs;
+  // This should be kept in sync with how UITour.init() sets this.
+  Object.defineProperty(UITour, "seenPageIDs", {
+    get: UITour.restoreSeenPageIDs.bind(UITour),
+    configurable: true,
+  });
+}
+
+function checkExpectedSeenPageIDs(expected) {
+  is(UITour.seenPageIDs.size, expected.length, "Should be " + expected.length + " total seen page IDs");
+
+  for (let id of expected)
+    ok(UITour.seenPageIDs.has(id), "Should have seen '" + id + "' page ID");
+
+  let prefData = Services.prefs.getCharPref("browser.uitour.seenPageIDs");
+  prefData = new Map(JSON.parse(prefData));
+
+  is(prefData.size, expected.length, "Should be " + expected.length + " total seen page IDs persisted");
+
+  for (let id of expected)
+    ok(prefData.has(id), "Should have seen '" + id + "' page ID persisted");
+}
+
 let tests = [
-  function test_seenPageIDs_1(done) {
+  function test_seenPageIDs_restore(done) {
+    info("Setting up seenPageIDs to be restored from pref");
+    let data = JSON.stringify([
+      ["savedID1", { lastSeen: Date.now() }],
+      ["savedID2", { lastSeen: Date.now() }],
+      // 3 weeks ago, should auto expire.
+      ["savedID3", { lastSeen: Date.now() - 3 * 7 * 24 * 60 * 60 * 1000 }],
+    ]);
+    Services.prefs.setCharPref("browser.uitour.seenPageIDs",
+                               data);
+
+    resetSeenPageIDsLazyGetter();
+    checkExpectedSeenPageIDs(["savedID1", "savedID2"]);
+
+    done();
+  },
+  function test_seenPageIDs_set_1(done) {
     gContentAPI.registerPageID("testpage1");
 
-    is(UITour.seenPageIDs.size, 1, "Should be 1 seen page ID");
-    ok(UITour.seenPageIDs.has("testpage1"), "Should have seen 'testpage1' page ID");
+    checkExpectedSeenPageIDs(["savedID1", "savedID2", "testpage1"]);
 
     const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
     const SEP = BrowserUITelemetry.BUCKET_SEPARATOR;
 
     let bucket = PREFIX + "UITour" + SEP + "testpage1";
     is(BrowserUITelemetry.currentBucket, bucket, "Bucket should have correct name");
 
     gBrowser.selectedTab = gBrowser.addTab("about:blank");
@@ -37,22 +82,20 @@ let tests = [
     is(BrowserUITelemetry.currentBucket, bucket,
        "After switching tabs, bucket should be expiring");
 
     gBrowser.removeTab(gBrowser.selectedTab);
     gBrowser.selectedTab = gTestTab;
     BrowserUITelemetry.setBucket(null);
     done();
   },
-  function test_seenPageIDs_2(done) {
+  function test_seenPageIDs_set_2(done) {
     gContentAPI.registerPageID("testpage2");
 
-    is(UITour.seenPageIDs.size, 2, "Should be 2 seen page IDs");
-    ok(UITour.seenPageIDs.has("testpage1"), "Should have seen 'testpage1' page ID");
-    ok(UITour.seenPageIDs.has("testpage2"), "Should have seen 'testpage2' page ID");
+    checkExpectedSeenPageIDs(["savedID1", "savedID2", "testpage1", "testpage2"]);
 
     const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
     const SEP = BrowserUITelemetry.BUCKET_SEPARATOR;
 
     let bucket = PREFIX + "UITour" + SEP + "testpage2";
     is(BrowserUITelemetry.currentBucket, bucket, "Bucket should have correct name");
 
     gBrowser.removeTab(gTestTab);