Bug 1441070 - Instrument toolbox panel navigation with event telemetry r=yulia
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Thu, 19 Apr 2018 12:09:28 +0100
changeset 471353 3577a6b9b1a13a0dfd93073f4ef9209c08764eb4
parent 471352 be5ba8b6dea2d025e0e1f7268336e1d1f0c32399
child 471354 fcc3959e00193176843abee95893c69ec7728906
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyulia
bugs1441070, 1456073
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1441070 - Instrument toolbox panel navigation with event telemetry r=yulia This is pretty simple... finding the best locations for the probes was the struggle here. In order to land these probes before the freeze the test for these events will be created as part of bug 1456073. MozReview-Commit-ID: 7hIfbD3wQ1I
devtools/client/framework/attach-thread.js
devtools/client/framework/components/toolbox-tab.js
devtools/client/framework/components/toolbox-tabs.js
devtools/client/framework/devtools.js
devtools/client/framework/toolbox.js
devtools/client/shared/telemetry.js
devtools/docs/frontend/telemetry.md
devtools/server/actors/webconsole.js
toolkit/components/telemetry/Events.yaml
--- a/devtools/client/framework/attach-thread.js
+++ b/devtools/client/framework/attach-thread.js
@@ -22,17 +22,17 @@ function handleThreadState(toolbox, even
 
   if (event === "paused") {
     toolbox.highlightTool("jsdebugger");
 
     if (packet.why.type === "debuggerStatement" ||
        packet.why.type === "breakpoint" ||
        packet.why.type === "exception") {
       toolbox.raise();
-      toolbox.selectTool("jsdebugger");
+      toolbox.selectTool("jsdebugger", packet.why.type);
     }
   } else if (event === "resumed") {
     toolbox.unhighlightTool("jsdebugger");
   }
 }
 
 function attachThread(toolbox) {
   let deferred = defer();
--- a/devtools/client/framework/components/toolbox-tab.js
+++ b/devtools/client/framework/components/toolbox-tab.js
@@ -56,20 +56,20 @@ class ToolboxTab extends Component {
         className,
         id: `toolbox-tab-${id}`,
         "data-id": id,
         title: tooltip,
         type: "button",
         "aria-pressed": currentToolId === id ? "true" : "false",
         tabIndex: focusedButton === id ? "0" : "-1",
         onFocus: () => focusButton(id),
-        onMouseDown: () => selectTool(id),
+        onMouseDown: () => selectTool(id, "tab_switch"),
         onKeyDown: (evt) => {
           if (evt.key === "Enter" || evt.key === " ") {
-            selectTool(id);
+            selectTool(id, "tab_switch");
           }
         },
       },
       span(
         {
           className: "devtools-tab-line"
         }
       ),
--- a/devtools/client/framework/components/toolbox-tabs.js
+++ b/devtools/client/framework/components/toolbox-tabs.js
@@ -199,17 +199,17 @@ class ToolboxTabs extends Component {
         let menu = new Menu({
           id: "tools-chevron-menupopup"
         });
 
         panelDefinitions.forEach(({id, label}) => {
           if (this.state.overflowedTabIds.includes(id)) {
             menu.append(new MenuItem({
               click: () => {
-                selectTool(id);
+                selectTool(id, "tab_switch");
               },
               id: "tools-chevron-menupopup-" + id,
               label,
               type: "checkbox",
             }));
           }
         });
 
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -454,17 +454,17 @@ DevTools.prototype = {
   async showToolbox(target, toolId, hostType, hostOptions, startTime) {
     let toolbox = this._toolboxes.get(target);
     if (toolbox) {
       if (hostType != null && toolbox.hostType != hostType) {
         await toolbox.switchHost(hostType);
       }
 
       if (toolId != null && toolbox.currentToolId != toolId) {
-        await toolbox.selectTool(toolId);
+        await toolbox.selectTool(toolId, "toolbox_show");
       }
 
       toolbox.raise();
     } else {
       // As toolbox object creation is async, we have to be careful about races
       // Check for possible already in process of loading toolboxes before
       // actually trying to create a new one.
       let promise = this._creatingToolboxes.get(target);
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -155,16 +155,17 @@ function Toolbox(target, selectedTool, h
   this._onPickerClick = this._onPickerClick.bind(this);
   this._onPickerKeypress = this._onPickerKeypress.bind(this);
   this._onPickerStarted = this._onPickerStarted.bind(this);
   this._onPickerStopped = this._onPickerStopped.bind(this);
   this._onInspectObject = this._onInspectObject.bind(this);
   this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this);
   this.updatePickerButton = this.updatePickerButton.bind(this);
   this.selectTool = this.selectTool.bind(this);
+  this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this);
   this.toggleSplitConsole = this.toggleSplitConsole.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
   }
   this._defaultToolId = selectedTool;
@@ -531,17 +532,17 @@ Toolbox.prototype = {
       // react modules and freeze the event loop for a significant time.
       // requestIdleCallback allows releasing it to allow user events to be processed.
       // Use 16ms maximum delay to allow one frame to be rendered at 60FPS
       // (1000ms/60FPS=16ms)
       this.win.requestIdleCallback(() => {
         this.component.setCanRender();
       }, {timeout: 16});
 
-      await this.selectTool(this._defaultToolId);
+      await this.selectTool(this._defaultToolId, "initial_panel");
 
       // Wait until the original tool is selected so that the split
       // console input will receive focus.
       let splitConsolePromise = promise.resolve();
       if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
         splitConsolePromise = this.openSplitConsole();
         this._telemetry.addEventProperty(
           "devtools.main", "open", "tools", null, "splitconsole", true);
@@ -839,19 +840,19 @@ Toolbox.prototype = {
   },
 
   _buildOptions: function() {
     let selectOptions = event => {
       // Flip back to the last used panel if we are already
       // on the options panel.
       if (this.currentToolId === "options" &&
           gDevTools.getToolDefinition(this.lastUsedToolId)) {
-        this.selectTool(this.lastUsedToolId);
+        this.selectTool(this.lastUsedToolId, "toggle_settings_off");
       } else {
-        this.selectTool("options");
+        this.selectTool("options", "toggle_settings_on");
       }
     };
     this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
   },
 
   _splitConsoleOnKeypress: function(e) {
     if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
       this.toggleSplitConsole();
@@ -1011,17 +1012,17 @@ Toolbox.prototype = {
       } else {
         key.setAttribute("key", shortcut);
       }
 
       key.setAttribute("modifiers", modifiers);
       // needed. See bug 371900
       key.setAttribute("oncommand", "void(0);");
       key.addEventListener("command", () => {
-        this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
+        this.selectTool(toolId, "key_shortcut").then(() => this.fireCustomKey(toolId));
       }, true);
       doc.getElementById("toolbox-keyset").appendChild(key);
     }
 
     // Add key for toggling the browser console from the detached window
     if (!doc.getElementById("key_browserconsole")) {
       let key = doc.createElement("key");
       key.id = "key_browserconsole";
@@ -1813,18 +1814,20 @@ Toolbox.prototype = {
     }
   },
 
   /**
    * Switch to the tool with the given id
    *
    * @param {string} id
    *        The id of the tool to switch to
+   * @param {string} reason
+   *        Reason the tool was opened
    */
-  selectTool: function(id) {
+  selectTool: function(id, reason = "unknown") {
     if (this.currentToolId == id) {
       let panel = this._toolPanels.get(id);
       if (panel) {
         // We have a panel instance, so the tool is already fully loaded.
 
         // re-focus tool to get key events again
         this.focusTool(id);
 
@@ -1842,17 +1845,18 @@ Toolbox.prototype = {
 
     // Check if the tool exists.
     if (this.panelDefinitions.find((definition) => definition.id === id) ||
         id === "options" ||
         this.additionalToolDefinitions.get(id)) {
       if (this.currentToolId) {
         this._telemetry.toolClosed(this.currentToolId);
       }
-      this._telemetry.toolOpened(id);
+
+      this._pingTelemetrySelectTool(id, reason);
     } else {
       throw new Error("No tool found");
     }
 
     // and select the right iframe
     let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
     this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
 
@@ -1868,16 +1872,41 @@ Toolbox.prototype = {
       this.focusTool(id);
 
       this.emit("select", id);
       this.emit(id + "-selected", panel);
       return panel;
     });
   },
 
+  _pingTelemetrySelectTool(id, reason) {
+    const width = Math.ceil(this.win.outerWidth / 50) * 50;
+
+    let panelName = id;
+    if (!/webconsole|inspector|jsdebugger|styleeditor|netmonitor|storage/.test(id)) {
+      panelName = "other";
+    }
+
+    this._telemetry.addEventProperties("devtools.main", "enter", panelName, null, {
+      "host": this._hostType,
+      "width": width,
+      "start_state": reason,
+      "panel_name": id,
+      "cold": !this.getPanel(id)
+    });
+
+    const pending = ["host", "width", "start_state", "panel_name", "cold"];
+    if (id === "webconsole") {
+      pending.push("message_count");
+    }
+    this._telemetry.preparePendingEvent(
+      "devtools.main", "enter", panelName, null, pending);
+    this._telemetry.toolOpened(id);
+  },
+
   /**
    * Focus a tool's panel by id
    * @param  {string} id
    *         The id of tool to focus
    */
   focusTool: function(id, state = true) {
     let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
 
@@ -1990,29 +2019,29 @@ Toolbox.prototype = {
    * Loads the tool next to the currently selected tool.
    */
   selectNextTool: function() {
     let definitions = this.component.panelDefinitions;
     const index = definitions.findIndex(({id}) => id === this.currentToolId);
     let definition = index === -1 || index >= definitions.length - 1
                      ? definitions[0]
                      : definitions[index + 1];
-    return this.selectTool(definition.id);
+    return this.selectTool(definition.id, "select_next_key");
   },
 
   /**
    * Loads the tool just left to the currently selected tool.
    */
   selectPreviousTool: function() {
     let definitions = this.component.panelDefinitions;
     const index = definitions.findIndex(({id}) => id === this.currentToolId);
     let definition = index === -1 || index < 1
                      ? definitions[definitions.length - 1]
                      : definitions[index - 1];
-    return this.selectTool(definition.id);
+    return this.selectTool(definition.id, "select_prev_key");
   },
 
   /**
    * Highlights the tool's tab if it is not the currently selected tool.
    *
    * @param {string} id
    *        The id of the tool to highlight
    */
@@ -2465,17 +2494,17 @@ Toolbox.prototype = {
 
       if (nextTool) {
         toolNameToSelect = nextTool.id;
       }
       if (previousTool) {
         toolNameToSelect = previousTool.id;
       }
       if (toolNameToSelect) {
-        this.selectTool(toolNameToSelect);
+        this.selectTool(toolNameToSelect, "tool_unloaded");
       }
     }
 
     // Remove this tool from the current panel definitions.
     this.panelDefinitions = this.panelDefinitions.filter(({id}) => id !== toolId);
     this.visibleAdditionalTools = this.visibleAdditionalTools
                                       .filter(id => id !== toolId);
     this._combineAndSortPanelDefinitions();
@@ -2576,17 +2605,17 @@ Toolbox.prototype = {
     if (objectActor.preview &&
         objectActor.preview.nodeType === domNodeConstants.ELEMENT_NODE) {
       // Open the inspector and select the DOM Element.
       await this.loadTool("inspector");
       const inspector = this.getPanel("inspector");
       const nodeFound = await inspector.inspectNodeActor(objectActor.actor,
                                                          inspectFromAnnotation);
       if (nodeFound) {
-        await this.selectTool("inspector");
+        await this.selectTool("inspector", "inspect_dom");
       }
     } else if (objectActor.type !== "null" &&
                objectActor.type !== "undefined") {
       // Open then split console and inspect the object in the variables view,
       // when the objectActor doesn't represent an undefined or null value.
       await this.openSplitConsole();
       const panel = this.getPanel("webconsole");
       const jsterm = panel.hud.jsterm;
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -552,16 +552,43 @@ class Telemetry {
       // The property was not expected, warn and bail.
       throw new Error(`An attempt was made to add the unexpected property ` +
                       `"${pendingPropName}" to a telemetry event with the ` +
                       `signature "${sig}"\n`);
     }
   }
 
   /**
+   * Adds expected properties for either a current or future pending event.
+   * This means that if preparePendingEvent() is called before or after sending
+   * the event properties they will automatically added to the event.
+   *
+   * @param {String} category
+   *        The telemetry event category (a group name for events and helps to
+   *        avoid name conflicts) e.g. "devtools.main"
+   * @param {String} method
+   *        The telemetry event method (describes the type of event that
+   *        occurred e.g. "open")
+   * @param {String} object
+   *        The telemetry event object name (the name of the object the event
+   *        occurred on) e.g. "tools" or "setting"
+   * @param {String|null} value
+   *        The telemetry event value (a user defined value, providing context
+   *        for the event) e.g. "console"
+   * @param {String} pendingObject
+   *        An object containing key, value pairs that should be added to the
+   *        event as properties.
+   */
+  addEventProperties(category, method, object, value, pendingObject) {
+    for (let [key, val] of Object.entries(pendingObject)) {
+      this.addEventProperty(category, method, object, value, key, val);
+    }
+  }
+
+  /**
    * Send a telemetry event.
    *
    * @param {String} category
    *        The telemetry event category (a group name for events and helps to
    *        avoid name conflicts) e.g. "devtools.main"
    * @param {String} method
    *        The telemetry event method (describes the type of event that
    *        occurred e.g. "open")
--- a/devtools/docs/frontend/telemetry.md
+++ b/devtools/docs/frontend/telemetry.md
@@ -274,16 +274,25 @@ this._telemetry.preparePendingEvent("dev
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "first_panel", "inspector");
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "host", "bottom");
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "splitconsole", false);
 this._telemetry.addEventProperty(
   "devtools.main", "open", "tools", null, "width", 1024);
+
+// You can also add properties in batches using e.g.:
+this._telemetry.addEventProperties("devtools.main", "open", "tools", null, {
+  "first_panel": "inspector",
+  "host": "bottom",
+  "splitconsole": false,
+  "width": 1024
+});
+
 ```
 
 Notes:
 
 - `mytoolname` is the id we declared in the `Scalars.yaml` module.
 - Because we are not logging tool's time opened in `Scalars.yaml` we don't care
   about toolClosed. Of course, if there was an accompanying `timerHistogram`
   field defined in `telemetry.js` and `histograms.json` then `toolClosed` should
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -25,16 +25,17 @@ loader.lazyRequireGetter(this, "StackTra
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 
 // Overwrite implemented listeners for workers so that we don't attempt
 // to load an unsupported module.
 if (isWorker) {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/worker-listeners", true);
   loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/worker-listeners", true);
 } else {
   loader.lazyRequireGetter(this, "ConsoleAPIListener", "devtools/server/actors/webconsole/listeners", true);
@@ -70,16 +71,18 @@ function WebConsoleActor(connection, par
   this.dbg = this.parentActor.makeDebugger();
 
   this._netEvents = new Map();
   this._networkEventActorsByURL = new Map();
   this._gripDepth = 0;
   this._listeners = new Set();
   this._lastConsoleInputEvaluation = undefined;
 
+  this._telemetry = new Telemetry();
+
   this.objectGrip = this.objectGrip.bind(this);
   this._onWillNavigate = this._onWillNavigate.bind(this);
   this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(this);
   EventEmitter.on(this.parentActor, "changed-toplevel-document",
             this._onChangedToplevelDocument);
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
@@ -857,16 +860,19 @@ WebConsoleActor.prototype =
             }
             messages.push(message);
           });
           break;
         }
       }
     }
 
+    this._telemetry.addEventProperty(
+      "devtools.main", "enter", "webconsole", null, "message_count", messages.length);
+
     return {
       from: this.actorID,
       messages: messages,
     };
   },
 
   /**
    * Handler for the "evaluateJSAsync" request. This method evaluates the given
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -183,8 +183,23 @@ devtools.main:
     notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
     record_in_processes: ["main"]
     description: User closes devtools toolbox.
     release_channel_collection: opt-out
     expiry_version: never
     extra_keys:
       host: "Toolbox host (positioning): bottom, side, window or other."
       width: Toolbox width rounded up to the nearest 50px.
+  enter:
+    objects: ["webconsole", "inspector", "jsdebugger", "styleeditor", "netmonitor", "storage", "other"]
+    bug_numbers: [1441070]
+    notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
+    record_in_processes: ["main"]
+    description: User opens a tool in the devtools toolbox.
+    release_channel_collection: opt-out
+    expiry_version: never
+    extra_keys:
+      host: "Toolbox host (positioning): bottom, side, window or other."
+      width: Toolbox width rounded up to the nearest 50px.
+      message_count: The number of cached console messages.
+      start_state: debuggerStatement, breakpoint, exception, tab_switch, toolbox_show, initial_panel, toggle_settings_off, toggle_settings_on, key_shortcut, select_next_key, select_prev_key, tool_unloaded, inspect_dom, unknown etc.
+      panel_name: The name of the panel opened, webconsole, inspector, jsdebugger, styleeditor, netmonitor, storage or other
+      cold: Is this the first time the current panel has been opened in this toolbox?