Bug 916804 - Telemetry for WebIDE. r=mratcliffe, r=paul
☠☠ backed out by 2a621e79e6b9 ☠ ☠
authorJ. Ryan Stinnett <jryans@gmail.com>
Wed, 06 Aug 2014 20:37:00 -0400
changeset 198577 5c1ba06b972b6a8b903362ad0ebb9b7112665e86
parent 198576 07bf0e09b1b0d47e310538f277113dfe9e15fc93
child 198578 2ead24c96a8e9e278fca79543c91aac03b031800
push id27277
push userryanvm@gmail.com
push dateFri, 08 Aug 2014 20:25:17 +0000
treeherdermozilla-central@1d6500527f66 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe, paul
bugs916804
milestone34.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 916804 - Telemetry for WebIDE. r=mratcliffe, r=paul
browser/devtools/shared/telemetry.js
browser/devtools/webide/content/runtimedetails.js
browser/devtools/webide/content/webide.js
browser/devtools/webide/modules/app-manager.js
browser/devtools/webide/modules/runtimes.js
browser/devtools/webide/test/chrome.ini
browser/devtools/webide/test/head.js
browser/devtools/webide/test/test_telemetry.html
toolkit/components/telemetry/Histograms.json
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -31,17 +31,17 @@
  * 6. When your tool is closed call:
  *      this._telemetry.toolClosed("mytoolname");
  *
  * Note:
  * You can view telemetry stats for your local Firefox instance via
  * about:telemetry.
  *
  * You can view telemetry stats for large groups of Firefox users at
- * metrics.mozilla.com.
+ * telemetry.mozilla.org.
  */
 
 const TOOLS_OPENED_PREF = "devtools.telemetry.tools.opened.version";
 
 this.Telemetry = function() {
   // Bind pretty much all functions so that callers do not need to.
   this.toolOpened = this.toolOpened.bind(this);
   this.toolClosed = this.toolClosed.bind(this);
@@ -165,16 +165,21 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
     },
     developertoolbar: {
       histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
     },
+    webide: {
+      histogram: "DEVTOOLS_WEBIDE_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG",
+      timerHistogram: "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS"
+    },
     custom: {
       histogram: "DEVTOOLS_CUSTOM_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
     }
   },
 
   /**
@@ -189,33 +194,52 @@ Telemetry.prototype = {
 
     if (charts.histogram) {
       this.log(charts.histogram, true);
     }
     if (charts.userHistogram) {
       this.logOncePerBrowserVersion(charts.userHistogram, true);
     }
     if (charts.timerHistogram) {
-      this._timers.set(charts.timerHistogram, new Date());
+      this.startTimer(charts.timerHistogram);
     }
   },
 
   toolClosed: function(id) {
     let charts = this._histograms[id];
 
     if (!charts || !charts.timerHistogram) {
       return;
     }
 
-    let startTime = this._timers.get(charts.timerHistogram);
+    this.stopTimer(charts.timerHistogram);
+  },
 
+  /**
+   * Record the start time for a timing-based histogram entry.
+   *
+   * @param String histogramId
+   *        Histogram in which the data is to be stored.
+   */
+  startTimer: function(histogramId) {
+    this._timers.set(histogramId, new Date());
+  },
+
+  /**
+   * Stop the timer and log elasped time for a timing-based histogram entry.
+   *
+   * @param String histogramId
+   *        Histogram in which the data is to be stored.
+   */
+  stopTimer: function(histogramId) {
+    let startTime = this._timers.get(histogramId);
     if (startTime) {
       let time = (new Date() - startTime) / 1000;
-      this.log(charts.timerHistogram, time);
-      this._timers.delete(charts.timerHistogram);
+      this.log(histogramId, time);
+      this._timers.delete(histogramId);
     }
   },
 
   /**
    * Log a value to a histogram.
    *
    * @param  {String} histogramId
    *         Histogram in which the data is to be stored.
@@ -253,20 +277,17 @@ Telemetry.prototype = {
       latestObj[perUserHistogram] = currentVersion;
       latest = JSON.stringify(latestObj);
       Services.prefs.setCharPref(TOOLS_OPENED_PREF, latest);
       this.log(perUserHistogram, value);
     }
   },
 
   destroy: function() {
-    for (let [histogram, time] of this._timers) {
-      time = (new Date() - time) / 1000;
-
-      this.log(histogram, time);
-      this._timers.delete(histogram);
+    for (let histogramId of this._timers.keys()) {
+      this.stopTimer(histogramId);
     }
   }
 };
 
 XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
 });
--- a/browser/devtools/webide/content/runtimedetails.js
+++ b/browser/devtools/webide/content/runtimedetails.js
@@ -84,17 +84,17 @@ function CheckLockState() {
 
   if (AppManager.connection &&
       AppManager.connection.status == Connection.Status.CONNECTED &&
       AppManager.deviceFront) {
 
     // ADB check
     if (AppManager.selectedRuntime instanceof USBRuntime) {
       let device = Devices.getByName(AppManager.selectedRuntime.id);
-      if (device.summonRoot) {
+      if (device && device.summonRoot) {
         device.isRoot().then(isRoot => {
           if (isRoot) {
             adbCheckResult.textContent = sYes;
             flipCertPerfButton.removeAttribute("disabled");
           } else {
             adbCheckResult.textContent = sNo;
             adbRootAction.removeAttribute("hidden");
           }
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -16,16 +16,17 @@ const {Services} = Cu.import("resource:/
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const {Connection} = require("devtools/client/connection-manager");
 const {AppManager} = require("devtools/webide/app-manager");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const ProjectEditor = require("projecteditor/projecteditor");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {GetAvailableAddons} = require("devtools/webide/addons");
 const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
+const Telemetry = require("devtools/shared/telemetry");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const HTML = "http://www.w3.org/1999/xhtml";
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
 
 // download template index early
 GetTemplatesJSON(true);
@@ -42,16 +43,19 @@ window.addEventListener("load", function
 
 window.addEventListener("unload", function onUnload() {
   window.removeEventListener("unload", onUnload);
   UI.uninit();
 });
 
 let UI = {
   init: function() {
+    this._telemetry = new Telemetry();
+    this._telemetry.toolOpened("webide");
+
     AppManager.init();
 
     this.onMessage = this.onMessage.bind(this);
     window.addEventListener("message", this.onMessage);
 
     this.appManagerUpdate = this.appManagerUpdate.bind(this);
     AppManager.on("app-manager-update", this.appManagerUpdate);
 
@@ -93,16 +97,18 @@ let UI = {
     }
   },
 
   uninit: function() {
     window.removeEventListener("focus", this.onfocus, true);
     AppManager.off("app-manager-update", this.appManagerUpdate);
     AppManager.uninit();
     window.removeEventListener("message", this.onMessage);
+    this.updateConnectionTelemetry();
+    this._telemetry.toolClosed("webide");
   },
 
   onfocus: function() {
     // Because we can't track the activity in the folder project,
     // we need to validate the project regularly. Let's assume that
     // if a modification happened, it happened when the window was
     // not focused.
     if (AppManager.selectedProject &&
@@ -116,16 +122,17 @@ let UI = {
     // Got a message from app-manager.js
     switch (what) {
       case "runtimelist":
         this.updateRuntimeList();
         break;
       case "connection":
         this.updateRuntimeButton();
         this.updateCommands();
+        this.updateConnectionTelemetry();
         break;
       case "project":
         this.updateTitle();
         this.closeToolbox();
         this.updateCommands();
         this.updateProjectButton();
         this.openProject();
         break;
@@ -213,22 +220,23 @@ let UI = {
     }, 30000);
   },
 
   cancelBusyTimeout: function() {
     clearTimeout(this._busyTimeout);
   },
 
   busyWithProgressUntil: function(promise, operationDescription) {
-    this.busyUntil(promise, operationDescription);
+    let busy = this.busyUntil(promise, operationDescription);
     let win = document.querySelector("window");
     let progress = document.querySelector("#action-busy-determined");
     progress.mode = "undetermined";
     win.classList.add("busy-determined");
     win.classList.remove("busy-undetermined");
+    return busy;
   },
 
   busyUntil: function(promise, operationDescription) {
     // Freeze the UI until the promise is resolved. A 30s timeout
     // will unfreeze the UI, just in case the promise never gets
     // resolved.
     this._busyPromise = promise;
     this._busyOperationDescription = operationDescription;
@@ -328,29 +336,71 @@ let UI = {
         }, true);
       }
     }
   },
 
   connectToRuntime: function(runtime) {
     let name = runtime.getName();
     let promise = AppManager.connectToRuntime(runtime);
+    promise.then(() => this.initConnectionTelemetry());
     return this.busyUntil(promise, "connecting to runtime");
   },
 
   updateRuntimeButton: function() {
     let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
     if (!AppManager.selectedRuntime) {
       labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
     } else {
       let name = AppManager.selectedRuntime.getName();
       labelNode.setAttribute("value", name);
     }
   },
 
+  _actionsToLog: new Set(),
+
+  /**
+   * For each new connection, track whether play and debug were ever used.  Only
+   * one value is collected for each button, even if they are used multiple
+   * times during a connection.
+   */
+  initConnectionTelemetry: function() {
+    this._actionsToLog.add("play");
+    this._actionsToLog.add("debug");
+  },
+
+  /**
+   * Action occurred.  Log that it happened, and remove it from the loggable
+   * set.
+   */
+  onAction: function(action) {
+    if (!this._actionsToLog.has(action)) {
+      return;
+    }
+    this.logActionState(action, true);
+    this._actionsToLog.delete(action);
+  },
+
+  /**
+   * Connection status changed or we are shutting down.  Record any loggable
+   * actions as having not occurred.
+   */
+  updateConnectionTelemetry: function() {
+    for (let action of this._actionsToLog.values()) {
+      this.logActionState(action, false);
+    }
+    this._actionsToLog.clear();
+  },
+
+  logActionState: function(action, state) {
+    let histogramId = "DEVTOOLS_WEBIDE_CONNECTION_" +
+                      action.toUpperCase() + "_USED";
+    this._telemetry.log(histogramId, state);
+  },
+
   /********** PROJECTS **********/
 
   // Panel & button
 
   updateProjectButton: function() {
     let buttonNode = document.querySelector("#project-panel-button");
     let labelNode = buttonNode.querySelector(".panel-button-label");
     let imageNode = buttonNode.querySelector(".panel-button-image");
@@ -655,18 +705,17 @@ let UI = {
     // properly anymore.
     this.toolboxIframe.remove();
     this.toolboxIframe = null;
 
     let splitter = document.querySelector(".devtools-horizontal-splitter");
     splitter.setAttribute("hidden", "true");
     document.querySelector("#action-button-debug").removeAttribute("active");
   },
-}
-
+};
 
 let Cmds = {
   quit: function() {
     window.close();
   },
 
   /**
    * testOptions: {       chrome mochitest support
@@ -904,32 +953,43 @@ let Cmds = {
     UI.selectDeckPanel("runtimedetails");
   },
 
   showMonitor: function() {
     UI.selectDeckPanel("monitor");
   },
 
   play: function() {
+    let busy;
     switch(AppManager.selectedProject.type) {
       case "packaged":
-        return UI.busyWithProgressUntil(AppManager.installAndRunProject(), "installing and running app");
+        busy = UI.busyWithProgressUntil(AppManager.installAndRunProject(),
+                                        "installing and running app");
+        break;
       case "hosted":
-        return UI.busyUntil(AppManager.installAndRunProject(), "installing and running app");
+        busy = UI.busyUntil(AppManager.installAndRunProject(),
+                            "installing and running app");
+        break;
       case "runtimeApp":
-        return UI.busyUntil(AppManager.runRuntimeApp(), "running app");
+        busy = UI.busyUntil(AppManager.runRuntimeApp(), "running app");
+        break;
     }
-    return promise.reject();
+    if (!busy) {
+      return promise.reject();
+    }
+    UI.onAction("play");
+    return busy;
   },
 
   stop: function() {
     return UI.busyUntil(AppManager.stopRunningApp(), "stopping app");
   },
 
   toggleToolbox: function() {
+    UI.onAction("debug");
     if (UI.toolboxIframe) {
       UI.closeToolbox();
       return promise.resolve();
     } else {
       UI.toolboxPromise = AppManager.getTarget().then((target) => {
         return UI.showToolbox(target);
       }, console.error);
       UI.busyUntil(UI.toolboxPromise, "opening toolbox");
@@ -956,9 +1016,9 @@ let Cmds = {
 
   showAddons: function() {
     UI.selectDeckPanel("addons");
   },
 
   showPrefs: function() {
     UI.selectDeckPanel("prefs");
   },
-}
+};
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -20,16 +20,17 @@ const {ConnectionManager, Connection} = 
 const AppActorFront = require("devtools/app-actor-front");
 const {getDeviceFront} = require("devtools/server/actors/device");
 const {getPreferenceFront} = require("devtools/server/actors/preference");
 const {setTimeout} = require("sdk/timers");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const {USBRuntime, WiFiRuntime, SimulatorRuntime,
        gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
 const discovery = require("devtools/toolkit/discovery/discovery");
+const Telemetry = require("devtools/shared/telemetry");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const WIFI_SCANNING_PREF = "devtools.remote.wifi.scan";
 
 exports.AppManager = AppManager = {
 
   // FIXME: will break when devtools/app-manager will be removed:
@@ -61,16 +62,18 @@ exports.AppManager = AppManager = {
     this.trackWiFiRuntimes();
     this.trackSimulatorRuntimes();
 
     this.onInstallProgress = this.onInstallProgress.bind(this);
     AppActorFront.on("install-progress", this.onInstallProgress);
 
     this.observe = this.observe.bind(this);
     Services.prefs.addObserver(WIFI_SCANNING_PREF, this, false);
+
+    this._telemetry = new Telemetry();
   },
 
   uninit: function() {
     AppActorFront.off("install-progress", this.onInstallProgress);
     this._unlistenToApps();
     this.selectedProject = null;
     this.selectedRuntime = null;
     this.untrackUSBRuntimes();
@@ -340,16 +343,35 @@ exports.AppManager = AppManager = {
           () => {},
           () => {deferred.reject()});
       } catch(e) {
         console.error(e);
         deferred.reject();
       }
     }, deferred.reject);
 
+    // Record connection result in telemetry
+    let logResult = result => {
+      this._telemetry.log("DEVTOOLS_WEBIDE_CONNECTION_RESULT", result);
+      if (runtime.type) {
+        this._telemetry.log("DEVTOOLS_WEBIDE_" + runtime.type +
+                            "_CONNECTION_RESULT", result);
+      }
+    };
+    deferred.promise.then(() => logResult(true), () => logResult(false));
+
+    // If successful, record connection time in telemetry
+    deferred.promise.then(() => {
+      const timerId = "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS";
+      this._telemetry.startTimer(timerId);
+      this.connection.once(Connection.Events.STATUS_CHANGED, () => {
+        this._telemetry.stopTimer(timerId);
+      });
+    });
+
     return deferred.promise;
   },
 
   isMainProcessDebuggable: function() {
     return this._listTabsResponse &&
            this._listTabsResponse.consoleActor;
   },
 
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -8,21 +8,31 @@ const {Services} = Cu.import("resource:/
 const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
 const {DebuggerServer} = require("resource://gre/modules/devtools/dbg-server.jsm");
 const discovery = require("devtools/toolkit/discovery/discovery");
 const promise = require("promise");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
+// These type strings are used for logging events to Telemetry
+let RuntimeTypes = {
+  usb: "USB",
+  wifi: "WIFI",
+  simulator: "SIMULATOR",
+  remote: "REMOTE",
+  local: "LOCAL"
+};
+
 function USBRuntime(id) {
   this.id = id;
 }
 
 USBRuntime.prototype = {
+  type: RuntimeTypes.usb,
   connect: function(connection) {
     let device = Devices.getByName(this.id);
     if (!device) {
       return promise.reject("Can't find device: " + this.getName());
     }
     return device.connect().then((port) => {
       connection.host = "localhost";
       connection.port = port;
@@ -54,16 +64,17 @@ USBRuntime.prototype = {
   },
 }
 
 function WiFiRuntime(deviceName) {
   this.deviceName = deviceName;
 }
 
 WiFiRuntime.prototype = {
+  type: RuntimeTypes.wifi,
   connect: function(connection) {
     let service = discovery.getRemoteService("devtools", this.deviceName);
     if (!service) {
       return promise.reject("Can't find device: " + this.getName());
     }
     connection.host = service.host;
     connection.port = service.port;
     connection.connect();
@@ -77,16 +88,17 @@ WiFiRuntime.prototype = {
   },
 }
 
 function SimulatorRuntime(version) {
   this.version = version;
 }
 
 SimulatorRuntime.prototype = {
+  type: RuntimeTypes.simulator,
   connect: function(connection) {
     let port = ConnectionManager.getFreeTCPPort();
     let simulator = Simulator.getByVersion(this.version);
     if (!simulator || !simulator.launch) {
       return promise.reject("Can't find simulator: " + this.getName());
     }
     return simulator.launch({port: port}).then(() => {
       connection.port = port;
@@ -99,32 +111,34 @@ SimulatorRuntime.prototype = {
     return this.version;
   },
   getName: function() {
     return Simulator.getByVersion(this.version).appinfo.label;
   },
 }
 
 let gLocalRuntime = {
+  type: RuntimeTypes.local,
   connect: function(connection) {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     connection.port = null;
     connection.host = null; // Force Pipe transport
     connection.connect();
     return promise.resolve();
   },
   getName: function() {
     return Strings.GetStringFromName("local_runtime");
   },
 }
 
 let gRemoteRuntime = {
+  type: RuntimeTypes.remote,
   connect: function(connection) {
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (!win) {
       return promise.reject();
     }
     let ret = {value: connection.host + ":" + connection.port};
     Services.prompt.prompt(win,
                            Strings.GetStringFromName("remote_runtime_promptTitle"),
--- a/browser/devtools/webide/test/chrome.ini
+++ b/browser/devtools/webide/test/chrome.ini
@@ -26,8 +26,9 @@ support-files =
 
 [test_basic.html]
 [test_newapp.html]
 [test_import.html]
 [test_runtime.html]
 [test_manifestUpdate.html]
 [test_addons.html]
 [test_deviceinfo.html]
+[test_telemetry.html]
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -76,31 +76,40 @@ function closeWebIDE(win) {
 
   return deferred.promise;
 }
 
 function removeAllProjects() {
   return Task.spawn(function* () {
     yield AppProjects.load();
     let projects = AppProjects.store.object.projects;
-    for (let i = 0; i < projects.length; i++) {
-      yield AppProjects.remove(projects[i].location);
+    // AppProjects.remove mutates the projects array in-place
+    while (projects.length > 0) {
+      yield AppProjects.remove(projects[0].location);
     }
   });
 }
 
 function nextTick() {
   let deferred = promise.defer();
   SimpleTest.executeSoon(() => {
     deferred.resolve();
   });
 
   return deferred.promise;
 }
 
+function waitForTime(time) {
+  let deferred = promise.defer();
+  setTimeout(() => {
+    deferred.resolve();
+  }, time);
+  return deferred.promise;
+}
+
 function documentIsLoaded(doc) {
   let deferred = promise.defer();
   if (doc.readyState == "complete") {
     deferred.resolve();
   } else {
     doc.addEventListener("readystatechange", function onChange() {
       if (doc.readyState == "complete") {
         doc.removeEventListener("readystatechange", onChange);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/test/test_telemetry.html
@@ -0,0 +1,242 @@
+<!DOCTYPE html>
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+    <script type="application/javascript;version=1.8" src="head.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  </head>
+
+  <body>
+
+    <script type="application/javascript;version=1.8">
+      const Telemetry = require("devtools/shared/telemetry");
+      const { USBRuntime, WiFiRuntime, SimulatorRuntime, gRemoteRuntime,
+              gLocalRuntime } = require("devtools/webide/runtimes");
+
+      // Because we need to gather stats for the period of time that a tool has
+      // been opened we make use of setTimeout() to create tool active times.
+      const TOOL_DELAY = 200;
+
+      function patchTelemetry() {
+        Telemetry.prototype.telemetryInfo = {};
+        Telemetry.prototype._oldlog = Telemetry.prototype.log;
+        Telemetry.prototype.log = function(histogramId, value) {
+          if (histogramId) {
+            if (!this.telemetryInfo[histogramId]) {
+              this.telemetryInfo[histogramId] = [];
+            }
+            this.telemetryInfo[histogramId].push(value);
+          }
+        }
+      }
+
+      function resetTelemetry() {
+        Telemetry.prototype.log = Telemetry.prototype._oldlog;
+        delete Telemetry.prototype._oldlog;
+        delete Telemetry.prototype.telemetryInfo;
+      }
+
+      function cycleWebIDE() {
+        return Task.spawn(function*() {
+          let win = yield openWebIDE();
+          // Wait a bit, so we're open for a non-zero time
+          yield waitForTime(TOOL_DELAY);
+          yield closeWebIDE(win);
+        });
+      }
+
+      function addFakeRuntimes(win) {
+        // We use the real runtimes here (and switch out some functionality)
+        // so we can ensure that logging happens as it would in real use.
+
+        let usb = new USBRuntime("fakeUSB");
+        // Use local pipe instead
+        usb.connect = function(connection) {
+          ok(connection, win.AppManager.connection, "connection is valid");
+          connection.host = null; // force connectPipe
+          connection.connect();
+          return promise.resolve();
+        };
+        win.AppManager.runtimeList.usb.push(usb);
+
+        let wifi = new WiFiRuntime("fakeWiFi");
+        // Use local pipe instead
+        wifi.connect = function(connection) {
+          ok(connection, win.AppManager.connection, "connection is valid");
+          connection.host = null; // force connectPipe
+          connection.connect();
+          return promise.resolve();
+        };
+        win.AppManager.runtimeList.wifi.push(wifi);
+
+        let sim = new SimulatorRuntime("fakeSimulator");
+        // Use local pipe instead
+        sim.connect = function(connection) {
+          ok(connection, win.AppManager.connection, "connection is valid");
+          connection.host = null; // force connectPipe
+          connection.connect();
+          return promise.resolve();
+        };
+        sim.getName = function() {
+          return this.version;
+        };
+        win.AppManager.runtimeList.simulator.push(sim);
+
+        let remote = gRemoteRuntime;
+        // Use local pipe instead
+        remote.connect = function(connection) {
+          ok(connection, win.AppManager.connection, "connection is valid");
+          connection.host = null; // force connectPipe
+          connection.connect();
+          return promise.resolve();
+        };
+        let local = gLocalRuntime;
+        win.AppManager.runtimeList.custom = [gRemoteRuntime, gLocalRuntime];
+
+        win.AppManager.update("runtimelist");
+      }
+
+      function addTestApp(win) {
+        return Task.spawn(function*() {
+          let packagedAppLocation = getTestFilePath("app");
+          yield win.Cmds.importPackagedApp(packagedAppLocation);
+        });
+      }
+
+      function startConnection(win, type, index) {
+        let panelNode = win.document.querySelector("#runtime-panel");
+        let items = panelNode.querySelectorAll(".runtime-panel-item-" + type);
+        if (index === undefined) {
+          is(items.length, 1, "Found one runtime button");
+        }
+
+        let deferred = promise.defer();
+        win.AppManager.connection.once(
+            win.Connection.Events.CONNECTED,
+            () => deferred.resolve());
+
+        items[index || 0].click();
+
+        return deferred.promise;
+      }
+
+      function waitUntilConnected(win) {
+        return Task.spawn(function*() {
+          ok(win.document.querySelector("window").className, "busy", "UI is busy");
+          yield win.UI._busyPromise;
+          is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
+        });
+      }
+
+      function connectToRuntime(win, type, index) {
+        return Task.spawn(function*() {
+          yield startConnection(win, type, index);
+          yield waitUntilConnected(win);
+        });
+      }
+
+      function checkResults() {
+        let result = Telemetry.prototype.telemetryInfo;
+        for (let [histId, value] of Iterator(result)) {
+          if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+            ok(value.length === 1 && !!value[0],
+               "Per user value " + histId + " has a single value of true");
+          } else if (histId.endsWith("OPENED_BOOLEAN")) {
+            ok(value.length > 1, histId + " has more than one entry");
+
+            let okay = value.every(function(element) {
+              return !!element;
+            });
+
+            ok(okay, "All " + histId + " entries are true");
+          } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+            ok(value.length > 1, histId + " has more than one entry");
+
+            let okay = value.every(function(element) {
+              return element > 0;
+            });
+
+            ok(okay, "All " + histId + " entries have time > 0");
+          } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
+            ok(value.length === 5, histId + " has 5 connection results");
+
+            let okay = value.every(function(element) {
+              return !!element;
+            });
+
+            ok(okay, "All " + histId + " connections succeeded");
+          } else if (histId.endsWith("CONNECTION_RESULT")) {
+            ok(value.length === 1 && !!value[0],
+               histId + " has 1 successful connection");
+          } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS") {
+            ok(value.length === 5, histId + " has 5 connection results");
+
+            let okay = value.every(function(element) {
+              return element > 0;
+            });
+
+            ok(okay, "All " + histId + " connections have time > 0");
+          } else if (histId.endsWith("USED")) {
+            info(value.length);
+            ok(value.length === 5, histId + " has 5 connection actions");
+
+            let okay = value.every(function(element) {
+              return !element;
+            });
+
+            ok(okay, "All " + histId + " actions were skipped");
+          } else {
+            ok(false, "Unexpected " + histId + " was logged");
+          }
+        }
+      }
+
+      window.onload = function() {
+        SimpleTest.waitForExplicitFinish();
+        Task.spawn(function* () {
+          Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+          DebuggerServer.init(function () { return true; });
+          DebuggerServer.addBrowserActors();
+
+          patchTelemetry();
+
+          // Cycle once, so we can test for multiple opens
+          yield cycleWebIDE();
+
+          let win = yield openWebIDE();
+          // Wait a bit, so we're open for a non-zero time
+          yield waitForTime(TOOL_DELAY);
+          addFakeRuntimes(win);
+          yield addTestApp(win);
+
+          // Each one should log a connection result and non-zero connection
+          // time
+          yield connectToRuntime(win, "usb");
+          yield waitForTime(TOOL_DELAY);
+          yield connectToRuntime(win, "wifi");
+          yield waitForTime(TOOL_DELAY);
+          yield connectToRuntime(win, "simulator");
+          yield waitForTime(TOOL_DELAY);
+          yield connectToRuntime(win, "custom", 0 /* remote */);
+          yield waitForTime(TOOL_DELAY);
+          yield connectToRuntime(win, "custom", 1 /* local */);
+          yield waitForTime(TOOL_DELAY);
+          yield closeWebIDE(win);
+
+          checkResults();
+          resetTelemetry();
+
+          DebuggerServer.destroy();
+
+          SimpleTest.finish();
+        });
+      }
+    </script>
+  </body>
+</html>
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5866,16 +5866,21 @@
     "kind": "boolean",
     "description": "How many times has the devtool's Responsive View been opened via the toolbox button?"
   },
   "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has the devtool's Developer Toolbar been opened via the toolbox button?"
   },
+  "DEVTOOLS_WEBIDE_OPENED_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has the DevTools WebIDE been opened?"
+  },
   "DEVTOOLS_CUSTOM_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has a custom developer tool been opened via the toolbox button?"
   },
   "DEVTOOLS_TOOLBOX_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
@@ -5981,16 +5986,21 @@
     "kind": "flag",
     "description": "How many users have opened the devtool's Responsive View been opened via the toolbox button?"
   },
   "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened the devtool's Developer Toolbar been opened via the toolbox button?"
   },
+  "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have opened the DevTools WebIDE?"
+  },
   "DEVTOOLS_CUSTOM_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened a custom developer tool via the toolbox button?"
   },
   "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
@@ -6140,23 +6150,77 @@
   },
   "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the developer toolbar been active (seconds)"
   },
+  "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000000",
+    "n_buckets": 100,
+    "description": "How long has WebIDE been active (seconds)"
+  },
   "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has a custom developer tool been active (seconds)"
   },
+  "DEVTOOLS_WEBIDE_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE runtime connection succeed?"
+  },
+  "DEVTOOLS_WEBIDE_USB_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE USB runtime connection succeed?"
+  },
+  "DEVTOOLS_WEBIDE_WIFI_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE WiFi runtime connection succeed?"
+  },
+  "DEVTOOLS_WEBIDE_SIMULATOR_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE simulator runtime connection succeed?"
+  },
+  "DEVTOOLS_WEBIDE_REMOTE_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE remote runtime connection succeed?"
+  },
+  "DEVTOOLS_WEBIDE_LOCAL_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE local runtime connection succeed?"
+  },
+  "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000000",
+    "n_buckets": 100,
+    "description": "How long was WebIDE connected to a runtime (seconds)?"
+  },
+  "DEVTOOLS_WEBIDE_CONNECTION_PLAY_USED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Was WebIDE's play button used during this runtime connection?"
+  },
+  "DEVTOOLS_WEBIDE_CONNECTION_DEBUG_USED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Was WebIDE's debug button used during this runtime connection?"
+  },
   "BROWSER_IS_USER_DEFAULT": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "The result of the startup default desktop browser check."
   },
   "MIXED_CONTENT_PAGE_LOAD": {
     "expires_in_version": "never",
     "kind": "enumerated",