Bug 1241571 - Collect synced tabs and sidebar ui telemetry. r?Gijs
Adds UI telemetry for sidebar opening and closing and actions taken in the
SyncedTabs menu and side. Also adds a "sync-state" object so that analysis
of the Synced Tabs data can determine if the user has Sync configured at the
time.
MozReview-Commit-ID: JDxFmlNMi7n
--- a/browser/base/content/browser-sidebar.js
+++ b/browser/base/content/browser-sidebar.js
@@ -190,16 +190,20 @@ var SidebarUI = {
show(commandID) {
return new Promise((resolve, reject) => {
let sidebarBroadcaster = document.getElementById(commandID);
if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
reject(new Error("Invalid sidebar broadcaster specified: " + commandID));
return;
}
+ if (this.isOpen && commandID != this.currentID) {
+ BrowserUITelemetry.countSidebarEvent(this.currentID, "hide");
+ }
+
let broadcasters = document.getElementsByAttribute("group", "sidebar");
for (let broadcaster of broadcasters) {
// skip elements that observe sidebar broadcasters and random
// other elements
if (broadcaster.localName != "broadcaster") {
continue;
}
@@ -252,16 +256,17 @@ var SidebarUI = {
this._fireFocusedEvent();
resolve();
}
let selBrowser = gBrowser.selectedBrowser;
selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
{commandID: commandID, isOpen: true}
);
+ BrowserUITelemetry.countSidebarEvent(commandID, "show");
});
},
/**
* Hide the sidebar.
*/
hide() {
if (!this.isOpen) {
@@ -289,16 +294,17 @@ var SidebarUI = {
this._box.hidden = true;
this._splitter.hidden = true;
let selBrowser = gBrowser.selectedBrowser;
selBrowser.focus();
selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
{commandID: commandID, isOpen: false}
);
+ BrowserUITelemetry.countSidebarEvent(commandID, "hide");
},
};
/**
* This exists for backards compatibility - it will be called once a sidebar is
* ready, following any request to show it.
*
* @deprecated
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -482,16 +482,17 @@ const CustomizableWidgets = [
item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
item.setAttribute("image", tabInfo.icon);
item.setAttribute("tooltiptext", tooltipText);
// We need to use "click" instead of "command" here so openUILink
// respects different buttons (eg, to open in a new tab).
item.addEventListener("click", e => {
doc.defaultView.openUILink(tabInfo.url, e);
CustomizableUI.hidePanelForNode(item);
+ BrowserUITelemetry.countSyncedTabEvent("open", "toolbarbutton-subview");
});
return item;
},
}, {
id: "privatebrowsing-button",
shortcutId: "key_privatebrowsing",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(e) {
--- a/browser/components/syncedtabs/TabListComponent.js
+++ b/browser/components/syncedtabs/TabListComponent.js
@@ -1,19 +1,24 @@
/* 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";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("Sync.RemoteTabs");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
+ "resource:///modules/BrowserUITelemetry.jsm");
+
this.EXPORTED_SYMBOLS = [
"TabListComponent"
];
/**
* TabListComponent
*
* The purpose of this component is to compose the view, state, and actions.
@@ -100,16 +105,17 @@ TabListComponent.prototype = {
onBookmarkTab(uri, title) {
this._window.top.PlacesCommandHook
.bookmarkLink(this._window.top.PlacesUtils.bookmarksMenuFolderId, uri, title)
.catch(Cu.reportError);
},
onOpenTab(url, where, params) {
this._window.openUILinkIn(url, where, params);
+ BrowserUITelemetry.countSyncedTabEvent("open", "sidebar");
},
onCopyTabLocation(url) {
this._clipboardHelper.copyString(url);
},
onSyncRefresh() {
this._SyncedTabs.syncTabs(true);
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -174,16 +174,19 @@ this.BrowserUITelemetry = {
this.getToolbarMeasures.bind(this));
UITelemetry.addSimpleMeasureFunction("contextmenu",
this.getContextMenuInfo.bind(this));
// Ensure that UITour.jsm remains lazy-loaded, yet always registers its
// simple measure function with UITelemetry.
UITelemetry.addSimpleMeasureFunction("UITour",
() => UITour.getTelemetry());
+ UITelemetry.addSimpleMeasureFunction("syncstate",
+ this.getSyncState.bind(this));
+
Services.obs.addObserver(this, "sessionstore-windows-restored", false);
Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
Services.obs.addObserver(this, "autocomplete-did-enter-text", false);
CustomizableUI.addListener(this);
},
observe: function(aSubject, aTopic, aData) {
switch(aTopic) {
@@ -580,16 +583,28 @@ this.BrowserUITelemetry = {
getToolbarMeasures: function() {
let result = this._firstWindowMeasurements || {};
result.countableEvents = this._countableEvents;
result.durations = this._durations;
return result;
},
+ getSyncState: function() {
+ let result = {};
+ for (let sub of ["desktop", "mobile"]) {
+ let count = 0;
+ try {
+ count = Services.prefs.getIntPref("services.sync.clients.devices." + sub);
+ } catch (ex) {}
+ result[sub] = count;
+ }
+ return result;
+ },
+
countCustomizationEvent: function(aEventType) {
this._countEvent(["customize", aEventType]);
},
countSearchEvent: function(source, query, selection) {
this._countEvent(["search", source]);
if ((/^[a-zA-Z]+:[^\/\\]/).test(query)) {
this._countEvent(["search", "urlbar-keyword"]);
@@ -610,16 +625,28 @@ this.BrowserUITelemetry = {
countPanicEvent: function(timeId) {
this._countEvent(["forget-button", timeId]);
},
countTabMutingEvent: function(action, reason) {
this._countEvent(["tab-audio-control", action, reason || "no reason given"]);
},
+ countSyncedTabEvent: function(what, where) {
+ // "what" will be, eg, "open"
+ // "where" will be "menu" or "sidebar"
+ this._countEvent(["synced-tabs", what, where]);
+ },
+
+ countSidebarEvent: function(sidebarID, action) {
+ // sidebarID is the ID of the sidebar (duh!)
+ // action will be "hide" or "show"
+ this._countEvent(["sidebar", sidebarID, action]);
+ },
+
_logAwesomeBarSearchResult: function (url) {
let spec = Services.search.parseSubmissionURL(url);
if (spec.engine) {
let matchedEngine = "default";
if (spec.engine.name !== Services.search.currentEngine.name) {
matchedEngine = "other";
}
this.countSearchEvent("autocomplete-" + matchedEngine);
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -1,13 +1,15 @@
[DEFAULT]
support-files =
head.js
[browser_BrowserUITelemetry_buckets.js]
+[browser_BrowserUITelemetry_sidebar.js]
+[browser_BrowserUITelemetry_syncedtabs.js]
[browser_ProcessHangNotifications.js]
skip-if = !e10s
[browser_ContentSearch.js]
support-files =
contentSearch.js
contentSearchBadImage.xml
contentSearchSuggestions.sjs
contentSearchSuggestions.xml
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_BrowserUITelemetry_sidebar.js
@@ -0,0 +1,56 @@
+// Test the sidebar counters in BrowserUITelemetry.
+"use strict";
+
+const { BrowserUITelemetry: BUIT } = Cu.import("resource:///modules/BrowserUITelemetry.jsm", {});
+
+add_task(function* testSidebarOpenClose() {
+ // Reset BrowserUITelemetry's world.
+ BUIT._countableEvents = {};
+
+ yield SidebarUI.show("viewTabsSidebar");
+
+ let counts = BUIT._countableEvents[BUIT.currentBucket];
+
+ Assert.deepEqual(counts, { sidebar: { viewTabsSidebar: { show: 1 } } });
+ yield SidebarUI.hide();
+ Assert.deepEqual(counts, { sidebar: { viewTabsSidebar: { show: 1, hide: 1 } } });
+
+ yield SidebarUI.show("viewBookmarksSidebar");
+ Assert.deepEqual(counts, {
+ sidebar: {
+ viewTabsSidebar: { show: 1, hide: 1 },
+ viewBookmarksSidebar: { show: 1 },
+ }
+ });
+ // Re-open the tabs sidebar while bookmarks is open - bookmarks should
+ // record a close.
+ yield SidebarUI.show("viewTabsSidebar");
+ Assert.deepEqual(counts, {
+ sidebar: {
+ viewTabsSidebar: { show: 2, hide: 1 },
+ viewBookmarksSidebar: { show: 1, hide: 1 },
+ }
+ });
+ yield SidebarUI.hide();
+ Assert.deepEqual(counts, {
+ sidebar: {
+ viewTabsSidebar: { show: 2, hide: 2 },
+ viewBookmarksSidebar: { show: 1, hide: 1 },
+ }
+ });
+ // Toggle - this will re-open viewTabsSidebar
+ yield SidebarUI.toggle("viewTabsSidebar");
+ Assert.deepEqual(counts, {
+ sidebar: {
+ viewTabsSidebar: { show: 3, hide: 2 },
+ viewBookmarksSidebar: { show: 1, hide: 1 },
+ }
+ });
+ yield SidebarUI.toggle("viewTabsSidebar");
+ Assert.deepEqual(counts, {
+ sidebar: {
+ viewTabsSidebar: { show: 3, hide: 3 },
+ viewBookmarksSidebar: { show: 1, hide: 1 },
+ }
+ });
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_BrowserUITelemetry_syncedtabs.js
@@ -0,0 +1,114 @@
+// Test the SyncedTabs counters in BrowserUITelemetry.
+"use strict";
+
+const { BrowserUITelemetry: BUIT } = Cu.import("resource:///modules/BrowserUITelemetry.jsm", {});
+const {SyncedTabs} = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
+
+function mockSyncedTabs() {
+ // Mock SyncedTabs.jsm
+ let mockedInternal = {
+ get isConfiguredToSyncTabs() { return true; },
+ getTabClients() {
+ return Promise.resolve([
+ {
+ id: "guid_desktop",
+ type: "client",
+ name: "My Desktop",
+ tabs: [
+ {
+ title: "http://example.com/10",
+ lastUsed: 10, // the most recent
+ },
+ ],
+ }
+ ]);
+ },
+ syncTabs() {
+ return Promise.resolve();
+ },
+ hasSyncedThisSession: true,
+ };
+
+ let oldInternal = SyncedTabs._internal;
+ SyncedTabs._internal = mockedInternal;
+
+ // configure our broadcasters so we are in the right state.
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = false;
+
+ registerCleanupFunction(() => {
+ SyncedTabs._internal = oldInternal;
+
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = false;
+ document.getElementById("sync-syncnow-state").hidden = true;
+ });
+}
+
+mockSyncedTabs();
+
+function promiseTabsUpdated() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(onNotification, aTopic);
+ resolve();
+ }, "synced-tabs-menu:test:tabs-updated", false);
+ });
+}
+
+add_task(function* test_menu() {
+ // Reset BrowserUITelemetry's world.
+ BUIT._countableEvents = {};
+
+ let tabsUpdated = promiseTabsUpdated();
+
+ // check the button's functionality
+ yield PanelUI.show();
+
+ let syncButton = document.getElementById("sync-button");
+ syncButton.click();
+
+ yield tabsUpdated;
+ // Get our 1 tab and click on it.
+ let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
+ let tabEntry = tabList.firstChild.nextSibling;
+ tabEntry.click();
+
+ let counts = BUIT._countableEvents[BUIT.currentBucket];
+ Assert.deepEqual(counts, {
+ "click-builtin-item": { "sync-button": { left: 1 } },
+ "synced-tabs": { open: { "toolbarbutton-subview": 1 } },
+ });
+});
+
+add_task(function* test_sidebar() {
+ // Reset BrowserUITelemetry's world.
+ BUIT._countableEvents = {};
+
+ yield SidebarUI.show('viewTabsSidebar');
+
+ let syncedTabsDeckComponent = SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
+
+ syncedTabsDeckComponent._accountStatus = () => Promise.resolve(true);
+
+ // Once the tabs container has been selected (which here means "'selected'
+ // added to the class list") we are ready to test.
+ let container = SidebarUI.browser.contentDocument.querySelector(".tabs-container");
+ let promiseUpdated = BrowserTestUtils.waitForAttribute("class", container);
+
+ yield syncedTabsDeckComponent.updatePanel();
+ yield promiseUpdated;
+
+ let selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected");
+ let tab = selectedPanel.querySelector(".tab");
+ tab.click();
+ let counts = BUIT._countableEvents[BUIT.currentBucket];
+ Assert.deepEqual(counts, {
+ sidebar: {
+ viewTabsSidebar: { show: 1 },
+ },
+ "synced-tabs": { open: { sidebar: 1 } }
+ });
+ yield SidebarUI.hide();
+});
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -220,31 +220,36 @@ ClientEngine.prototype = {
},
_uploadOutgoing() {
this._clearedCommands = null;
SyncEngine.prototype._uploadOutgoing.call(this);
},
_syncFinish() {
- // Record telemetry for our device types.
+ // Record histograms for our device types, and also write them to a pref
+ // so non-histogram telemetry (eg, UITelemetry) has easy access to them.
for (let [deviceType, count] of this.deviceTypes) {
let hid;
+ let prefName = this.name + ".devices.";
switch (deviceType) {
case "desktop":
hid = "WEAVE_DEVICE_COUNT_DESKTOP";
+ prefName += "desktop";
break;
case "mobile":
hid = "WEAVE_DEVICE_COUNT_MOBILE";
+ prefName += "mobile";
break;
default:
this._log.warn(`Unexpected deviceType "${deviceType}" recording device telemetry.`);
continue;
}
Services.telemetry.getHistogramById(hid).add(count);
+ Svc.Prefs.set(prefName, count);
}
SyncEngine.prototype._syncFinish.call(this);
},
_reconcile: function _reconcile(item) {
// Every incoming record is reconciled, so we use this to track the
// contents of the collection on the server.
this._incomingClients[item.id] = item.modified;