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
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