Bug 1465697 - Add menu open probes for Savant Shield study; r=jaws draft
authorBianca Danforth <bdanforth@mozilla.com>
Wed, 06 Jun 2018 06:26:38 -0700
changeset 805938 83ee5cb70e11effed0ef3b8a66251dc712f55bad
parent 805878 26a84bd3c58fe24c293448f2d60e413ec62c682d
child 805940 4767bc8c9610c1264bb925df3bad936b62bf5c11
child 805944 3708e443de487766ff8bf38237a462519a9db82b
push id112810
push userbdanforth@mozilla.com
push dateFri, 08 Jun 2018 18:19:41 +0000
reviewersjaws
bugs1465697
milestone62.0a1
Bug 1465697 - Add menu open probes for Savant Shield study; r=jaws These probes will register and record (for the duration of the study only): * When the library menu opens. * When the hamburger menu opens. * When the dotdotdot menu opens. These will be captured in existing and new windows. Note: The library panel is dynamically added and removed from the document (see PanelUI.js for where the panel is created and removed), so a listener can't be added to it in advance. However, the library menu "ViewShowing" event bubbles up to the navBar in its default location. A separate listener is needed if it is moved to the overflow panel via Hamburger > Customize. MozReview-Commit-ID: EBBBgXAQnxE
browser/modules/SavantShieldStudy.jsm
toolkit/components/telemetry/Events.yaml
--- a/browser/modules/SavantShieldStudy.jsm
+++ b/browser/modules/SavantShieldStudy.jsm
@@ -35,19 +35,20 @@ class SavantShieldStudyClass {
     this.STUDY_DURATION_OVERRIDE_PREF = "shield.savant.duration_override";
     this.STUDY_EXPIRATION_DATE_PREF = "shield.savant.expiration_date";
     // ms = 'x' weeks * 7 days/week * 24 hours/day * 60 minutes/hour
     // * 60 seconds/minute * 1000 milliseconds/second
     this.DEFAULT_STUDY_DURATION_MS = 4 * 7 * 24 * 60 * 60 * 1000;
   }
 
   init() {
-    this.TelemetryEvents = new TelemetryEvents(this.STUDY_TELEMETRY_CATEGORY);
-    this.AddonListener = new AddonListener(this.STUDY_TELEMETRY_CATEGORY);
-    this.BookmarkObserver = new BookmarkObserver(this.STUDY_TELEMETRY_CATEGORY);
+    this.telemetryEvents = new TelemetryEvents(this.STUDY_TELEMETRY_CATEGORY);
+    this.addonListener = new AddonListener(this.STUDY_TELEMETRY_CATEGORY);
+    this.bookmarkObserver = new BookmarkObserver(this.STUDY_TELEMETRY_CATEGORY);
+    this.menuListener = new MenuListener(this.STUDY_TELEMETRY_CATEGORY);
 
     // check the pref in case Normandy flipped it on before we could add the pref listener
     this.shouldCollect = Services.prefs.getBoolPref(this.STUDY_PREF);
     if (this.shouldCollect) {
       this.startupStudy();
     }
     Services.prefs.addObserver(this.STUDY_PREF, this);
   }
@@ -62,32 +63,33 @@ class SavantShieldStudyClass {
         // The pref has been turned off
         this.endStudy("study_disable");
       }
     }
   }
 
   startupStudy() {
     // enable before any possible calls to endStudy, since it sends an 'end_study' event
-    this.TelemetryEvents.enableCollection();
+    this.telemetryEvents.enableCollection();
 
     if (!this.isEligible()) {
       this.endStudy("ineligible");
       return;
     }
 
     this.initStudyDuration();
 
     if (this.isStudyExpired()) {
       log.debug("Study expired in between this and the previous session.");
       this.endStudy("expired");
     }
 
-    this.AddonListener.init();
-    this.BookmarkObserver.init();
+    this.addonListener.init();
+    this.bookmarkObserver.init();
+    this.menuListener.init();
   }
 
   isEligible() {
     const isAlwaysPrivateBrowsing = Services.prefs.getBoolPref(this.ALWAYS_PRIVATE_BROWSING_PREF);
     if (isAlwaysPrivateBrowsing) {
       return false;
     }
 
@@ -137,35 +139,36 @@ class SavantShieldStudyClass {
     return false;
   }
 
   endStudy(reason) {
     log.debug(`Ending the study due to reason: ${ reason }`);
     const isStudyEnding = true;
     Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, "end_study", reason, null,
                                   { subcategory: "shield" });
-    this.TelemetryEvents.disableCollection();
+    this.telemetryEvents.disableCollection();
     this.uninit(isStudyEnding);
     // These prefs needs to persist between restarts, so only reset on endStudy
     Services.prefs.clearUserPref(this.STUDY_PREF);
     Services.prefs.clearUserPref(this.STUDY_EXPIRATION_DATE_PREF);
   }
 
   // Called on every Firefox shutdown and endStudy
   uninit(isStudyEnding = false) {
     // if just shutting down, check for expiration, so the endStudy event can
     // be sent along with this session's main ping.
     if (!isStudyEnding && this.isStudyExpired()) {
       log.debug("Study expired during this session.");
       this.endStudy("expired");
       return;
     }
 
-    this.AddonListener.uninit();
-    this.BookmarkObserver.uninit();
+    this.addonListener.uninit();
+    this.bookmarkObserver.uninit();
+    this.menuListener.uninit();
 
     Services.prefs.removeObserver(this.ALWAYS_PRIVATE_BROWSING_PREF, this);
     Services.prefs.removeObserver(this.STUDY_PREF, this);
     Services.prefs.removeObserver(this.STUDY_DURATION_OVERRIDE_PREF, this);
     Services.prefs.clearUserPref(PREF_LOG_LEVEL);
     Services.prefs.clearUserPref(this.STUDY_DURATION_OVERRIDE_PREF);
   }
 }
@@ -313,9 +316,206 @@ class BookmarkObserver {
     PlacesUtils.bookmarks.removeObserver(this);
   }
 
   uninit() {
     this.removeObservers();
   }
 }
 
+class MenuListener {
+  constructor(studyCategory) {
+    this.STUDY_TELEMETRY_CATEGORY = studyCategory;
+    this.NAVIGATOR_TOOLBOX_ID = "navigator-toolbox";
+    this.OVERFLOW_PANEL_ID = "widget-overflow";
+    this.LIBRARY_PANELVIEW_ID = "appMenu-libraryView";
+    this.HAMBURGER_PANEL_ID = "appMenu-popup";
+    this.DOTDOTDOT_PANEL_ID = "pageActionPanel";
+    this.windowWatcher = new WindowWatcher();
+  }
+
+  init() {
+    this.windowWatcher.init(this.loadIntoWindow.bind(this),
+    this.unloadFromWindow.bind(this),
+    this.onWindowError.bind(this));
+  }
+
+ loadIntoWindow(win) {
+    this.addListeners(win);
+  }
+
+  unloadFromWindow(win) {
+    this.removeListeners(win);
+  }
+
+  onWindowError(msg) {
+    log.error(msg);
+  }
+
+  addListeners(win) {
+    const doc = win.document;
+    const navToolbox = doc.getElementById(this.NAVIGATOR_TOOLBOX_ID);
+    const overflowPanel = doc.getElementById(this.OVERFLOW_PANEL_ID);
+    const hamburgerPanel = doc.getElementById(this.HAMBURGER_PANEL_ID);
+    const dotdotdotPanel = doc.getElementById(this.DOTDOTDOT_PANEL_ID);
+
+    /*
+    * the library menu "ViewShowing" event bubbles up on the navToolbox in its
+    * default location. A separate listener is needed if it is moved to the
+    * overflow panel via Hamburger > Customize
+    */
+    navToolbox.addEventListener("ViewShowing", this);
+    overflowPanel.addEventListener("ViewShowing", this);
+    hamburgerPanel.addEventListener("popupshown", this);
+    dotdotdotPanel.addEventListener("popupshown", this);
+  }
+
+  handleEvent(evt) {
+    switch (evt.type) {
+      case "ViewShowing":
+        if (evt.target.id === this.LIBRARY_PANELVIEW_ID) {
+          log.debug("Library panel opened.");
+          this.recordEvent("library_menu");
+        }
+        break;
+      case "popupshown":
+        switch (evt.target.id) {
+          case this.HAMBURGER_PANEL_ID:
+            log.debug("Hamburger panel opened.");
+            this.recordEvent("hamburger_menu");
+            break;
+          case this.DOTDOTDOT_PANEL_ID:
+            log.debug("Dotdotdot panel opened.");
+            this.recordEvent("dotdotdot_menu");
+            break;
+          default:
+            break;
+        }
+      break;
+    }
+  }
+
+  recordEvent(method) {
+    Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, method, "open", null,
+                                  { subcategory: "menu" });
+  }
+
+  removeListeners(win) {
+    const doc = win.document;
+    const navToolbox = doc.getElementById(this.NAVIGATOR_TOOLBOX_ID);
+    const overflowPanel = doc.getElementById(this.OVERFLOW_PANEL_ID);
+    const hamburgerPanel = doc.getElementById(this.HAMBURGER_PANEL_ID);
+    const dotdotdotPanel = doc.getElementById(this.DOTDOTDOT_PANEL_ID);
+
+    try {
+      navToolbox.removeEventListener("ViewShowing", this);
+      overflowPanel.removeEventListener("ViewShowing", this);
+      hamburgerPanel.removeEventListener("popupshown", this);
+      dotdotdotPanel.removeEventListener("popupshown", this);
+    } catch (err) {
+      // Firefox is shutting down; elements have already been removed.
+    }
+  }
+
+  uninit() {
+    this.windowWatcher.uninit();
+  }
+}
+
+/*
+* The WindowWatcher is used to add/remove listeners from MenuListener
+* to/from all windows.
+*/
+class WindowWatcher {
+  constructor() {
+    this._isActive = false;
+    this._loadCallback = null;
+    this._unloadCallback = null;
+    this._errorCallback = null;
+  }
+
+  // It is expected that loadCallback, unloadCallback, and errorCallback are bound
+  // to a `this` value.
+  init(loadCallback, unloadCallback, errorCallback) {
+    if (this._isActive) {
+      errorCallback("Called init, but WindowWatcher was already running");
+      return;
+    }
+
+    this._isActive = true;
+    this._loadCallback = loadCallback;
+    this._unloadCallback = unloadCallback;
+    this._errorCallback = errorCallback;
+
+    // Add loadCallback to existing windows
+    const windows = Services.wm.getEnumerator("navigator:browser");
+    while (windows.hasMoreElements()) {
+      const win = windows.getNext();
+      try {
+        this._loadCallback(win);
+      } catch (ex) {
+        this._errorCallback(`WindowWatcher code loading callback failed: ${ ex }`);
+      }
+    }
+
+    // Add loadCallback to future windows
+    // This will call the observe method on WindowWatcher
+    Services.ww.registerNotification(this);
+  }
+
+  uninit() {
+    if (!this._isActive) {
+      this._errorCallback("Called uninit, but WindowWatcher was already uninited");
+      return;
+    }
+
+    const windows = Services.wm.getEnumerator("navigator:browser");
+    while (windows.hasMoreElements()) {
+      const win = windows.getNext();
+      try {
+        this._unloadCallback(win);
+      } catch (ex) {
+        this._errorCallback(`WindowWatcher code unloading callback failed: ${ ex }`);
+      }
+    }
+
+    Services.ww.unregisterNotification(this);
+
+    this._loadCallback = null;
+    this._unloadCallback = null;
+    this._errorCallback = null;
+    this._isActive = false;
+  }
+
+  observe(win, topic) {
+    switch (topic) {
+      case "domwindowopened":
+        this._onWindowOpened(win);
+        break;
+      case "domwindowclosed":
+        this._onWindowClosed(win);
+        break;
+      default:
+        break;
+    }
+  }
+
+  _onWindowOpened(win) {
+    win.addEventListener("load", this, { once: true });
+  }
+
+  // only one event type expected: "load"
+  handleEvent(evt) {
+    const win = evt.target.ownerGlobal;
+
+    // make sure we only add window listeners to a DOMWindow (browser.xul)
+    const winType = win.document.documentElement.getAttribute("windowtype");
+    if (winType === "navigator:browser") {
+      this._loadCallback(win);
+    }
+  }
+
+  _onWindowClosed(win) {
+    this._unloadCallback(win);
+  }
+}
+
 const SavantShieldStudy = new SavantShieldStudyClass();
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -182,16 +182,55 @@ savant:
       This is recorded when the user selects a urlbar bookmark or history result.
     bug_numbers: [1457226, 1465704]
     notification_emails:
       - "bdanforth@mozilla.com"
       - "shong@mozilla.com"
     expiry_version: "65"
     extra_keys:
       subcategory: The broad event category for this probe. E.g. navigation
+  library_menu:
+    objects: ["open"]
+    release_channel_collection: opt-out
+    record_in_processes: ["main"]
+    description: >
+      This is recorded any time the library menu is opened.
+    bug_numbers: [1457226, 1465697]
+    notification_emails:
+      - "bdanforth@mozilla.com"
+      - "shong@mozilla.com"
+    expiry_version: "65"
+    extra_keys:
+      subcategory: The broad event category for this probe. E.g. navigation
+  hamburger_menu:
+    objects: ["open"]
+    release_channel_collection: opt-out
+    record_in_processes: ["main"]
+    description: >
+      This is recorded any time the hamburger menu is opened.
+    bug_numbers: [1457226, 1465697]
+    notification_emails:
+      - "bdanforth@mozilla.com"
+      - "shong@mozilla.com"
+    expiry_version: "65"
+    extra_keys:
+      subcategory: The broad event category for this probe. E.g. navigation
+  dotdotdot_menu:
+    objects: ["open"]
+    release_channel_collection: opt-out
+    record_in_processes: ["main"]
+    description: >
+      This is recorded any time the dotdotdot (aka pageAction) menu is opened.
+    bug_numbers: [1457226, 1465697]
+    notification_emails:
+      - "bdanforth@mozilla.com"
+      - "shong@mozilla.com"
+    expiry_version: "65"
+    extra_keys:
+      subcategory: The broad event category for this probe. E.g. navigation
 
 # This category contains event entries used for Telemetry tests.
 # They will not be sent out with any pings.
 telemetry.test:
   test:
     methods: ["test1", "test2"]
     objects: ["object1", "object2"]
     bug_numbers: [1286606]