gecko/browser/modules/BrowserUITelemetry.jsm

422 lines
13 KiB
JavaScript

// This Source Code Form is subject to the terms of the Mozilla Public
// 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/.
"use strict";
this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"];
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
"resource://gre/modules/UITelemetry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyGetter(this, "DEFAULT_TOOLBAR_PLACEMENTS", function() {
let result = {
"PanelUI-contents": [
"edit-controls",
"zoom-controls",
"new-window-button",
"privatebrowsing-button",
"save-page-button",
"print-button",
"history-panelmenu",
"fullscreen-button",
"find-button",
"preferences-button",
"add-ons-button",
],
"nav-bar": [
"urlbar-container",
"search-container",
"webrtc-status-button",
"bookmarks-menu-button",
"downloads-button",
"home-button",
"social-share-button",
],
// It's true that toolbar-menubar is not visible
// on OS X, but the XUL node is definitely present
// in the document.
"toolbar-menubar": [
"menubar-items",
],
"TabsToolbar": [
"tabbrowser-tabs",
"new-tab-button",
"alltabs-button",
"tabs-closebutton",
],
"PersonalToolbar": [
"personal-bookmarks",
],
};
let showCharacterEncoding = Services.prefs.getComplexValue(
"browser.menu.showCharacterEncoding",
Ci.nsIPrefLocalizedString
).data;
if (showCharacterEncoding == "true") {
result["PanelUI-contents"].push("characterencoding-button");
}
if (Services.sysinfo.getProperty("hasWindowsTouchInterface")) {
result["PanelUI-contents"].push("switch-to-metro-button");
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
let result = [
"open-file-button",
"developer-button",
"feed-button",
"email-link-button",
"sync-button",
"tabview-button",
];
let panelPlacements = DEFAULT_TOOLBAR_PLACEMENTS["PanelUI-contents"];
if (panelPlacements.indexOf("characterencoding-button") == -1) {
result.push("characterencoding-button");
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() {
let result = [];
for (let [, buttons] of Iterator(DEFAULT_TOOLBAR_PLACEMENTS)) {
result = result.concat(buttons);
}
return result;
});
XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() {
// These special cases are for click events on built-in items that are
// contained within customizable items (like the navigation widget).
const SPECIAL_CASES = [
"back-button",
"forward-button",
"urlbar-stop-button",
"urlbar-go-button",
"urlbar-reload-button",
"searchbar",
"cut-button",
"copy-button",
"paste-button",
"zoom-out-button",
"zoom-reset-button",
"zoom-in-button",
]
return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
.concat(SPECIAL_CASES);
});
const OTHER_MOUSEUP_MONITORED_ITEMS = [
"PlacesChevron",
"PlacesToolbarItems",
];
this.BrowserUITelemetry = {
init: function() {
UITelemetry.addSimpleMeasureFunction("toolbars",
this.getToolbarMeasures.bind(this));
Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "browser-delayed-startup-finished") {
this._registerWindow(aSubject);
}
},
/**
* For the _countableEvents object, constructs a chain of
* Javascript Objects with the keys in aKeys, with the final
* key getting the value in aEndWith. If the final key already
* exists in the final object, its value is not set. In either
* case, a reference to the second last object in the chain is
* returned.
*
* Example - suppose I want to store:
* _countableEvents: {
* a: {
* b: {
* c: 0
* }
* }
* }
*
* And then increment the "c" value by 1, you could call this
* function like this:
*
* let example = this._ensureObjectChain([a, b, c], 0);
* example["c"]++;
*
* Subsequent repetitions of these last two lines would
* simply result in the c value being incremented again
* and again.
*
* @param aKeys the Array of keys to chain Objects together with.
* @param aEndWith the value to assign to the last key.
* @returns a reference to the second last object in the chain -
* so in our example, that'd be "b".
*/
_ensureObjectChain: function(aKeys, aEndWith) {
let current = this._countableEvents;
let parent = null;
for (let [i, key] of Iterator(aKeys)) {
if (!(key in current)) {
if (i == aKeys.length - 1) {
current[key] = aEndWith;
} else {
current[key] = {};
}
}
parent = current;
current = current[key];
}
return parent;
},
_countableEvents: {},
_countMouseUpEvent: function(aCategory, aAction, aButton) {
const BUTTONS = ["left", "middle", "right"];
let buttonKey = BUTTONS[aButton];
if (buttonKey) {
let countObject =
this._ensureObjectChain([aCategory, aAction, buttonKey], 0);
countObject[buttonKey]++;
}
},
_firstWindowMeasurements: null,
_registerWindow: function(aWindow) {
// We'll gather measurements on the first non-popup window that opens
// after it has painted. We do this here instead of waiting for
// UITelemetry to ask for our measurements because at that point
// all browser windows have probably been closed, since the vast
// majority of saved-session pings are gathered during shutdown.
if (!this._firstWindowMeasurements && aWindow.toolbar.visible) {
this._firstWindowMeasurements = this._getWindowMeasurements(aWindow);
}
aWindow.addEventListener("unload", this);
let document = aWindow.document;
for (let areaID of CustomizableUI.areas) {
let areaNode = document.getElementById(areaID);
if (areaNode) {
(areaNode.customizationTarget || areaNode).addEventListener("mouseup", this);
}
}
for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
let item = document.getElementById(itemID);
if (item) {
item.addEventListener("mouseup", this);
}
}
},
_unregisterWindow: function(aWindow) {
aWindow.removeEventListener("unload", this);
let document = aWindow.document;
for (let areaID of CustomizableUI.areas) {
let areaNode = document.getElementById(areaID);
if (areaNode) {
(areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this);
}
}
for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) {
let item = document.getElementById(itemID);
if (item) {
item.removeEventListener("mouseup", this);
}
}
},
handleEvent: function(aEvent) {
switch(aEvent.type) {
case "unload":
this._unregisterWindow(aEvent.currentTarget);
break;
case "mouseup":
this._handleMouseUp(aEvent);
break;
}
},
_handleMouseUp: function(aEvent) {
let targetID = aEvent.currentTarget.id;
switch (targetID) {
case "PlacesToolbarItems":
this._PlacesToolbarItemsMouseUp(aEvent);
break;
case "PlacesChevron":
this._PlacesChevronMouseUp(aEvent);
break;
default:
this._checkForBuiltinItem(aEvent);
}
},
_PlacesChevronMouseUp: function(aEvent) {
let target = aEvent.originalTarget;
let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item";
this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
},
_PlacesToolbarItemsMouseUp: function(aEvent) {
let target = aEvent.originalTarget;
// If this isn't a bookmark-item, we don't care about it.
if (!target.classList.contains("bookmark-item")) {
return;
}
let result = target.hasAttribute("container") ? "container" : "item";
this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button);
},
_bookmarksMenuButtonMouseUp: function(aEvent) {
let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button");
if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
// In the menu panel, only the star is visible, and that opens up the
// bookmarks subview.
this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel",
aEvent.button);
} else {
let clickedItem = aEvent.originalTarget;
// Did we click on the star, or the dropmarker? The star
// has an anonid of "button". If we don't find that, we'll
// assume we clicked on the dropmarker.
let action = "menu";
if (clickedItem.getAttribute("anonid") == "button") {
// We clicked on the star - now we just need to record
// whether or not we're adding a bookmark or editing an
// existing one.
let bookmarksMenuNode =
bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node;
action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add";
}
this._countMouseUpEvent("click-bookmarks-menu-button", action,
aEvent.button);
}
},
_checkForBuiltinItem: function(aEvent) {
let item = aEvent.originalTarget;
// We special-case the bookmarks-menu-button, since we want to
// monitor more than just clicks on it.
if (item.id == "bookmarks-menu-button" ||
getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") {
this._bookmarksMenuButtonMouseUp(aEvent);
return;
}
// Perhaps we're seeing one of the default toolbar items
// being clicked.
if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
// Base case - we clicked directly on one of our built-in items,
// and we can go ahead and register that click.
this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
return;
}
// If not, we need to check if one of the ancestors of the clicked
// item is in our list of built-in items to check.
let candidate = getIDBasedOnFirstIDedAncestor(item);
if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
}
},
_getWindowMeasurements: function(aWindow) {
let document = aWindow.document;
let result = {};
// Determine if the Bookmarks bar is currently visible
let bookmarksBar = document.getElementById("PersonalToolbar");
result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed;
// Examine all customizable areas and see what default items
// are present and missing.
let defaultKept = [];
let defaultMoved = [];
let nondefaultAdded = [];
for (let areaID of CustomizableUI.areas) {
let items = CustomizableUI.getWidgetIdsInArea(areaID);
for (let item of items) {
// Is this a default item?
if (DEFAULT_ITEMS.indexOf(item) != -1) {
// Ok, it's a default item - but is it in its default
// toolbar? We use Array.isArray instead of checking for
// toolbarID in DEFAULT_TOOLBAR_PLACEMENTS because an add-on might
// be clever and give itself the id of "toString" or something.
if (Array.isArray(DEFAULT_TOOLBAR_PLACEMENTS[areaID]) &&
DEFAULT_TOOLBAR_PLACEMENTS[areaID].indexOf(item) != -1) {
// The item is in its default toolbar
defaultKept.push(item);
} else {
defaultMoved.push(item);
}
} else if (PALETTE_ITEMS.indexOf(item) != -1) {
// It's a palette item that's been moved into a toolbar
nondefaultAdded.push(item);
}
// else, it's provided by an add-on, and we won't record it.
}
}
// Now go through the items in the palette to see what default
// items are in there.
let paletteItems =
CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette);
let defaultRemoved = [item.id for (item of paletteItems)
if (DEFAULT_ITEMS.indexOf(item.id) != -1)];
result.defaultKept = defaultKept;
result.defaultMoved = defaultMoved;
result.nondefaultAdded = nondefaultAdded;
result.defaultRemoved = defaultRemoved;
return result;
},
getToolbarMeasures: function() {
let result = this._firstWindowMeasurements || {};
result.countableEvents = this._countableEvents;
return result;
},
};
/**
* Returns the id of the first ancestor of aNode that has an id. If aNode
* has no parent, or no ancestor has an id, returns null.
*
* @param aNode the node to find the first ID'd ancestor of
*/
function getIDBasedOnFirstIDedAncestor(aNode) {
while (!aNode.id) {
aNode = aNode.parentNode;
if (!aNode) {
return null;
}
}
return aNode.id;
}