Bug 1463144 - Instrument inspection of "Filter changes" in the Network Panel with event telemetry r=miker
authorJan Odvarko <odvarko@gmail.com>
Tue, 14 Aug 2018 12:20:07 +0000
changeset 828997 0af5942407da25f69e19a5533e921c5f6722fbf8
parent 828996 419fd4edef4fb7057f7a168ee03242df759f6fac
child 828998 07bf059b54930e579c660cfb7434cb37f3dd3582
push id118741
push userbmo:kshvmdn@gmail.com
push dateTue, 14 Aug 2018 18:31:47 +0000
reviewersmiker
bugs1463144
milestone63.0a1
Bug 1463144 - Instrument inspection of "Filter changes" in the Network Panel with event telemetry r=miker Differential Revision: https://phabricator.services.mozilla.com/D3205
devtools/client/netmonitor/src/api.js
devtools/client/netmonitor/src/create-store.js
devtools/client/netmonitor/src/middleware/event-telemetry.js
devtools/client/netmonitor/src/middleware/moz.build
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_telemetry_filters_changed.js
toolkit/components/telemetry/Events.yaml
--- a/devtools/client/netmonitor/src/api.js
+++ b/devtools/client/netmonitor/src/api.js
@@ -7,16 +7,19 @@
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
 const { Connector } = require("./connector/index");
 const { configureStore } = require("./create-store");
 const { EVENTS } = require("./constants");
 const Actions = require("./actions/index");
 
+// Telemetry
+const Telemetry = require("devtools/client/shared/telemetry");
+
 const {
   getDisplayedRequestById,
   getSortedRequests
 } = require("./selectors/index");
 
 /**
  * API object for NetMonitor panel (like a facade). This object can be
  * consumed by other panels, WebExtension API, etc.
@@ -25,18 +28,21 @@ const {
  * and used even if the Network panel UI doesn't exist.
  */
 function NetMonitorAPI() {
   EventEmitter.decorate(this);
 
   // Connector to the backend.
   this.connector = new Connector();
 
+  // Telemetry
+  this.telemetry = new Telemetry();
+
   // Configure store/state object.
-  this.store = configureStore(this.connector);
+  this.store = configureStore(this.connector, this.telemetry);
 
   // List of listeners for `devtools.network.onRequestFinished` WebExt API
   this._requestFinishedListeners = new Set();
 
   // Bind event handlers
   this.onRequestAdded = this.onRequestAdded.bind(this);
   this.actions = bindActionCreators(Actions, this.store.dispatch);
 }
--- a/devtools/client/netmonitor/src/create-store.js
+++ b/devtools/client/netmonitor/src/create-store.js
@@ -8,29 +8,30 @@ const Services = require("Services");
 const { applyMiddleware, createStore } = require("devtools/client/shared/vendor/redux");
 
 // Middleware
 const batching = require("./middleware/batching");
 const prefs = require("./middleware/prefs");
 const thunk = require("./middleware/thunk");
 const recording = require("./middleware/recording");
 const throttling = require("./middleware/throttling");
+const eventTelemetry = require("./middleware/event-telemetry");
 
 // Reducers
 const rootReducer = require("./reducers/index");
 const { FilterTypes, Filters } = require("./reducers/filters");
 const { Requests } = require("./reducers/requests");
 const { Sort } = require("./reducers/sort");
 const { TimingMarkers } = require("./reducers/timing-markers");
 const { UI, Columns } = require("./reducers/ui");
 
 /**
  * Configure state and middleware for the Network monitor tool.
  */
-function configureStore(connector) {
+function configureStore(connector, telemetry) {
   // Prepare initial state.
   const initialState = {
     filters: new Filters({
       requestFilterTypes: getFilterState()
     }),
     requests: new Requests(),
     sort: new Sort(),
     timingMarkers: new TimingMarkers(),
@@ -41,16 +42,17 @@ function configureStore(connector) {
 
   // Prepare middleware.
   const middleware = applyMiddleware(
     thunk,
     prefs,
     batching,
     recording(connector),
     throttling(connector),
+    eventTelemetry(connector, telemetry),
   );
 
   return createStore(rootReducer, initialState, middleware);
 }
 
 // Helpers
 
 /**
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/middleware/event-telemetry.js
@@ -0,0 +1,86 @@
+/* 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 { gDevTools } = require("devtools/client/framework/devtools");
+
+const {
+  TOGGLE_REQUEST_FILTER_TYPE,
+  ENABLE_REQUEST_FILTER_TYPE_ONLY,
+  SET_REQUEST_FILTER_TEXT,
+} = require("../constants");
+
+/**
+ * Event telemetry middleware is responsible for logging
+ * specific filter events to telemetry. This telemetry
+ * helps to track Net panel filtering usage.
+ */
+function eventTelemetryMiddleware(connector, telemetry) {
+  return store => next => action => {
+    const oldState = store.getState();
+    const res = next(action);
+    const toolbox = gDevTools.getToolbox(connector.getTabTarget());
+    if (!toolbox) {
+      return res;
+    }
+
+    const state = store.getState();
+
+    const filterChangeActions = [
+      TOGGLE_REQUEST_FILTER_TYPE,
+      ENABLE_REQUEST_FILTER_TYPE_ONLY,
+      SET_REQUEST_FILTER_TEXT,
+    ];
+
+    if (filterChangeActions.includes(action.type)) {
+      filterChange({
+        action,
+        state,
+        oldState,
+        telemetry,
+        sessionId: toolbox.sessionId,
+      });
+    }
+
+    return res;
+  };
+}
+
+function filterChange({action, state, oldState, telemetry, sessionId}) {
+  const oldFilterState = oldState.filters;
+  const filterState = state.filters;
+  const activeFilters = [];
+  const inactiveFilters = [];
+
+  for (const [key, value] of Object.entries(filterState.requestFilterTypes)) {
+    if (value) {
+      activeFilters.push(key);
+    } else {
+      inactiveFilters.push(key);
+    }
+  }
+
+  let trigger;
+  if (action.type === TOGGLE_REQUEST_FILTER_TYPE ||
+      action.type === ENABLE_REQUEST_FILTER_TYPE_ONLY) {
+    trigger = action.filter;
+  } else if (action.type === SET_REQUEST_FILTER_TEXT) {
+    if (oldFilterState.requestFilterText !== "" &&
+        filterState.requestFilterText !== "") {
+      return;
+    }
+
+    trigger = "text";
+  }
+
+  telemetry.recordEvent("devtools.main", "filters_changed", "netmonitor", null, {
+    "trigger": trigger,
+    "active": activeFilters.join(","),
+    "inactive": inactiveFilters.join(","),
+    "session_id": sessionId
+  });
+}
+
+module.exports = eventTelemetryMiddleware;
--- a/devtools/client/netmonitor/src/middleware/moz.build
+++ b/devtools/client/netmonitor/src/middleware/moz.build
@@ -1,11 +1,12 @@
 # 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/.
 
 DevToolsModules(
     'batching.js',
+    'event-telemetry.js',
     'prefs.js',
     'recording.js',
     'throttling.js',
     'thunk.js',
 )
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -180,15 +180,16 @@ skip-if = true # Bug 1258809
 [browser_net_sort-01.js]
 [browser_net_sort-02.js]
 [browser_net_statistics-01.js]
 skip-if = true # Bug 1373558
 [browser_net_statistics-02.js]
 [browser_net_status-bar.js]
 [browser_net_status-codes.js]
 [browser_net_streaming-response.js]
+[browser_net_telemetry_filters_changed.js]
 [browser_net_throttle.js]
 [browser_net_timeline_ticks.js]
 skip-if = true # TODO: fix the test
 [browser_net_timing-division.js]
 [browser_net_truncate.js]
 [browser_net_view-source-debugger.js]
 [browser_net_waterfall-click.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_telemetry_filters_changed.js
@@ -0,0 +1,109 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+
+/**
+ * Test the filters_changed telemetry event.
+ */
+add_task(async function() {
+  const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
+  info("Starting test... ");
+
+  const { document, store, windowRequire } = monitor.panelWin;
+  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  const {
+    getDisplayedRequests,
+  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+
+  store.dispatch(Actions.batchEnable(false));
+
+  // Remove all telemetry events (you can check about:telemetry).
+  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");
+
+  // Reload to have one request in the list.
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.loadURI(SIMPLE_URL);
+  await wait;
+
+  info("Click on the 'HTML' filter");
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector(".requests-list-filter-html-button"));
+
+  checkTelemetryEvent({
+    trigger: "html",
+    active: "html",
+    inactive: "all,css,js,xhr,fonts,images,media,ws,other",
+  });
+
+  info("Click on the 'CSS' filter");
+  EventUtils.sendMouseEvent({ type: "click" },
+    document.querySelector(".requests-list-filter-css-button"));
+
+  checkTelemetryEvent({
+    trigger: "css",
+    active: "html,css",
+    inactive: "all,js,xhr,fonts,images,media,ws,other",
+  });
+
+  info("Filter the output using the text filter input");
+  setFreetextFilter(monitor, "nomatch");
+
+  // Wait till the text filter is applied.
+  await waitUntil(() => getDisplayedRequests(store.getState()).length == 0);
+
+  checkTelemetryEvent({
+    trigger: "text",
+    active: "html,css",
+    inactive: "all,js,xhr,fonts,images,media,ws,other",
+  });
+
+  return teardown(monitor);
+});
+
+function setFreetextFilter(monitor, value) {
+  const { document } = monitor.panelWin;
+
+  const filterBox = document.querySelector(".devtools-filterinput");
+  filterBox.focus();
+  filterBox.value = "";
+
+  for (const ch of value) {
+    EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
+  }
+}
+
+function checkTelemetryEvent(expectedEvent) {
+  const events = getFiltersChangedEventsExtra();
+  is(events.length, 1, "There was only 1 event logged");
+  const [event] = events;
+  ok(event.session_id > 0, "There is a valid session_id in the logged event");
+  const f = e => JSON.stringify(e, null, 2);
+  is(f(event), f({
+    ...expectedEvent,
+    "session_id": event.session_id
+  }), "The event has the expected data");
+}
+
+function getFiltersChangedEventsExtra() {
+  // Retrieve and clear telemetry events.
+  const snapshot = Services.telemetry.snapshotEvents(OPTOUT, true);
+
+  const filtersChangedEvents = snapshot.parent.filter(event =>
+    event[1] === "devtools.main" &&
+    event[2] === "filters_changed" &&
+    event[3] === "netmonitor"
+  );
+
+  // Since we already know we have the correct event, we only return the `extra` field
+  // that was passed to it (which is event[5] here).
+  return filtersChangedEvents.map(event => event[5]);
+}
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -628,25 +628,25 @@ devtools.main:
     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.
   filters_changed:
-    objects: ["webconsole"]
-    bug_numbers: [1463095]
+    objects: ["netmonitor", "webconsole"]
+    bug_numbers: [1463144, 1463095]
     notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
     record_in_processes: ["main"]
     description: User has changed filters in the web console.
     release_channel_collection: opt-out
     expiry_version: never
     extra_keys:
-      trigger: "The cause of the filter change: error, warn, log, info, debug, css, netxhr, net, text or reset"
+      trigger: "The cause of the filter change: error, warn, log, info, debug, css, netxhr, net, text or reset and all, html, css, js, xhr, fonts, images, media, ws or other for netmonitor"
       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.
   jump_to_definition:
     objects: ["webconsole"]
     bug_numbers: [1463101]
     notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
     record_in_processes: ["main"]