--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -4,23 +4,21 @@
"use strict";
const {Cu} = require("chrome");
const Services = require("Services");
const {DevToolsShim} = require("chrome://devtools-startup/content/DevToolsShim.jsm");
-// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
loader.lazyRequireGetter(this, "TabTarget", "devtools/client/framework/target", true);
-loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true);
-loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice", true);
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
loader.lazyRequireGetter(this, "WebExtensionInspectedWindowFront",
"devtools/shared/fronts/webextension-inspected-window", true);
const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
require("devtools/client/definitions");
@@ -38,16 +36,18 @@ const MAX_ORDINAL = 99;
function DevTools() {
this._tools = new Map(); // Map<toolId, tool>
this._themes = new Map(); // Map<themeId, theme>
this._toolboxes = new Map(); // Map<target, toolbox>
// List of toolboxes that are still in process of creation
this._creatingToolboxes = new Map(); // Map<target, toolbox Promise>
EventEmitter.decorate(this);
+ this._telemetry = new Telemetry();
+ this._telemetry.setEventRecordingEnabled("devtools.main", true);
// Listen for changes to the theme pref.
this._onThemeChanged = this._onThemeChanged.bind(this);
addThemeObserver(this._onThemeChanged);
// This is important step in initialization codepath where we are going to
// start registering all default tools and themes: create menuitems, keys, emit
// related events.
@@ -476,16 +476,21 @@ DevTools.prototype = {
toolbox = await toolboxPromise;
this._creatingToolboxes.delete(target);
if (startTime) {
this.logToolboxOpenTime(toolbox.currentToolId, startTime);
}
this._firstShowToolbox = false;
}
+
+ let width = Math.ceil(toolbox.win.outerWidth / 50) * 50;
+ this._telemetry.addEventProperty(
+ "devtools.main", "open", "tools", null, "width", width);
+
return toolbox;
},
/**
* Log telemetry related to toolbox opening.
* Two distinct probes are logged. One for cold startup, when we open the very first
* toolbox. This one includes devtools framework loading. And a second one for all
* subsequent toolbox opening, which should all be faster.
@@ -495,20 +500,44 @@ DevTools.prototype = {
* The id of the opened tool.
* @param {Number} startTime
* Indicates the time at which the user event related to the toolbox
* opening started. This is a `performance.now()` timing.
*/
logToolboxOpenTime(toolId, startTime) {
let { performance } = Services.appShell.hiddenDOMWindow;
let delay = performance.now() - startTime;
+
let telemetryKey = this._firstShowToolbox ?
"DEVTOOLS_COLD_TOOLBOX_OPEN_DELAY_MS" : "DEVTOOLS_WARM_TOOLBOX_OPEN_DELAY_MS";
- let histogram = Services.telemetry.getKeyedHistogramById(telemetryKey);
- histogram.add(toolId, delay);
+ this._telemetry.logKeyed(telemetryKey, toolId, delay);
+
+ this._telemetry.addEventProperty(
+ "devtools.main", "open", "tools", null, "first_panel",
+ this.makeToolIdHumanReadable(toolId));
+ },
+
+ makeToolIdHumanReadable(toolId) {
+ if (/^[0-9a-fA-F]{40}_temporary-addon/.test(toolId)) {
+ return "temporary-addon";
+ }
+
+ let matches = toolId.match(
+ /^_([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_/
+ );
+ if (matches && matches.length === 2) {
+ return matches[1];
+ }
+
+ matches = toolId.match(/^_?(.*)-\d+-\d+-devtools-panel$/);
+ if (matches && matches.length === 2) {
+ return matches[1];
+ }
+
+ return toolId;
},
async createToolbox(target, toolId, hostType, hostOptions) {
let manager = new ToolboxHostManager(target, hostType, hostOptions);
let toolbox = await manager.create(toolId);
this._toolboxes.set(target, toolbox);
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -540,16 +540,21 @@ Toolbox.prototype = {
await this.selectTool(this._defaultToolId);
// 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);
+ } else {
+ this._telemetry.addEventProperty(
+ "devtools.main", "open", "tools", null, "splitconsole", false);
}
await promise.all([
splitConsolePromise,
buttonsPromise,
framesPromise
]);
@@ -695,27 +700,46 @@ Toolbox.prototype = {
case Toolbox.HostType.BOTTOM: return 0;
case Toolbox.HostType.SIDE: return 1;
case Toolbox.HostType.WINDOW: return 2;
case Toolbox.HostType.CUSTOM: return 3;
default: return 9;
}
},
+ // Return HostType string for telemetry
+ _getTelemetryHostString: function() {
+ switch (this.hostType) {
+ case Toolbox.HostType.BOTTOM: return "bottom";
+ case Toolbox.HostType.SIDE: return "side";
+ case Toolbox.HostType.WINDOW: return "window";
+ case Toolbox.HostType.CUSTOM: return "other";
+ default: return "bottom";
+ }
+ },
+
_pingTelemetry: function() {
this._telemetry.toolOpened("toolbox");
this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
system.getScreenDimensions());
this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
// Log current theme. The question we want to answer is:
// "What proportion of users use which themes?"
let currentTheme = Services.prefs.getCharPref("devtools.theme");
this._telemetry.logKeyedScalar(CURRENT_THEME_SCALAR, currentTheme, 1);
+
+ this._telemetry.preparePendingEvent(
+ "devtools.main", "open", "tools", null,
+ ["entrypoint", "first_panel", "host", "splitconsole", "width"]
+ );
+ this._telemetry.addEventProperty(
+ "devtools.main", "open", "tools", null, "host", this._getTelemetryHostString()
+ );
},
/**
* Create a simple object to store the state of a toolbox button. The checked state of
* a button can be updated arbitrarily outside of the scope of the toolbar and its
* controllers. In order to simplify this interaction this object emits an
* "updatechecked" event any time the isChecked value is updated, allowing any consuming
* components to listen and respond to updates.
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -8,25 +8,33 @@
* Comprehensive documentation is in docs/frontend/telemetry.md
*/
"use strict";
const Services = require("Services");
const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
+// Object to be shared among all instances.
+const PENDING_EVENTS = new Map();
+const PENDING_EVENT_PROPERTIES = new Map();
+
class Telemetry {
constructor() {
// Bind pretty much all functions so that callers do not need to.
this.toolOpened = this.toolOpened.bind(this);
this.toolClosed = this.toolClosed.bind(this);
this.log = this.log.bind(this);
this.logScalar = this.logScalar.bind(this);
this.logKeyedScalar = this.logKeyedScalar.bind(this);
this.logOncePerBrowserVersion = this.logOncePerBrowserVersion.bind(this);
+ this.recordEvent = this.recordEvent.bind(this);
+ this.setEventRecordingEnabled = this.setEventRecordingEnabled.bind(this);
+ this.preparePendingEvent = this.preparePendingEvent.bind(this);
+ this.addEventProperty = this.addEventProperty.bind(this);
this.destroy = this.destroy.bind(this);
this._timers = new Map();
}
get histograms() {
return {
toolbox: {
@@ -285,20 +293,20 @@ class Telemetry {
* Value to store.
*/
logScalar(scalarId, value) {
if (!scalarId) {
return;
}
try {
- if (isNaN(value)) {
- dump(`Warning: An attempt was made to write a non-numeric value ` +
- `${value} to the ${scalarId} scalar. Only numeric values are ` +
- `allowed.`);
+ if (isNaN(value) && typeof value !== "boolean") {
+ dump(`Warning: An attempt was made to write a non-numeric and ` +
+ `non-boolean value ${value} to the ${scalarId} scalar. Only ` +
+ `numeric and boolean values are allowed.`);
return;
}
Services.telemetry.scalarSet(scalarId, value);
} catch (e) {
dump(`Warning: An attempt was made to write to the ${scalarId} ` +
`scalar, which is not defined in Scalars.yaml\n`);
}
@@ -379,16 +387,199 @@ class Telemetry {
lastVersionHistogramUpdated !== currentVersion) {
latestObj[perUserHistogram] = currentVersion;
latest = JSON.stringify(latestObj);
Services.prefs.setCharPref(TOOLS_OPENED_PREF, latest);
this.log(perUserHistogram, value);
}
}
+ /**
+ * Event telemetry is disabled by default. Use this method to enable it for
+ * a particular category.
+ *
+ * @param {String} category
+ * The telemetry event category e.g. "devtools.main"
+ * @param {Boolean} enabled
+ * Enabled: true or false.
+ */
+ setEventRecordingEnabled(category, enabled) {
+ return Services.telemetry.setEventRecordingEnabled(category, enabled);
+ }
+
+ /**
+ * Telemetry events often need to make use of a number of properties from
+ * completely different codepaths. To make this possible we create a
+ * "pending event" along with an array of property names that we need to wait
+ * for before sending the event.
+ *
+ * As each property is received via addEventProperty() we check if all
+ * properties have been received. Once they have all been received we send the
+ * 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")
+ * @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 {Array} expected
+ * An array of the properties needed before sending the telemetry
+ * event e.g.
+ * [
+ * "host",
+ * "width"
+ * ]
+ */
+ preparePendingEvent(category, method, object, value, expected = []) {
+ const sig = `${category},${method},${object},${value}`;
+
+ if (expected.length === 0) {
+ throw new Error(`preparePendingEvent() was called without any expected ` +
+ `properties.`);
+ }
+
+ PENDING_EVENTS.set(sig, {
+ extra: {},
+ expected: new Set(expected)
+ });
+
+ const props = PENDING_EVENT_PROPERTIES.get(sig);
+ if (props) {
+ for (let [name, val] of Object.entries(props)) {
+ this.addEventProperty(category, method, object, value, name, val);
+ }
+ PENDING_EVENT_PROPERTIES.delete(sig);
+ }
+ }
+
+ /**
+ * Adds an expected property 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} pendingPropName
+ * The pending property name
+ * @param {String} pendingPropValue
+ * The pending property value
+ */
+ addEventProperty(category, method, object, value, pendingPropName, pendingPropValue) {
+ const sig = `${category},${method},${object},${value}`;
+
+ // If the pending event has not been created add the property to the pending
+ // list.
+ if (!PENDING_EVENTS.has(sig)) {
+ PENDING_EVENT_PROPERTIES.set(sig, {
+ [pendingPropName]: pendingPropValue
+ });
+ return;
+ }
+
+ const { expected, extra } = PENDING_EVENTS.get(sig);
+
+ if (expected.has(pendingPropName)) {
+ extra[pendingPropName] = pendingPropValue;
+
+ if (expected.size === Object.keys(extra).length) {
+ this._sendPendingEvent(category, method, object, value);
+ }
+ } else {
+ // 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`);
+ }
+ }
+
+ /**
+ * 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")
+ * @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 {Object} extra
+ * The telemetry event extra object containing the properties that will
+ * be sent with the event e.g.
+ * {
+ * host: "bottom",
+ * width: "1024"
+ * }
+ */
+ recordEvent(category, method, object, value, extra) {
+ // Only string values are allowed so cast all values to strings.
+ for (let [name, val] of Object.entries(extra)) {
+ val = val + "";
+ extra[name] = val;
+
+ if (val.length > 80) {
+ const sig = `${category},${method},${object},${value}`;
+
+ throw new Error(`The property "${name}" was added to a telemetry ` +
+ `event with the signature ${sig} but it's value ` +
+ `"${val}" is longer than the maximum allowed length ` +
+ `of 80 characters\n`);
+ }
+ }
+ Services.telemetry.recordEvent(category, method, object, value, extra);
+ }
+
+ /**
+ * A private method that is not to be used externally. This method is used to
+ * prepare a pending telemetry event for sending and then send it via
+ * recordEvent().
+ *
+ * @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"
+ */
+ _sendPendingEvent(category, method, object, value) {
+ const sig = `${category},${method},${object},${value}`;
+ const { extra } = PENDING_EVENTS.get(sig);
+
+ PENDING_EVENTS.delete(sig);
+ PENDING_EVENT_PROPERTIES.delete(sig);
+ this.recordEvent(category, method, object, value, extra);
+ }
+
destroy() {
for (let histogramId of this._timers.keys()) {
this.stopTimer(histogramId);
}
}
}
module.exports = Telemetry;
--- a/devtools/startup/DevToolsShim.jsm
+++ b/devtools/startup/DevToolsShim.jsm
@@ -8,16 +8,25 @@ const { Services } = ChromeUtils.import(
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
XPCOMUtils.defineLazyGetter(this, "DevtoolsStartup", () => {
return Cc["@mozilla.org/devtools/startup-clh;1"]
.getService(Ci.nsICommandLineHandler)
.wrappedJSObject;
});
+// We don't want to spend time initializing the full loader here so we create
+// our own lazy require.
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
+ const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
+ const Telemetry = require("devtools/client/shared/telemetry");
+
+ return Telemetry;
+});
+
const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
this.EXPORTED_SYMBOLS = [
"DevToolsShim",
];
function removeItem(array, callback) {
@@ -34,16 +43,24 @@ function removeItem(array, callback) {
* It can be used to start listening to devtools events before DevTools are ready. As soon
* as DevTools are enabled, the DevToolsShim will forward all the requests received until
* then to the real DevTools instance.
*/
this.DevToolsShim = {
_gDevTools: null,
listeners: [],
+ get telemetry() {
+ if (!this._telemetry) {
+ this._telemetry = new Telemetry();
+ this._telemetry.setEventRecordingEnabled("devtools.main", true);
+ }
+ return this._telemetry;
+ },
+
/**
* Returns true if DevTools are enabled for the current profile. If devtools are not
* enabled, initializing DevTools will open the onboarding page. Some entry points
* should no-op in this case.
*/
isEnabled: function() {
let enabled = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF);
return enabled && !this.isDisabledByPolicy();
@@ -208,16 +225,22 @@ this.DevToolsShim = {
* optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
* in toolkit/components/telemetry/Histograms.json
*/
initDevTools: function(reason) {
if (!this.isEnabled()) {
throw new Error("DevTools are not enabled and can not be initialized.");
}
+ if (reason) {
+ this.telemetry.addEventProperty(
+ "devtools.main", "open", "tools", null, "entrypoint", reason
+ );
+ }
+
if (!this.isInitialized()) {
DevtoolsStartup.initDevTools(reason);
}
},
};
/**
* Compatibility layer for webextensions.
--- a/devtools/startup/devtools-startup.js
+++ b/devtools/startup/devtools-startup.js
@@ -30,25 +30,35 @@ const kDebuggerPrefs = [
// startup.
const TOOLBAR_VISIBLE_PREF = "devtools.toolbar.visible";
const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
ChromeUtils.defineModuleGetter(this, "CustomizableWidgets",
"resource:///modules/CustomizableWidgets.jsm");
+// We don't want to spend time initializing the full loader here so we create
+// our own lazy require.
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
+ const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
+ const Telemetry = require("devtools/client/shared/telemetry");
+
+ return Telemetry;
+});
+
XPCOMUtils.defineLazyGetter(this, "StartupBundle", function() {
const url = "chrome://devtools-startup/locale/startup.properties";
return Services.strings.createBundle(url);
});
XPCOMUtils.defineLazyGetter(this, "KeyShortcutsBundle", function() {
const url = "chrome://devtools-startup/locale/key-shortcuts.properties";
return Services.strings.createBundle(url);
@@ -184,16 +194,24 @@ DevToolsStartup.prototype = {
/**
* Boolean flag to check if the devtools initialization was already sent to telemetry.
* We only want to record one devtools entry point per Firefox run, but we are not
* interested in all the entry points (e.g. devtools.toolbar.visible).
*/
recorded: false,
+ get telemetry() {
+ if (!this._telemetry) {
+ this._telemetry = new Telemetry();
+ this._telemetry.setEventRecordingEnabled("devtools.main", true);
+ }
+ return this._telemetry;
+ },
+
/**
* Flag that indicates if the developer toggle was already added to customizableUI.
*/
developerToggleCreated: false,
isDisabledByPolicy: function() {
return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
},
@@ -311,17 +329,17 @@ DevToolsStartup.prototype = {
pingOnboardingTelemetry() {
// Only ping telemetry once per profile.
let alreadyLoggedPref = "devtools.onboarding.telemetry.logged";
if (Services.prefs.getBoolPref(alreadyLoggedPref)) {
return;
}
let scalarId = "devtools.onboarding.is_devtools_user";
- Services.telemetry.scalarSet(scalarId, this.isDevToolsUser());
+ this.telemetry.logScalar(scalarId, this.isDevToolsUser());
Services.prefs.setBoolPref(alreadyLoggedPref, true);
},
/**
* Register listeners to all possible entry points for Developer Tools.
* But instead of implementing the actual actions, defer to DevTools codebase.
* In most cases, it only needs to call this.initDevTools which handles the rest.
* We do that to prevent loading any DevTools module until the user intent to use them.
@@ -821,28 +839,37 @@ DevToolsStartup.prototype = {
}
if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
cmdLine.preventDefault = true;
}
},
sendEntryPointTelemetry(reason) {
- if (reason && !this.recorded) {
- // Only save the first call for each firefox run as next call
- // won't necessarely start the tool. For example key shortcuts may
- // only change the currently selected tool.
- try {
- Services.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT")
- .add(reason);
- } catch (e) {
- dump("DevTools telemetry entry point failed: " + e + "\n");
- }
- this.recorded = true;
+ if (!reason) {
+ return;
+ }
+
+ this.telemetry.addEventProperty(
+ "devtools.main", "open", "tools", null, "entrypoint", reason
+ );
+
+ if (this.recorded) {
+ return;
}
+
+ // Only save the first call for each firefox run as next call
+ // won't necessarely start the tool. For example key shortcuts may
+ // only change the currently selected tool.
+ try {
+ this.telemetry.log("DEVTOOLS_ENTRY_POINT", reason);
+ } catch (e) {
+ dump("DevTools telemetry entry point failed: " + e + "\n");
+ }
+ this.recorded = true;
},
// Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded
// in a window window.
get KeyShortcuts() {
return KeyShortcuts;
},
get wrappedJSObject() {
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -156,8 +156,24 @@ telemetry.test.second:
objects: ["object1", "object2", "object3"]
bug_numbers: [1286606]
notification_emails: ["telemetry-client-dev@mozilla.com"]
record_in_processes: ["main"]
description: This is a test entry for Telemetry.
expiry_version: never
extra_keys:
key1: This is just a test description.
+
+devtools.main:
+ open:
+ objects: ["tools"]
+ bug_numbers: [1416024]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"]
+ record_in_processes: ["main"]
+ description: User opens devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ entrypoint: How was the toolbox opened? CommandLine, ContextMenu, DeveloperToolbar, HamburgerMenu, KeyShortcut, SessionRestore or SystemMenu
+ first_panel: The name of the first panel opened.
+ host: "Toolbox host (positioning): bottom, side, window or other."
+ splitconsole: Indicates whether the split console was open.
+ width: Toolbox width rounded up to the nearest 50px.