--- 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?