Bug 1347517 - Add telemetry for GCLI commands used r?jwalker draft
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Wed, 15 Mar 2017 18:33:52 +0100
changeset 555534 4a029442f41e8bff9003f66bc759cced0fdd44dc
parent 555511 891981e67948aaebf7a63bba5181ef0a538ce163
child 622620 d24c44a7e2c4713343db1bbaee85dcb3b8dea244
push id52252
push userbmo:mratcliffe@mozilla.com
push dateTue, 04 Apr 2017 12:04:42 +0000
reviewersjwalker
bugs1347517
milestone55.0a1
Bug 1347517 - Add telemetry for GCLI commands used r?jwalker MozReview-Commit-ID: FBBsZeZkPEt
devtools/client/commandline/test/browser.ini
devtools/client/commandline/test/browser_gcli_telemetry.js
devtools/client/commandline/test/head.js
devtools/client/shared/telemetry.js
devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
toolkit/components/telemetry/Histograms.json
--- a/devtools/client/commandline/test/browser.ini
+++ b/devtools/client/commandline/test/browser.ini
@@ -113,13 +113,14 @@ skip-if = true # Bug 1093205 - Test does
 [browser_gcli_keyboard4.js]
 [browser_gcli_keyboard5.js]
 [browser_gcli_menu.js]
 [browser_gcli_node.js]
 [browser_gcli_resource.js]
 [browser_gcli_short.js]
 [browser_gcli_spell.js]
 [browser_gcli_split.js]
+[browser_gcli_telemetry.js]
 [browser_gcli_tokenize.js]
 [browser_gcli_tooltip.js]
 skip-if = true # Bug 1093205 - Test does not run in Firefox due to missing terminal
 [browser_gcli_types.js]
 [browser_gcli_union.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/commandline/test/browser_gcli_telemetry.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global helpers, btoa, whenDelayedStartupFinished, OpenBrowserWindow */
+
+// Test that GCLI telemetry works properly
+
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_gcli_telemetry.js";
+const COMMAND_HISTOGRAM_ID = "DEVTOOLS_GCLI_COMMANDS_KEYED";
+
+function test() {
+  return Task.spawn(spawnTest).then(finish, helpers.handleError);
+}
+
+function* spawnTest() {
+  let options = yield helpers.openTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
+
+  yield helpers.openToolbar(options);
+
+  yield helpers.audit(options, [
+    {
+      setup: "addon list<RETURN>"
+    },
+    {
+      setup: "appcache clear<RETURN>"
+    },
+    {
+      setup: "clear<RETURN>"
+    },
+    {
+      setup: "console clear<RETURN>"
+    },
+    {
+      setup: "cookie list<RETURN>"
+    },
+    {
+      setup: "help<RETURN>"
+    },
+    {
+      setup: "help addon<RETURN>"
+    },
+    {
+      setup: "screenshot<RETURN>"
+    },
+    {
+      setup: "listen 6000<RETURN>"
+    },
+    {
+      setup: "unlisten<RETURN>"
+    },
+    {
+      setup: "context addon<RETURN>"
+    },
+  ]);
+
+  let results = Telemetry.prototype.telemetryInfo;
+
+  checkTelemetryResults(results);
+  stopRecordingTelemetryLogs(Telemetry);
+
+  info("Closing Developer Toolbar");
+  yield helpers.closeToolbar(options);
+
+  info("Closing tab");
+  yield helpers.closeTab(options);
+}
+
+/**
+ * Load the Telemetry utils, then stub Telemetry.prototype.log and
+ * Telemetry.prototype.logKeyed in order to record everything that's logged in
+ * it.
+ * Store all recordings in Telemetry.telemetryInfo.
+ * @return {Telemetry}
+ */
+function loadTelemetryAndRecordLogs() {
+  info("Mock the Telemetry log function to record logged information");
+
+  let Telemetry = require("devtools/client/shared/telemetry");
+
+  Telemetry.prototype.telemetryInfo = {};
+  Telemetry.prototype._oldlog = Telemetry.prototype.log;
+  Telemetry.prototype.log = function (histogramId, value) {
+    if (!this.telemetryInfo) {
+      // Telemetry instance still in use after stopRecordingTelemetryLogs
+      return;
+    }
+    if (histogramId) {
+      if (!this.telemetryInfo[histogramId]) {
+        this.telemetryInfo[histogramId] = [];
+      }
+      this.telemetryInfo[histogramId].push(value);
+    }
+  };
+  Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
+  Telemetry.prototype.logKeyed = function (histogramId, key, value) {
+    this.log(`${histogramId}|${key}`, value);
+  };
+
+  return Telemetry;
+}
+
+/**
+ * Stop recording the Telemetry logs and put back the utils as it was before.
+ * @param {Telemetry} Required Telemetry
+ *        Telemetry object that needs to be stopped.
+ */
+function stopRecordingTelemetryLogs(Telemetry) {
+  info("Stopping Telemetry");
+  Telemetry.prototype.log = Telemetry.prototype._oldlog;
+  Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
+  delete Telemetry.prototype._oldlog;
+  delete Telemetry.prototype._oldlogKeyed;
+  delete Telemetry.prototype.telemetryInfo;
+}
+
+function checkTelemetryResults(results) {
+  let prefix = COMMAND_HISTOGRAM_ID + "|";
+  let keys = Object.keys(results).filter(result => {
+    return result.startsWith(prefix);
+  });
+
+  let commands = [
+    "addon list",
+    "appcache clear",
+    "clear",
+    "console clear",
+    "cookie list",
+    "screenshot",
+    "listen",
+    "unlisten",
+    "context",
+    "help"
+  ];
+
+  for (let command of commands) {
+    let key = prefix + command;
+
+    switch (key) {
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|addon list":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|appcache clear":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|clear":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|console clear":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|cookie list":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|screenshot":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|listen":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|unlisten":
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|context":
+        is(results[key].length, 1, `${key} is correct`);
+        break;
+      case "DEVTOOLS_GCLI_COMMANDS_KEYED|help":
+        is(results[key].length, 2, `${key} is correct`);
+        break;
+      default:
+        ok(false, `No telemetry pings were sent for command "${command}"`);
+    }
+  }
+}
--- a/devtools/client/commandline/test/head.js
+++ b/devtools/client/commandline/test/head.js
@@ -1,11 +1,16 @@
 /* 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/. */
+ /* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+ /* import-globals-from helpers.js */
+ /* import-globals-from mockCommands.js */
+
+"use strict";
 
 const TEST_BASE_HTTP = "http://example.com/browser/devtools/client/commandline/test/";
 const TEST_BASE_HTTPS = "https://example.com/browser/devtools/client/commandline/test/";
 
 var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var { console } = require("resource://gre/modules/Console.jsm");
 var flags = require("devtools/shared/flags");
 
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -273,24 +273,29 @@ Telemetry.prototype = {
 
   /**
    * Log a value to a keyed histogram.
    *
    * @param  {String} histogramId
    *         Histogram in which the data is to be stored.
    * @param  {String} key
    *         The key within the single histogram.
-   * @param  value
-   *         Value to store.
+   * @param  [value]
+   *         Optional value to store.
    */
   logKeyed: function (histogramId, key, value) {
     if (histogramId) {
       try {
         let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
-        histogram.add(key, value);
+
+        if (typeof value === "undefined") {
+          histogram.add(key);
+        } else {
+          histogram.add(key, value);
+        }
       } catch (e) {
         dump("Warning: An attempt was made to write to the " + histogramId +
              " histogram, which is not defined in Histograms.json\n");
       }
     }
   },
 
   /**
@@ -317,9 +322,8 @@ Telemetry.prototype = {
   },
 
   destroy: function () {
     for (let histogramId of this._timers.keys()) {
       this.stopTimer(histogramId);
     }
   }
 };
-
--- a/devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
+++ b/devtools/shared/gcli/source/lib/gcli/mozui/inputter.js
@@ -17,16 +17,18 @@
 'use strict';
 
 var util = require('../util/util');
 var KeyEvent = require('../util/util').KeyEvent;
 
 var Status = require('../types/types').Status;
 var History = require('../ui/history').History;
 
+var Telemetry = require("devtools/client/shared/telemetry");
+
 var RESOLVED = Promise.resolve(true);
 
 /**
  * A wrapper to take care of the functions concerning an input element
  * @param components Object that links to other UI components. GCLI provided:
  * - requisition
  * - focusManager
  * - element
@@ -42,16 +44,19 @@ function Inputter(components) {
   this.document = this.element.ownerDocument;
 
   // Used to distinguish focus from TAB in CLI. See onKeyUp()
   this.lastTabDownAt = 0;
 
   // Used to effect caret changes. See _processCaretChange()
   this._caretChange = null;
 
+  // Use telemetry
+  this._telemetry = new Telemetry();
+
   // Ensure that TAB/UP/DOWN isn't handled by the browser
   this.onKeyDown = this.onKeyDown.bind(this);
   this.onKeyUp = this.onKeyUp.bind(this);
   this.element.addEventListener('keydown', this.onKeyDown);
   this.element.addEventListener('keyup', this.onKeyUp);
 
   // Setup History
   this.history = new History();
@@ -117,16 +122,17 @@ Inputter.prototype.destroy = function() 
   this.outputted = undefined;
   this.onMouseUp = undefined;
   this.onKeyDown = undefined;
   this.onKeyUp = undefined;
   this.onWindowResize = undefined;
   this.tooltip = undefined;
   this.document = undefined;
   this.element = undefined;
+  this._telemetry = undefined;
 };
 
 /**
  * Make ourselves visually similar to the input element, and make the input
  * element transparent so our background shines through
  */
 Inputter.prototype.onWindowResize = function() {
   // Mochitest sometimes causes resize after shutdown. See Bug 743190
@@ -552,16 +558,19 @@ Inputter.prototype._handleDownArrow = fu
  * RETURN checks status and might exec
  */
 Inputter.prototype._handleReturn = function() {
   // Deny RETURN unless the command might work
   if (this.requisition.status === Status.VALID) {
     this._scrollingThroughHistory = false;
     this.history.add(this.element.value);
 
+    let name = this.requisition.commandAssignment.value.name;
+    this._telemetry.logKeyed("DEVTOOLS_GCLI_COMMANDS_KEYED", name);
+
     return this.requisition.exec().then(function() {
       this.textChanged();
     }.bind(this));
   }
 
   // If we can't execute the command, but there is a menu choice to use
   // then use it.
   if (!this.tooltip.selectChoice()) {
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8640,16 +8640,24 @@
     "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
     "expires_in_version": "58",
     "kind": "enumerated",
     "bug_numbers": [1205845],
     "n_values": 9,
     "releaseChannelCollection": "opt-out",
     "description": "Records DevTools toolbox host each time the toolbox is opened and when the host is changed (0:Bottom, 1:Side, 2:Window, 3:Custom, 9:Unknown)."
   },
+  "DEVTOOLS_GCLI_COMMANDS_KEYED": {
+    "bug_numbers": [1347517],
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+    "expires_in_version": "never",
+    "keyed": true,
+    "kind": "count",
+    "description": "Reports the command name used in GCLI e.g. 'screenshot'"
+  },
   "VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN": {
     "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
     "expires_in_version": "53",
     "kind": "boolean",
     "description": "How many times has view source in browser / tab been opened?"
   },
   "VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN": {
     "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],