Bug 1463095 - Instrument inspection of filter changes in the Web Console with event telemetry draft
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Mon, 02 Jul 2018 14:06:21 +0100
changeset 815599 238348de85c924ddc4a8d524768fa395bb4eba13
parent 814704 afdeb0288690f0acde2ba6bd008f860e1acdd026
push id115564
push usermratcliffe@mozilla.com
push dateMon, 09 Jul 2018 13:21:01 +0000
bugs1463095
milestone63.0a1
Bug 1463095 - Instrument inspection of filter changes in the Web Console with event telemetry There are also a couple of telemetry.md changes that are not worth splitting out into a different bug. MozReview-Commit-ID: JyzQs4NDe3j
devtools/client/webconsole/actions/filters.js
devtools/client/webconsole/components/FilterButton.js
devtools/client/webconsole/test/mochitest/browser_console_filters.js
devtools/docs/frontend/telemetry.md
toolkit/components/telemetry/Events.yaml
--- a/devtools/client/webconsole/actions/filters.js
+++ b/devtools/client/webconsole/actions/filters.js
@@ -2,16 +2,18 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 { getAllFilters } = require("devtools/client/webconsole/selectors/filters");
+const Telemetry = require("devtools/client/shared/telemetry");
+const telemetry = new Telemetry();
 
 const {
   FILTER_TEXT_SET,
   FILTER_TOGGLE,
   FILTERS_CLEAR,
   DEFAULT_FILTERS_RESET,
   PREFS,
   FILTERS,
@@ -20,24 +22,36 @@ const {
 
 function filterTextSet(text) {
   return {
     type: FILTER_TEXT_SET,
     text
   };
 }
 
-function filterToggle(filter) {
+function filterToggle(filter, sessionId) {
   return (dispatch, getState, {prefsService}) => {
     dispatch({
       type: FILTER_TOGGLE,
       filter,
     });
     const filterState = getAllFilters(getState());
-    prefsService.setBoolPref(PREFS.FILTER[filter.toUpperCase()], filterState[filter]);
+    const state = filterState[filter];
+    const active = Object.keys(filterState)
+                         .filter(key => key !== "text" ? filterState[key] : "");
+    const inactive = Object.keys(filterState)
+                           .filter(key => key !== "text" ? !filterState[key] : "");
+
+    telemetry.recordEvent("devtools.main", "filter_changed", "webconsole", null, {
+      "clicked": filter,
+      "active": active.join(","),
+      "inactive": inactive.join(","),
+      "session_id": sessionId
+    });
+    prefsService.setBoolPref(PREFS.FILTER[filter.toUpperCase()], state);
   };
 }
 
 function filtersClear() {
   return (dispatch, getState, {prefsService}) => {
     dispatch({
       type: FILTERS_CLEAR,
     });
--- a/devtools/client/webconsole/components/FilterButton.js
+++ b/devtools/client/webconsole/components/FilterButton.js
@@ -24,15 +24,22 @@ function FilterButton(props) {
   ];
   if (active) {
     classList.push("checked");
   }
 
   return dom.button({
     "aria-pressed": active === true,
     className: classList.join(" "),
-    onClick: () => {
-      dispatch(actions.filterToggle(filterKey));
+    onClick: event => {
+      const frame = event.target.ownerGlobal.frameElement;
+      let sessionId = -1;
+
+      if (frame && frame.ownerGlobal.frameElement) {
+        sessionId = frame.ownerGlobal.frameElement.getAttribute("session_id");
+      }
+
+      dispatch(actions.filterToggle(filterKey, sessionId));
     },
   }, label);
 }
 
 module.exports = FilterButton;
--- a/devtools/client/webconsole/test/mochitest/browser_console_filters.js
+++ b/devtools/client/webconsole/test/mochitest/browser_console_filters.js
@@ -4,18 +4,43 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Check that the Browser Console does not use the same filter prefs as the Web
 // Console.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf8,<p>browser console filters";
+const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+
+// We only go though the filterClicked() code path once in this test but that is
+// enough to test the telemetry event.
+const DATA = [
+  {
+    timestamp: null,
+    category: "devtools.main",
+    method: "filter_changed",
+    object: "webconsole",
+    value: null,
+    extra: {
+      clicked: "error",
+      active: "warn,log,info,debug",
+      inactive: "error,css,net,netxhr"
+    }
+  }
+];
 
 add_task(async function() {
+  // Let's reset the counts.
+  Services.telemetry.clearEvents();
+
+  // Ensure no events have been logged
+  const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
+  ok(!snapshot.parent, "No events have been logged for the main process");
+
   let hud = await openNewTabAndConsole(TEST_URI);
   ok(hud, "web console opened");
 
   let filterState = await getFilterState(hud);
   ok(filterState.error, "The web console error filter is enabled");
 
   info(`toggle "error" filter`);
   await setFilterState(hud, {
@@ -41,9 +66,36 @@ add_task(async function() {
     error: false
   });
 
   filterState = await getFilterState(hud);
   ok(!filterState.error, "The browser console error filter is disabled");
 
   await resetFilters(hud);
   await setFilterBarVisible(hud, false);
+
+  checkResults();
 });
+
+function checkResults() {
+  const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
+  const events = snapshot.parent.filter(event => event[1] === "devtools.main" &&
+                                                 event[2] === "filter_changed" &&
+                                                 event[3] === "webconsole" &&
+                                                 event[4] === null
+  );
+
+  for (const i in DATA) {
+    const [ timestamp, category, method, object, value, extra ] = events[i];
+    const expected = DATA[i];
+
+    // ignore timestamp
+    ok(timestamp > 0, "timestamp is greater than 0");
+    is(category, expected.category, "category is correct");
+    is(method, expected.method, "method is correct");
+    is(object, expected.object, "object is correct");
+    is(value, expected.value, "value is correct");
+
+    is(extra.clicked, expected.extra.clicked, "clicked is correct");
+    is(extra.inactive, expected.extra.inactive, "inactive is correct");
+    is(extra.active, expected.extra.active, "active is correct");
+  }
+}
--- a/devtools/docs/frontend/telemetry.md
+++ b/devtools/docs/frontend/telemetry.md
@@ -103,17 +103,17 @@ devtools.copy.unique.css.selector:
       - dev-developer-tools@lists.mozilla.org
     release_channel_collection: opt-out
     record_in_processes:
       - 'main'
 ```
 
 ### Adding probes to `Events.yaml`
 
-Our entries are prefixed with `devtools.`. For example:
+Our entries are prefixed with `devtools.` and must contain a session_id key. For example:
 
 ```yaml
 devtools.main:
   open:
     objects: ["tools"]
     bug_numbers: [1416024]
     notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
     record_in_processes: ["main"]
@@ -121,16 +121,17 @@ devtools.main:
     release_channel_collection: opt-out
     expiry_version: never
     extra_keys:
       entrypoint: How was the toolbox opened? CommandLine, ContextMenu, DeveloperToolbar, HamburgerMenu, KeyShortcut, SessionRestore or SystemMenu
       first_panel: The name of the first panel opened.
       host: "Toolbox host (positioning): bottom, side, window or other."
       splitconsole: Indicates whether the split console was open.
       width: Toolbox width (px).
+      session_id: The toolbox session start time e.g. 13963. This must be read using toolbox.sessionId.
 ```
 
 ### 2. Using Histograms.json probes in DevTools code
 
 Once the probe has been declared in the `Histograms.json` file, you'll need to actually use it in our code.
 
 First, you need to give it an id in `devtools/client/shared/telemetry.js`. Similarly to the `Histograms.json` case, you'll want to follow the style of existing entries. For example:
 
@@ -393,17 +394,17 @@ async function openAndCloseToolbox(toolI
 function checkResults() {
   const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
   const events = snapshot.parent.filter(event => event[1] === "devtools.main" &&
                                                  event[2] === "close" &&
                                                  event[3] === "tools" &&
                                                  event[4] === null
   );
 
-  for (let i in DATA) {
+  for (const i in DATA) {
     const [ timestamp, category, method, object, value, extra ] = events[i];
     const expected = DATA[i];
 
     // ignore timestamp
     ok(timestamp > 0, "timestamp is greater than 0");
     is(category, expected.category, "category is correct");
     is(method, expected.method, "method is correct");
     is(object, expected.object, "object is correct");
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -603,8 +603,21 @@ devtools.main:
     bug_numbers: [1463092]
     notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
     record_in_processes: ["main"]
     description: User has clicked a link to a source file in the web console.
     release_channel_collection: opt-out
     expiry_version: never
     extra_keys:
       session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+  filter_changed:
+    objects: ["webconsole"]
+    bug_numbers: [1463095]
+    notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
+    record_in_processes: ["main"]
+    description: User has changed a filter in the web console.
+    release_channel_collection: opt-out
+    expiry_version: never
+    extra_keys:
+      clicked: "The clicked filter: error, warn, log, info, debug, css, netxhr or net"
+      active: Comma separated list of active filters.
+      inactive: Comma separated list of inactive filters.
+      session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.