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