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