Bug 1241571 - Collect synced tabs and sidebar ui telemetry. r=Gijs
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 21 Apr 2016 08:43:36 +1000
changeset 336739 59d131412f855525aff3e66b38a251d5c4feb3c0
parent 336738 64ad4c149ac09500108a684e9799664257c3d799
child 336740 079e427f6ac37e9632584bb60e55c73e4fd4e32c
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1241571
milestone49.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 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
@@ -484,16 +484,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
@@ -175,16 +175,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) {
@@ -581,16 +584,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"]);
@@ -611,16 +626,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 "toolbarbutton-subview" 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,14 +1,16 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_BrowserUITelemetry_buckets.js]
 [browser_BrowserUITelemetry_defaults.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;