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
--- 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]