Bug 1207089 - Telemetry for permission notifications. r=MattN,vladan

This commit is contained in:
Paolo Amadini 2015-10-27 14:24:51 +00:00
parent 7eb09f8840
commit e644940cbc
3 changed files with 179 additions and 24 deletions

View File

@ -5708,13 +5708,33 @@
"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",

View File

@ -492,7 +492,7 @@
</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">
@ -500,7 +500,7 @@
<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/>

View File

@ -7,6 +7,7 @@ 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";
@ -21,6 +22,21 @@ 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;
@ -54,6 +70,13 @@ function Notification(id, message, anchorID, mainAction, 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 = {
@ -68,6 +91,20 @@ Notification.prototype = {
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.
*/
@ -95,7 +132,45 @@ Notification.prototype = {
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);
}
},
};
/**
@ -416,6 +491,12 @@ PopupNotifications.prototype = {
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 () {
@ -465,7 +546,11 @@ PopupNotifications.prototype = {
/**
* 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();
@ -546,17 +631,21 @@ PopupNotifications.prototype = {
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");
}
@ -588,6 +677,8 @@ PopupNotifications.prototype = {
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);
@ -596,6 +687,13 @@ PopupNotifications.prototype = {
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) {
@ -658,9 +756,18 @@ PopupNotifications.prototype = {
// 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);
@ -979,6 +1086,16 @@ PopupNotifications.prototype = {
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) {
@ -990,7 +1107,7 @@ PopupNotifications.prototype = {
}, 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
@ -1002,27 +1119,42 @@ PopupNotifications.prototype = {
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) {
@ -1044,6 +1176,9 @@ PopupNotifications.prototype = {
throw "menucommand target has no associated action/notification";
event.stopPropagation();
target.notification._recordTelemetryStat(target.action.telemetryStatId);
try {
target.action.callback.call();
} catch(error) {