Bug 1463095 - Instrument inspection of filter changes in the Web Console with event telemetry; r=jdescottes. draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 24 Jul 2018 09:55:36 +0200
changeset 822876 7a7d6c7ff3fdbe1da6a16bc47017dbf5d90582db
parent 822828 7ba07ef0e4532b644b812942aa38af4510dbc74f
push id117502
push userbmo:nchevobbe@mozilla.com
push dateThu, 26 Jul 2018 07:23:19 +0000
reviewersjdescottes
bugs1463095
milestone63.0a1
Bug 1463095 - Instrument inspection of filter changes in the Web Console with event telemetry; r=jdescottes. This introduces an event-telemetry middleware that we'll be able to re-use for other events. A test is added to make sure we do log those events as intended. The telemetry mock for mocha test is modified to include recordEvent so the test still run. MozReview-Commit-ID: 1SHnVIRGdDz
devtools/client/webconsole/middleware/event-telemetry.js
devtools/client/webconsole/middleware/moz.build
devtools/client/webconsole/store.js
devtools/client/webconsole/test/helpers.js
devtools/client/webconsole/test/mocha-test-setup.js
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_webconsole_telemetry_filters_changed.js
devtools/client/webconsole/webconsole-output-wrapper.js
toolkit/components/telemetry/Events.yaml
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/middleware/event-telemetry.js
@@ -0,0 +1,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 {
+  FILTER_TEXT_SET,
+  FILTER_TOGGLE,
+  DEFAULT_FILTERS_RESET,
+} = require("devtools/client/webconsole/constants");
+
+/**
+ * Event telemetry middleware is responsible for logging specific events to telemetry.
+ */
+function eventTelemetryMiddleware(telemetry, sessionId, store) {
+  return next => action => {
+    const oldState = store.getState();
+    const res = next(action);
+    if (!sessionId) {
+      return res;
+    }
+
+    const state = store.getState();
+
+    const filterChangeActions = [
+      FILTER_TEXT_SET,
+      FILTER_TOGGLE,
+      DEFAULT_FILTERS_RESET,
+    ];
+
+    if (filterChangeActions.includes(action.type)) {
+      filterChange({
+        action,
+        state,
+        oldState,
+        telemetry,
+        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)) {
+    if (value) {
+      activeFilters.push(key);
+    } else {
+      inactiveFilters.push(key);
+    }
+  }
+
+  let trigger;
+  if (action.type === FILTER_TOGGLE) {
+    trigger = action.filter;
+  } else if (action.type === DEFAULT_FILTERS_RESET) {
+    trigger = "reset";
+  } else if (action.type === FILTER_TEXT_SET) {
+    if (oldFilterState.text !== "" && filterState.text !== "") {
+      return;
+    }
+
+    trigger = "text";
+  }
+
+  telemetry.recordEvent("devtools.main", "filters_changed", "webconsole", null, {
+    "trigger": trigger,
+    "active": activeFilters.join(","),
+    "inactive": inactiveFilters.join(","),
+    "session_id": sessionId
+  });
+}
+
+module.exports = eventTelemetryMiddleware;
--- a/devtools/client/webconsole/middleware/moz.build
+++ b/devtools/client/webconsole/middleware/moz.build
@@ -1,9 +1,10 @@
 # vim: set filetype=python:
 # 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(
+    'event-telemetry.js',
     'history-persistence.js',
     'thunk.js',
 )
--- a/devtools/client/webconsole/store.js
+++ b/devtools/client/webconsole/store.js
@@ -18,16 +18,17 @@ const {
 // Prefs
 const { PREFS } = require("devtools/client/webconsole/constants");
 const { getPrefsService } = require("devtools/client/webconsole/utils/prefs");
 
 // Reducers
 const { reducers } = require("./reducers/index");
 
 // Middleware
+const eventTelemetry = require("./middleware/event-telemetry");
 const historyPersistence = require("./middleware/history-persistence");
 const thunk = require("./middleware/thunk");
 
 // Enhancers
 const enableBatching = require("./enhancers/batching");
 const enableActorReleaser = require("./enhancers/actor-releaser");
 const ensureCSSErrorReportingEnabled = require("./enhancers/css-error-reporting");
 const enableNetProvider = require("./enhancers/net-provider");
@@ -73,16 +74,17 @@ function configureStore(hud, options = {
       persistLogs: getBoolPref(PREFS.UI.PERSIST),
     })
   };
 
   // Prepare middleware.
   const middleware = applyMiddleware(
     thunk.bind(null, {prefsService}),
     historyPersistence,
+    eventTelemetry.bind(null, options.telemetry, options.sessionId),
   );
 
   return createStore(
     createRootReducer(),
     initialState,
     compose(
       middleware,
       enableActorReleaser(hud),
--- a/devtools/client/webconsole/test/helpers.js
+++ b/devtools/client/webconsole/test/helpers.js
@@ -6,16 +6,17 @@
 const reduxActions = require("devtools/client/webconsole/actions/index");
 const { configureStore } = require("devtools/client/webconsole/store");
 const { IdGenerator } = require("devtools/client/webconsole/utils/id-generator");
 const { stubPackets } = require("devtools/client/webconsole/test/fixtures/stubs/index");
 const { getAllMessagesById } = require("devtools/client/webconsole/selectors/messages");
 const { getPrefsService } = require("devtools/client/webconsole/utils/prefs");
 const prefsService = getPrefsService({});
 const { PREFS } = require("devtools/client/webconsole/constants");
+const Telemetry = require("devtools/client/shared/telemetry");
 
 /**
  * Prepare actions for use in testing.
  */
 function setupActions() {
   // Some actions use dependency injection. This helps them avoid using state in
   // a hard-to-test way. We need to inject stubbed versions of these dependencies.
   const wrappedActions = Object.assign({}, reduxActions);
@@ -30,33 +31,37 @@ function setupActions() {
     messagesAdd: packets => reduxActions.messagesAdd(packets, idGenerator)
   };
 }
 
 /**
  * Prepare the store for use in testing.
  */
 function setupStore(input = [], {
-  storeOptions,
+  storeOptions = {},
   actions,
   hud,
 } = {}) {
   if (!hud) {
     hud = {
       proxy: {
         releaseActor: () => {},
         target: {
           activeTab: {
             ensureCSSErrorReportingEnabled: () => {}
           }
         },
       },
     };
   }
-  const store = configureStore(hud, storeOptions);
+  const store = configureStore(hud, {
+    ...storeOptions,
+    sessionId: -1,
+    telemetry: new Telemetry(),
+  });
 
   // Add the messages from the input commands to the store.
   const messagesAdd = actions
     ? actions.messagesAdd
     : reduxActions.messagesAdd;
   store.dispatch(messagesAdd(input.map(cmd => stubPackets.get(cmd))));
 
   return store;
--- a/devtools/client/webconsole/test/mocha-test-setup.js
+++ b/devtools/client/webconsole/test/mocha-test-setup.js
@@ -70,17 +70,19 @@ requireHacker.global_hook("default", (pa
       return `module.exports = require("devtools-modules/src/Services")`;
     case "devtools/shared/client/object-client":
     case "devtools/shared/client/long-string-client":
       return `() => {}`;
     case "devtools/client/netmonitor/src/components/TabboxPanel":
     case "devtools/client/webconsole/utils/context-menu":
       return "{}";
     case "devtools/client/shared/telemetry":
-      return `module.exports = function() {}`;
+      return `module.exports = function() {
+        this.recordEvent = () => {};
+      }`;
     case "devtools/shared/event-emitter":
       return `module.exports = require("devtools-modules/src/utils/event-emitter")`;
     case "devtools/client/shared/unicode-url":
       return `module.exports = require("devtools-modules/src/unicode-url")`;
   }
 
   // We need to rewrite all the modules assuming the root is mozilla-central and give them
   // an absolute path.
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -357,16 +357,17 @@ skip-if = verify
 [browser_webconsole_split_close_button.js]
 [browser_webconsole_split_escape_key.js]
 [browser_webconsole_split_focus.js]
 [browser_webconsole_split_persist.js]
 [browser_webconsole_stacktrace_location_debugger_link.js]
 [browser_webconsole_stacktrace_location_scratchpad_link.js]
 [browser_webconsole_strict_mode_errors.js]
 [browser_webconsole_string.js]
+[browser_webconsole_telemetry_filters_changed.js]
 [browser_webconsole_time_methods.js]
 [browser_webconsole_timestamps.js]
 [browser_webconsole_trackingprotection_errors.js]
 tags = trackingprotection
 [browser_webconsole_view_source.js]
 [browser_webconsole_visibility_messages.js]
 [browser_webconsole_warn_about_replaced_api.js]
 [browser_webconsole_websocket.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_telemetry_filters_changed.js
@@ -0,0 +1,99 @@
+/* -*- 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/ */
+
+// Tests the filters_changed telemetry event.
+
+"use strict";
+
+const TEST_URI = `data:text/html,<meta charset=utf8><script>
+  console.log("test message");
+</script>`;
+
+const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+
+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");
+
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  info("Click on the 'log' filter");
+  await setFilterState(hud, {
+    log: false
+  });
+
+  checkTelemetryEvent({
+    trigger: "log",
+    active: "error,warn,info,debug",
+    inactive: "text,log,css,net,netxhr",
+  });
+
+  info("Click on the 'netxhr' filter");
+  await setFilterState(hud, {
+    netxhr: true
+  });
+
+  checkTelemetryEvent({
+    trigger: "netxhr",
+    active: "error,warn,info,debug,netxhr",
+    inactive: "text,log,css,net",
+  });
+
+  info("Filter the output using the text filter input");
+  hud.ui.filterBox.focus();
+  hud.ui.filterBox.select();
+  EventUtils.sendString("no match");
+
+  checkTelemetryEvent({
+    trigger: "text",
+    active: "text,error,warn,info,debug,netxhr",
+    inactive: "log,css,net",
+  });
+
+  info("Clear the filters using the 'Reset filters' button");
+  const resetButton = await waitFor(() =>
+    hud.ui.window.document.querySelector(".reset-filters-button"));
+  const onResetButtonHidden = waitFor(() =>
+    !hud.ui.window.document.querySelector(".reset-filters-button"));
+  resetButton.click();
+  await onResetButtonHidden;
+
+  checkTelemetryEvent({
+    trigger: "reset",
+    active: "error,warn,log,info,debug,netxhr",
+    inactive: "text,css,net",
+  });
+});
+
+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] === "webconsole"
+  );
+
+  // 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/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -32,17 +32,21 @@ function WebConsoleOutputWrapper(parentN
 
   this.queuedMessageAdds = [];
   this.queuedMessageUpdates = [];
   this.queuedRequestUpdates = [];
   this.throttledDispatchPromise = null;
 
   this.telemetry = new Telemetry();
 
-  store = configureStore(this.hud);
+  store = configureStore(this.hud, {
+    // We may not have access to the toolbox (e.g. in the browser console).
+    sessionId: this.toolbox && this.toolbox.sessionId || -1,
+    telemetry: this.telemetry,
+  });
 }
 
 WebConsoleOutputWrapper.prototype = {
   init: function() {
     return new Promise((resolve) => {
       const attachRefToHud = (id, node) => {
         this.hud[id] = node;
       };
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -625,8 +625,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.
+  filters_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 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"
+      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.
\ No newline at end of file