diff --git a/devtools/client/responsive.html/index.js b/devtools/client/responsive.html/index.js
index c6eb95b86357..f45b135921c1 100644
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -40,7 +40,7 @@ const bootstrap = {
// toolbox session id.
this.telemetry.toolOpened("responsive", -1, this);
- const store = this.store = Store();
+ const store = this.store = Store({ telemetry: this.telemetry });
const provider = createElement(Provider, { store }, App());
ReactDOM.render(provider, document.querySelector("#root"));
message.post(window, "init:done");
diff --git a/devtools/client/responsive.html/middleware/moz.build b/devtools/client/responsive.html/middleware/moz.build
new file mode 100644
index 000000000000..1f275d5552e1
--- /dev/null
+++ b/devtools/client/responsive.html/middleware/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ 'telemetry.js',
+)
diff --git a/devtools/client/responsive.html/middleware/telemetry.js b/devtools/client/responsive.html/middleware/telemetry.js
new file mode 100644
index 000000000000..c92dba9dfaf5
--- /dev/null
+++ b/devtools/client/responsive.html/middleware/telemetry.js
@@ -0,0 +1,69 @@
+/* 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 { debounce } = require("devtools/shared/debounce");
+const {
+ RESIZE_VIEWPORT,
+ ROTATE_VIEWPORT,
+} = require("../actions/index");
+const TELEMETRY_SCALAR_VIEWPORT_CHANGE_COUNT =
+ "devtools.responsive.viewport_change_count";
+
+/**
+ * Redux middleware to observe actions dispatched to the Redux store and log to Telemetry.
+ *
+ * `telemetryMiddleware()` is a wrapper function which receives the Telemetry
+ * instance as its only argument and is used to create a closure to make that instance
+ * available to the returned Redux middleware and functions nested within.
+ *
+ * To be used with `applyMiddleware()` helper when creating the Redux store.
+ * @see https://redux.js.org/api/applymiddleware
+ *
+ * The wrapper returns the function that acts as the Redux middleware. This function
+ * receives a single `store` argument (the Redux Middleware API, an object containing
+ * `getState()` and `dispatch()`) and returns a function which receives a single `next`
+ * argument (the next middleware in the chain) which itself returns a function that
+ * receives a single `action` argument (the dispatched action).
+ *
+ * @param {Object} telemetry
+ * Instance of the Telemetry API
+ * @return {Function}
+ */
+function telemetryMiddleware(telemetry) {
+ function logViewportChange() {
+ telemetry.scalarAdd(TELEMETRY_SCALAR_VIEWPORT_CHANGE_COUNT, 1);
+ }
+
+ // Debounced logging to use in response to high frequency actions like RESIZE_VIEWPORT.
+ // Set debounce()'s `immediate` parameter to `true` to ensure the very first
+ // call is executed immediately. This helps the tests to check that logging is working
+ // without adding needless complexity to wait for the debounced call.
+ const logViewportChangeDebounced = debounce(logViewportChange, 300, null, true);
+
+ // This cascade of functions is the Redux middleware signature.
+ // @see https://redux.js.org/api/applymiddleware#arguments
+ return store => next => action => {
+ const res = next(action);
+ // Pass through to the next middleware if a telemetry instance is not available, for
+ // example when running unit tests.
+ if (!telemetry) {
+ return res;
+ }
+
+ switch (action.type) {
+ case ROTATE_VIEWPORT:
+ logViewportChange();
+ break;
+ case RESIZE_VIEWPORT:
+ logViewportChangeDebounced();
+ break;
+ }
+
+ return res;
+ };
+}
+
+module.exports = telemetryMiddleware;
diff --git a/devtools/client/responsive.html/moz.build b/devtools/client/responsive.html/moz.build
index 076277dd96f2..dd150a3b8349 100644
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -9,6 +9,7 @@ DIRS += [
'browser',
'components',
'images',
+ 'middleware',
'reducers',
'utils',
]
diff --git a/devtools/client/responsive.html/store.js b/devtools/client/responsive.html/store.js
index 564b1ebe65c7..d9a6037756a6 100644
--- a/devtools/client/responsive.html/store.js
+++ b/devtools/client/responsive.html/store.js
@@ -8,10 +8,12 @@ const { combineReducers } = require("devtools/client/shared/vendor/redux");
const createStore = require("devtools/client/shared/redux/create-store");
const reducers = require("./reducers");
const flags = require("devtools/shared/flags");
+const telemetryMiddleware = require("./middleware/telemetry");
-module.exports = function() {
+module.exports = function(options = {}) {
let shouldLog = false;
let history;
+ const { telemetry } = options;
// If testing, store the action history in an array
// we'll later attach to the store
@@ -23,6 +25,7 @@ module.exports = function() {
const store = createStore({
log: shouldLog,
history,
+ middleware: [telemetryMiddleware(telemetry)],
})(combineReducers(reducers), {});
if (history) {
diff --git a/devtools/client/responsive.html/test/browser/browser.ini b/devtools/client/responsive.html/test/browser/browser.ini
index 7c3d238c2a42..d753a2302505 100644
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -64,6 +64,8 @@ skip-if = true # Bug 1413765
[browser_tab_remoteness_change.js]
[browser_target_blank.js]
[browser_telemetry_activate_rdm.js]
+[browser_telemetry_viewport_change.js]
+skip-if = (os == "mac" && debug)
[browser_toggle_zoom.js]
[browser_toolbox_computed_view.js]
[browser_toolbox_rule_view.js]
diff --git a/devtools/client/responsive.html/test/browser/browser_telemetry_viewport_change.js b/devtools/client/responsive.html/test/browser/browser_telemetry_viewport_change.js
new file mode 100644
index 000000000000..c3836a16325d
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_telemetry_viewport_change.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that actions to change the RDM viewport size and rotation are logged to telemetry.
+ */
+
+const TEST_URL = "data:text/html;charset=utf-8,browser_telemetry_viewport_change.js";
+const TELEMETRY_SCALAR_VIEWPORT_CHANGE_COUNT =
+ "devtools.responsive.viewport_change_count";
+
+addRDMTask(TEST_URL, async function({ ui, manager }) {
+ info("Clear any existing Telemetry scalars");
+ Services.telemetry.clearScalars();
+
+ info("Resize the viewport");
+ await setViewportSize(ui, manager, 100, 300);
+
+ info("Rotate the viewport");
+ rotateViewport(ui);
+
+ const scalars = Services.telemetry.getSnapshotForScalars("main", false);
+ ok(scalars.parent, "Telemetry scalars present");
+
+ const count = scalars.parent[TELEMETRY_SCALAR_VIEWPORT_CHANGE_COUNT];
+ is(count, 2, "Scalar has correct number of viewport changes logged");
+});
diff --git a/devtools/shared/debounce.js b/devtools/shared/debounce.js
index 60636b62e5ef..639949109e80 100644
--- a/devtools/shared/debounce.js
+++ b/devtools/shared/debounce.js
@@ -14,9 +14,12 @@
* The wait period
* @param {Object} scope
* The scope to use for func
+ * @param {Boolean} immediate
+ * Whether to execute the method immediately on the first call.
+ * Optional. Default `false`.
* @return {Function} The debounced function
*/
-exports.debounce = function(func, wait, scope) {
+exports.debounce = function(func, wait, scope, immediate = false) {
let timer = null;
return function() {
@@ -29,5 +32,10 @@ exports.debounce = function(func, wait, scope) {
timer = null;
func.apply(scope, args);
}, wait);
+
+ if (immediate) {
+ immediate = false;
+ func.apply(scope, args);
+ }
};
};
diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml
index 41063d28f3a3..5cba39180227 100644
--- a/toolkit/components/telemetry/Scalars.yaml
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -1570,7 +1570,7 @@ devtools.responsive:
kind: uint
notification_emails:
- dev-developer-tools@lists.mozilla.org
- - jryans@mozilla.com
+ - mbalfanz@mozilla.com
record_in_processes:
- main
release_channel_collection: opt-out
@@ -1584,7 +1584,20 @@ devtools.responsive:
keyed: true
notification_emails:
- dev-developer-tools@lists.mozilla.org
- - jryans@mozilla.com
+ - mbalfanz@mozilla.com
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ viewport_change_count:
+ bug_numbers:
+ - 1552464
+ description: >
+ Number of times the viewport attributes changed while in Responsive Design Mode.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - mbalfanz@mozilla.com
record_in_processes:
- main
release_channel_collection: opt-out