Bug 1207089 - Telemetry for permission notifications. r=MattN,vladan
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 27 Oct 2015 14:24:51 +0000
changeset 305112 781512105804f3d2580aedd0f6b17fafe5e7c485
parent 305111 8edcaa544e24e081a7be9613e83b2f4d0f14551b
child 305113 d2bb527a1e8a1c351640b2624e3dd1a0487488a2
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, vladan
bugs1207089
milestone44.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 1207089 - Telemetry for permission notifications. r=MattN,vladan
toolkit/components/telemetry/Histograms.json
toolkit/content/widgets/notification.xml
toolkit/modules/PopupNotifications.jsm
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5703,23 +5703,43 @@
     "kind": "boolean",
     "description": "Count the number of times the user clicked 'block' on the hidden-plugin infobar."
   },
   "PLUGINS_INFOBAR_ALLOW": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Count the number of times the user clicked 'allow' on the hidden-plugin infobar."
   },
-  "POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS": {
-    "expires_in_version": "40",
-    "kind": "linear",
-    "low": 25,
-    "high": "80 * 25",
-    "n_buckets": "80 + 1",
-    "description": "The time (in milliseconds) after showing a PopupNotification that the mainAction was first triggered"
+  "POPUP_NOTIFICATION_STATS": {
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "48",
+    "kind": "enumerated",
+    "keyed": true,
+    "n_values": 40,
+    "description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
+  },
+  "POPUP_NOTIFICATION_MAIN_ACTION_MS": {
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "48",
+    "kind": "exponential",
+    "keyed": true,
+    "low": 100,
+    "high": 600000,
+    "n_buckets": 40,
+    "description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID"
+  },
+  "POPUP_NOTIFICATION_DISMISSAL_MS": {
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "48",
+    "kind": "exponential",
+    "keyed": true,
+    "low": 200,
+    "high": 20000,
+    "n_buckets": 50,
+    "description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID"
   },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_RELOAD_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'reload' request to go round trip."
   },
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -487,25 +487,25 @@
           </xul:vbox>
           <xul:toolbarbutton anonid="closebutton"
                              class="messageCloseButton close-icon popup-notification-closebutton tabbable"
                              xbl:inherits="oncommand=closebuttoncommand"
                              tooltiptext="&closeNotification.tooltip;"/>
         </xul:hbox>
         <children includes="popupnotificationcontent"/>
         <xul:label class="text-link popup-notification-learnmore-link"
-               xbl:inherits="href=learnmoreurl">&learnMore;</xul:label>
+               xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label>
         <xul:spacer flex="1"/>
         <xul:hbox class="popup-notification-button-container"
                   pack="end" align="center">
           <children includes="button"/>
           <xul:button anonid="button"
                       class="popup-notification-menubutton"
                       type="menu-button"
-                      xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
+                      xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey">
             <xul:menupopup anonid="menupopup"
                            xbl:inherits="oncommand=menucommand">
               <children/>
               <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
                             label="&closeNotificationItem.label;"
                             xbl:inherits="oncommand=closeitemcommand,hidden=hidenotnow"/>
             </xul:menupopup>
           </xul:button>
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -2,30 +2,46 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 this.EXPORTED_SYMBOLS = ["PopupNotifications"];
 
 var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
 const NOTIFICATION_EVENT_REMOVED = "removed";
 const NOTIFICATION_EVENT_SHOWING = "showing";
 const NOTIFICATION_EVENT_SHOWN = "shown";
 const NOTIFICATION_EVENT_SWAPPING = "swapping";
 
 const ICON_SELECTOR = ".notification-anchor-icon";
 const ICON_ATTRIBUTE_SHOWING = "showing";
 const ICON_ANCHOR_ATTRIBUTE = "popupnotificationanchor";
 
 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
 
+// Enumerated values for the POPUP_NOTIFICATION_STATS telemetry histogram.
+const TELEMETRY_STAT_OFFERED = 0;
+const TELEMETRY_STAT_ACTION_1 = 1;
+const TELEMETRY_STAT_ACTION_2 = 2;
+const TELEMETRY_STAT_ACTION_3 = 3;
+const TELEMETRY_STAT_ACTION_LAST = 4;
+const TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE = 5;
+const TELEMETRY_STAT_DISMISSAL_LEAVE_PAGE = 6;
+const TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON = 7;
+const TELEMETRY_STAT_DISMISSAL_NOT_NOW = 8;
+const TELEMETRY_STAT_OPEN_SUBMENU = 10;
+const TELEMETRY_STAT_LEARN_MORE = 11;
+
+const TELEMETRY_STAT_REOPENED_OFFSET = 20;
+
 var popupNotificationsMap = new WeakMap();
 var gNotificationParents = new WeakMap;
 
 function getAnchorFromBrowser(aBrowser, aAnchorID) {
   let attrPrefix = aAnchorID ? aAnchorID.replace("notification-icon", "") : "";
   let anchor = aBrowser.getAttribute(attrPrefix + ICON_ANCHOR_ATTRIBUTE) ||
                aBrowser[attrPrefix + ICON_ANCHOR_ATTRIBUTE] ||
                aBrowser.getAttribute(ICON_ANCHOR_ATTRIBUTE) ||
@@ -49,31 +65,52 @@ function Notification(id, message, ancho
   this.id = id;
   this.message = message;
   this.anchorID = anchorID;
   this.mainAction = mainAction;
   this.secondaryActions = secondaryActions || [];
   this.browser = browser;
   this.owner = owner;
   this.options = options || {};
+
+  this._dismissed = false;
+  this.wasDismissed = false;
+  this.recordedTelemetryStats = new Set();
+  this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(
+                                        this.browser.ownerDocument.defaultView);
+  this.timeCreated = this.owner.window.performance.now();
 }
 
 Notification.prototype = {
 
   id: null,
   message: null,
   anchorID: null,
   mainAction: null,
   secondaryActions: null,
   browser: null,
   owner: null,
   options: null,
   timeShown: null,
 
   /**
+   * Indicates whether the notification is currently dismissed.
+   */
+  set dismissed(value) {
+    this._dismissed = value;
+    if (value) {
+      // Keep the dismissal into account when recording telemetry.
+      this.wasDismissed = true;
+    }
+  },
+  get dismissed() {
+    return this._dismissed;
+  },
+
+  /**
    * Removes the notification and updates the popup accordingly if needed.
    */
   remove: function Notification_remove() {
     this.owner.remove(this);
   },
 
   get anchorElement() {
     let iconBox = this.owner.iconBox;
@@ -90,17 +127,55 @@ Notification.prototype = {
       anchorElement = iconBox.querySelector("#default-notification-icon") ||
                       iconBox;
 
     return anchorElement;
   },
 
   reshow: function() {
     this.owner._reshowNotifications(this.anchorElement, this.browser);
-  }
+  },
+
+  /**
+   * Adds a value to the specified histogram, that must be keyed by ID.
+   */
+  _recordTelemetry(histogramId, value) {
+    if (this.isPrivate) {
+      // The reason why we don't record telemetry in private windows is because
+      // the available actions can be different from regular mode. The main
+      // difference is that all of the persistent permission options like
+      // "Always remember" aren't there, so they really need to be handled
+      // separately to avoid skewing results. For notifications with the same
+      // choices, there would be no reason not to record in private windows as
+      // well, but it's just simpler to use the same check for everything.
+      return;
+    }
+    let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+    histogram.add("(all)", value);
+    histogram.add(this.id, value);
+  },
+
+  /**
+   * Adds an enumerated value to the POPUP_NOTIFICATION_STATS histogram,
+   * ensuring that it is recorded at most once for each distinct Notification.
+   *
+   * Statistics for reopened notifications are recorded in separate buckets.
+   *
+   * @param value
+   *        One of the TELEMETRY_STAT_ constants.
+   */
+  _recordTelemetryStat(value) {
+    if (this.wasDismissed) {
+      value += TELEMETRY_STAT_REOPENED_OFFSET;
+    }
+    if (!this.recordedTelemetryStats.has(value)) {
+      this.recordedTelemetryStats.add(value);
+      this._recordTelemetry("POPUP_NOTIFICATION_STATS", value);
+    }
+  },
 };
 
 /**
  * The PopupNotifications object manages popup notifications for a given browser
  * window.
  * @param tabbrowser
  *        window's <xul:tabbrowser/>. Used to observe tab switching events and
  *        for determining the active browser element.
@@ -411,16 +486,22 @@ PopupNotifications.prototype = {
   handleEvent: function (aEvent) {
     switch (aEvent.type) {
       case "popuphidden":
         this._onPopupHidden(aEvent);
         break;
       case "activate":
       case "TabSelect":
         let self = this;
+        // This is where we could detect if the panel is dismissed if the page
+        // was switched. Unfortunately, the user usually has clicked elsewhere
+        // at this point so this value only gets recorded for programmatic
+        // reasons, like the "Learn More" link being clicked and resulting in a
+        // tab switch.
+        this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_LEAVE_PAGE;
         // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
         // handler results in the popup being hidden again for some reason...
         this.window.setTimeout(function () {
           self._update();
         }, 0);
         break;
       case "click":
       case "keypress":
@@ -460,17 +541,21 @@ PopupNotifications.prototype = {
     // remove the notification
     notifications.splice(index, 1);
     this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
   },
 
   /**
    * Dismisses the notification without removing it.
    */
-  _dismiss: function PopupNotifications_dismiss() {
+  _dismiss: function PopupNotifications_dismiss(telemetryReason) {
+    if (telemetryReason) {
+      this.nextDismissReason = telemetryReason;
+    }
+
     let browser = this.panel.firstChild &&
                   this.panel.firstChild.notification.browser;
     this.panel.hidePopup();
     if (browser)
       browser.focus();
   },
 
   /**
@@ -541,27 +626,31 @@ PopupNotifications.prototype = {
       if (popupnotification)
         gNotificationParents.set(popupnotification, popupnotification.parentNode);
       else
         popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
 
       popupnotification.setAttribute("label", n.message);
       popupnotification.setAttribute("id", popupnotificationID);
       popupnotification.setAttribute("popupid", n.id);
-      popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
+      popupnotification.setAttribute("closebuttoncommand", `PopupNotifications._dismiss(${TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON});`);
       if (n.mainAction) {
         popupnotification.setAttribute("buttonlabel", n.mainAction.label);
         popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
-        popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
+        popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonEvent(event, 'buttoncommand');");
+        popupnotification.setAttribute("buttonpopupshown", "PopupNotifications._onButtonEvent(event, 'buttonpopupshown');");
+        popupnotification.setAttribute("learnmoreclick", "PopupNotifications._onButtonEvent(event, 'learnmoreclick');");
         popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
-        popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
+        popupnotification.setAttribute("closeitemcommand", `PopupNotifications._dismiss(${TELEMETRY_STAT_DISMISSAL_NOT_NOW});event.stopPropagation();`);
       } else {
         popupnotification.removeAttribute("buttonlabel");
         popupnotification.removeAttribute("buttonaccesskey");
         popupnotification.removeAttribute("buttoncommand");
+        popupnotification.removeAttribute("buttonpopupshown");
+        popupnotification.removeAttribute("learnmoreclick");
         popupnotification.removeAttribute("menucommand");
         popupnotification.removeAttribute("closeitemcommand");
       }
 
       if (n.options.popupIconURL)
         popupnotification.setAttribute("icon", n.options.popupIconURL);
 
       if (n.options.learnMoreURL)
@@ -583,24 +672,33 @@ PopupNotifications.prototype = {
           popupnotification.removeAttribute("origin");
         }
       } else
         popupnotification.removeAttribute("origin");
 
       popupnotification.notification = n;
 
       if (n.secondaryActions) {
+        let telemetryStatId = TELEMETRY_STAT_ACTION_2;
+
         n.secondaryActions.forEach(function (a) {
           let item = doc.createElementNS(XUL_NS, "menuitem");
           item.setAttribute("label", a.label);
           item.setAttribute("accesskey", a.accessKey);
           item.notification = n;
           item.action = a;
 
           popupnotification.appendChild(item);
+
+          // We can only record a limited number of actions in telemetry. If
+          // there are more, the latest are all recorded in the last bucket.
+          item.action.telemetryStatId = telemetryStatId;
+          if (telemetryStatId < TELEMETRY_STAT_ACTION_LAST) {
+            telemetryStatId++;
+          }
         }, this);
 
         if (n.options.hideNotNow) {
           popupnotification.setAttribute("hidenotnow", "true");
         }
         else if (n.secondaryActions.length) {
           let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
           popupnotification.appendChild(closeItemSeparator);
@@ -653,19 +751,28 @@ PopupNotifications.prototype = {
       }
 
       this._currentAnchorElement = anchorElement;
 
       // On OS X and Linux we need a different panel arrow color for
       // click-to-play plugins, so copy the popupid and use css.
       this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
       notificationsToShow.forEach(function (n) {
+        // Record that the notification was actually displayed on screen.
+        // Notifications that were opened a second time or that were originally
+        // shown with "options.dismissed" will be recorded in a separate bucket.
+        n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
         // Remember the time the notification was shown for the security delay.
         n.timeShown = this.window.performance.now();
       }, this);
+
+      // Unless the panel closing is triggered by a specific known code path,
+      // the next reason will be that the user clicked elsewhere.
+      this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE;
+
       this.panel.openPopup(anchorElement, "bottomcenter topleft");
       notificationsToShow.forEach(function (n) {
         this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
       }, this);
       // This notification is used by tests to know when all the processing
       // required to display the panel has happened.
       this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
     });
@@ -974,60 +1081,85 @@ PopupNotifications.prototype = {
     let notifications = this._getNotificationsForBrowser(browser);
     // Mark notifications as dismissed and call dismissal callbacks
     Array.forEach(this.panel.childNodes, function (nEl) {
       let notificationObj = nEl.notification;
       // Never call a dismissal handler on a notification that's been removed.
       if (notifications.indexOf(notificationObj) == -1)
         return;
 
+      // Record the time of the first notification dismissal if the main action
+      // was not triggered in the meantime.
+      let timeSinceShown = this.window.performance.now() - notificationObj.timeShown;
+      if (!notificationObj.wasDismissed &&
+          !notificationObj.recordedTelemetryMainAction) {
+        notificationObj._recordTelemetry("POPUP_NOTIFICATION_DISMISSAL_MS",
+                                         timeSinceShown);
+      }
+      notificationObj._recordTelemetryStat(this.nextDismissReason);
+
       // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
       // if the notification is removed.
       if (notificationObj.options.removeOnDismissal) {
         this._remove(notificationObj);
       } else {
         notificationObj.dismissed = true;
         this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
       }
     }, this);
   },
 
-  _onButtonCommand: function PopupNotifications_onButtonCommand(event) {
+  _onButtonEvent(event, type) {
     // Need to find the associated notification object, which is a bit tricky
     // since it isn't associated with the button directly - this is kind of
     // gross and very dependent on the structure of the popupnotification
     // binding's content.
     let target = event.originalTarget;
     let notificationEl;
     let parent = target;
     while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
       notificationEl = parent;
 
     if (!notificationEl)
-      throw "PopupNotifications_onButtonCommand: couldn't find notification element";
+      throw "PopupNotifications._onButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
-      throw "PopupNotifications_onButtonCommand: couldn't find notification";
+      throw "PopupNotifications._onButtonEvent: couldn't find notification";
 
     let notification = notificationEl.notification;
-    let timeSinceShown = this.window.performance.now() - notification.timeShown;
 
-    // Only report the first time mainAction is triggered and remember that this occurred.
-    if (!notification.timeMainActionFirstTriggered) {
-      notification.timeMainActionFirstTriggered = timeSinceShown;
-      Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
-                         add(timeSinceShown);
+    if (type == "buttonpopupshown") {
+      notification._recordTelemetryStat(TELEMETRY_STAT_OPEN_SUBMENU);
+      return;
+    }
+
+    if (type == "learnmoreclick") {
+      notification._recordTelemetryStat(TELEMETRY_STAT_LEARN_MORE);
+      return;
     }
 
+    // Record the total timing of the main action since the notification was
+    // created, even if the notification was dismissed in the meantime.
+    let timeSinceCreated = this.window.performance.now() - notification.timeCreated;
+    if (!notification.recordedTelemetryMainAction) {
+      notification.recordedTelemetryMainAction = true;
+      notification._recordTelemetry("POPUP_NOTIFICATION_MAIN_ACTION_MS",
+                                    timeSinceCreated);
+    }
+
+    let timeSinceShown = this.window.performance.now() - notification.timeShown;
     if (timeSinceShown < this.buttonDelay) {
-      Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
+      Services.console.logStringMessage("PopupNotifications._onButtonEvent: " +
                                         "Button click happened before the security delay: " +
                                         timeSinceShown + "ms");
       return;
     }
+
+    notification._recordTelemetryStat(TELEMETRY_STAT_ACTION_1);
+
     try {
       notification.mainAction.callback.call();
     } catch(error) {
       Cu.reportError(error);
     }
 
     if (notification.mainAction.dismiss) {
       this._dismiss();
@@ -1039,16 +1171,19 @@ PopupNotifications.prototype = {
   },
 
   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
     let target = event.originalTarget;
     if (!target.action || !target.notification)
       throw "menucommand target has no associated action/notification";
 
     event.stopPropagation();
+
+    target.notification._recordTelemetryStat(target.action.telemetryStatId);
+
     try {
       target.action.callback.call();
     } catch(error) {
       Cu.reportError(error);
     }
 
     if (target.action.dismiss) {
       this._dismiss();