Bug 1241571 - Collect synced tabs and sidebar ui telemetry. r?Gijs draft
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 21 Apr 2016 08:43:36 +1000
changeset 367661 cd255abe72c7e4365eea53cddd7144cc681aa2bb
parent 367617 a7e57536b61c186973b8ef1a5efccf630e60d068
child 521074 247c6fee20a9e49002ee55d692cb4d6e2ca54aa4
push id18312
push userbmo:markh@mozilla.com
push dateTue, 17 May 2016 06:26:00 +0000
reviewersGijs
bugs1241571
milestone49.0a1
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
browser/base/content/browser-sidebar.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/syncedtabs/TabListComponent.js
browser/modules/BrowserUITelemetry.jsm
browser/modules/test/browser.ini
browser/modules/test/browser_BrowserUITelemetry_sidebar.js
browser/modules/test/browser_BrowserUITelemetry_syncedtabs.js
services/sync/modules/engines/clients.js
--- 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;