Bug 1436665 - Expose Net panel API without the UI; r=ochameau
☠☠ backed out by 00d4c3be380c ☠ ☠
authorJan Odvarko <odvarko@gmail.com>
Fri, 13 Apr 2018 15:49:08 +0200
changeset 414009 4b8fe40857db84a50f456815f36c2a583fc1ccaa
parent 414008 9a93ea4f7c11676cf9541671f4af7bbf15d7c2d1
child 414010 513c72b05382c41171f49f81b0a799a6cfc132ad
push id33857
push userncsoregi@mozilla.com
push dateTue, 17 Apr 2018 21:54:38 +0000
treeherdermozilla-central@1a1223d74b7b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1436665
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 1436665 - Expose Net panel API without the UI; r=ochameau MozReview-Commit-ID: 2Nuk6OZVk4Z
devtools/client/framework/toolbox.js
devtools/client/netmonitor/initializer.js
devtools/client/netmonitor/panel.js
devtools/client/netmonitor/src/api.js
devtools/client/netmonitor/src/app.js
devtools/client/netmonitor/src/connector/chrome-connector.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/har/har-exporter.js
devtools/client/netmonitor/src/moz.build
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -69,16 +69,18 @@ loader.lazyRequireGetter(this, "HUDServi
 loader.lazyRequireGetter(this, "viewSource",
   "devtools/client/shared/view-source");
 loader.lazyRequireGetter(this, "StyleSheetsFront",
   "devtools/shared/fronts/stylesheets", true);
 loader.lazyRequireGetter(this, "buildHarLog",
   "devtools/client/netmonitor/src/har/har-builder-utils", true);
 loader.lazyRequireGetter(this, "getKnownDeviceFront",
   "devtools/shared/fronts/device", true);
+loader.lazyRequireGetter(this, "NetMonitorAPI",
+  "devtools/client/netmonitor/src/api", true);
 
 loader.lazyGetter(this, "domNodeConstants", () => {
   return require("devtools/shared/dom-node-constants");
 });
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
@@ -111,24 +113,22 @@ function Toolbox(target, selectedTool, h
 
   this._toolPanels = new Map();
   this._inspectorExtensionSidebars = new Map();
   this._telemetry = new Telemetry();
 
   this._initInspector = null;
   this._inspector = null;
   this._styleSheets = null;
+  this._netMonitorAPI = null;
 
   // Map of frames (id => frame-info) and currently selected frame id.
   this.frameMap = new Map();
   this.selectedFrameId = null;
 
-  // List of listeners for `devtools.network.onRequestFinished` WebExt API
-  this._requestFinishedListeners = new Set();
-
   this._toolRegistered = this._toolRegistered.bind(this);
   this._toolUnregistered = this._toolUnregistered.bind(this);
   this._onWillNavigate = this._onWillNavigate.bind(this);
   this._refreshHostTitle = this._refreshHostTitle.bind(this);
   this.toggleNoAutohide = this.toggleNoAutohide.bind(this);
   this.showFramesMenu = this.showFramesMenu.bind(this);
   this.handleKeyDownOnFramesButton = this.handleKeyDownOnFramesButton.bind(this);
   this.showFramesMenuOnKeyDown = this.showFramesMenuOnKeyDown.bind(this);
@@ -2682,17 +2682,16 @@ Toolbox.prototype = {
                                   this._applyServiceWorkersTestingSettings);
 
     this._lastFocusedElement = null;
 
     if (this._sourceMapURLService) {
       this._sourceMapURLService.destroy();
       this._sourceMapURLService = null;
     }
-
     if (this._sourceMapService) {
       this._sourceMapService.stopSourceMapWorker();
       this._sourceMapService = null;
     }
 
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
@@ -2781,16 +2780,21 @@ Toolbox.prototype = {
     this._telemetry.destroy();
 
     // Finish all outstanding tasks (which means finish destroying panels and
     // then destroying the host, successfully or not) before destroying the
     // target.
     deferred.resolve(settleAll(outstanding)
         .catch(console.error)
         .then(() => {
+          let api = this._netMonitorAPI;
+          this._netMonitorAPI = null;
+          return api ? api.destroy() : null;
+        }, console.error)
+        .then(() => {
           this._removeHostListeners();
 
           // `location` may already be 'invalid' if the toolbox document is
           // already in process of destruction. Otherwise if it is still
           // around, ensure releasing toolbox document and triggering cleanup
           // thanks to unload event. We do that precisely here, before
           // nullifying the target as various cleanup code depends on the
           // target attribute to be still
@@ -3040,84 +3044,95 @@ Toolbox.prototype = {
    */
   viewSource: function(sourceURL, sourceLine) {
     return viewSource.viewSource(this, sourceURL, sourceLine);
   },
 
   // Support for WebExtensions API (`devtools.network.*`)
 
   /**
+   * Return Netmonitor API object. This object offers Network monitor
+   * public API that can be consumed by other panels or WE API.
+   */
+  getNetMonitorAPI: async function() {
+    let netPanel = this.getPanel("netmonitor");
+
+    // Return Net panel if it exists.
+    if (netPanel) {
+      return netPanel.panelWin.Netmonitor.api;
+    }
+
+    if (this._netMonitorAPI) {
+      return this._netMonitorAPI;
+    }
+
+    // Create and initialize Network monitor API object.
+    // This object is only connected to the backend - not to the UI.
+    this._netMonitorAPI = new NetMonitorAPI();
+    await this._netMonitorAPI.connect(this);
+
+    return this._netMonitorAPI;
+  },
+
+  /**
    * Returns data (HAR) collected by the Network panel.
    */
   getHARFromNetMonitor: async function() {
-    let netPanel = this.getPanel("netmonitor");
-
-    // The panel doesn't have to exist (it must be selected
-    // by the user at least once to be created).
-    // Return default empty HAR log in such case.
-    if (!netPanel) {
-      let har = await buildHarLog(Services.appinfo);
-      return har.log;
-    }
-
-    // Use Netmonitor object to get the current HAR log.
-    let har = await netPanel.panelWin.Netmonitor.getHar();
+    let netMonitor = await this.getNetMonitorAPI();
+    let har = await netMonitor.getHar();
+
+    // Return default empty HAR file if needed.
+    har = har || buildHarLog(Services.appinfo);
 
     // Return the log directly to be compatible with
     // Chrome WebExtension API.
     return har.log;
   },
 
   /**
    * Add listener for `onRequestFinished` events.
    *
    * @param {Object} listener
    *        The listener to be called it's expected to be
    *        a function that takes ({harEntry, requestId})
    *        as first argument.
    */
-  addRequestFinishedListener: function(listener) {
-    // Log console message informing the extension developer
-    // that the Network panel needs to be selected at least
-    // once in order to receive `onRequestFinished` events.
-    let message = "The Network panel needs to be selected at least" +
-      " once in order to receive 'onRequestFinished' events.";
-    this.target.logWarningInPage(message, "har");
-
-    // Add the listener into internal list.
-    this._requestFinishedListeners.add(listener);
+  addRequestFinishedListener: async function(listener) {
+    let netMonitor = await this.getNetMonitorAPI();
+    netMonitor.addRequestFinishedListener(listener);
   },
 
-  removeRequestFinishedListener: function(listener) {
-    this._requestFinishedListeners.delete(listener);
-  },
-
-  getRequestFinishedListeners: function() {
-    return this._requestFinishedListeners;
+  removeRequestFinishedListener: async function(listener) {
+    let netMonitor = await this.getNetMonitorAPI();
+    netMonitor.removeRequestFinishedListener(listener);
+
+    // Destroy Network monitor API object if the following is true:
+    // 1) there is no listener
+    // 2) the Net panel doesn't exist/use the API object (if the panel
+    //    exists it's also responsible for destroying it,
+    //    see `NetMonitorPanel.open` for more details)
+    let netPanel = this.getPanel("netmonitor");
+    let hasListeners = netMonitor.hasRequestFinishedListeners();
+    if (this._netMonitorAPI && !hasListeners && !netPanel) {
+      this._netMonitorAPI.destroy();
+      this._netMonitorAPI = null;
+    }
   },
 
   /**
    * Used to lazily fetch HTTP response content within
    * `onRequestFinished` event listener.
    *
    * @param {String} requestId
    *        Id of the request for which the response content
    *        should be fetched.
    */
-  fetchResponseContent: function(requestId) {
-    let netPanel = this.getPanel("netmonitor");
-
-    // The panel doesn't have to exist (it must be selected
-    // by the user at least once to be created).
-    // Return undefined content in such case.
-    if (!netPanel) {
-      return Promise.resolve({content: {}});
-    }
-
-    return netPanel.panelWin.Netmonitor.fetchResponseContent(requestId);
+  fetchResponseContent: async function(requestId) {
+    let netMonitor = await this.getNetMonitorAPI();
+    return netMonitor.fetchResponseContent(requestId);
   },
 
   // Support management of installed WebExtensions that provide a devtools_page.
 
   /**
    * List the subset of the active WebExtensions which have a devtools_page (used by
    * toolbox-options.js to create the list of the tools provided by the enabled
    * WebExtensions).
--- a/devtools/client/netmonitor/initializer.js
+++ b/devtools/client/netmonitor/initializer.js
@@ -1,205 +1,72 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* exported initialize */
 
 "use strict";
 
 /**
  * This script is the entry point of Network monitor panel.
  * See README.md for more information.
  */
 const { BrowserLoader } = ChromeUtils.import(
   "resource://devtools/client/shared/browser-loader.js", {});
 
 const require = window.windowRequire = BrowserLoader({
   baseURI: "resource://devtools/client/netmonitor/",
   window,
 }).require;
 
+const { NetMonitorAPI } = require("./src/api");
+const { NetMonitorApp } = require("./src/app");
 const EventEmitter = require("devtools/shared/event-emitter");
-const { createFactory } = require("devtools/client/shared/vendor/react");
-const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
-const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
-const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
-const { Connector } = require("./src/connector/index");
-const { configureStore } = require("./src/create-store");
-const App = createFactory(require("./src/components/App"));
-const { EVENTS } = require("./src/constants");
-const {
-  getDisplayedRequestById,
-  getSortedRequests
-} = require("./src/selectors/index");
 
 // Inject EventEmitter into global window.
 EventEmitter.decorate(window);
 
-// Configure store/state object.
-let connector = new Connector();
-const store = configureStore(connector);
-const actions = bindActionCreators(require("./src/actions/index"), store.dispatch);
+/**
+ * This is the initialization point for the Network monitor.
+ *
+ * @param {Object} api Allows reusing existing API object.
+ */
+function initialize(api) {
+  const app = new NetMonitorApp(api);
 
-// Inject to global window for testing
-window.store = store;
-window.connector = connector;
-window.actions = actions;
+  // Inject to global window for testing
+  window.Netmonitor = app;
+  window.store = app.api.store;
+  window.connector = app.api.connector;
+  window.actions = app.api.actions;
+
+  return app;
+}
 
 /**
- * Global Netmonitor object in this panel. This object can be consumed
- * by other panels (e.g. Console is using inspectRequest), by the
- * Launchpad (bootstrap), WebExtension API (getHAR), etc.
+ * The following code is used to open Network monitor in a tab.
+ * Like the Launchpad, but without Launchpad.
+ *
+ * For example:
+ * chrome://devtools/content/netmonitor/index.html?type=process
+ * loads the netmonitor for the parent process, exactly like the
+ * one in the browser toolbox
+ *
+ * It's also possible to connect to a tab.
+ * 1) go in about:debugging
+ * 2) In menu Tabs, click on a Debug button for particular tab
+ *
+ * This  will open an about:devtools-toolbox url, from which you can
+ * take type and id query parameters and reuse them for the chrome url
+ * of the netmonitor
+ *
+ * chrome://devtools/content/netmonitor/index.html?type=tab&id=1234 URLs
+ * where 1234 is the tab id, you can retrieve from about:debugging#tabs links.
+ * Simply copy the id from about:devtools-toolbox?type=tab&id=1234 URLs.
  */
-window.Netmonitor = {
-  bootstrap({ toolbox, panel }) {
-    this.mount = document.querySelector("#mount");
-    this.toolbox = toolbox;
-
-    const connection = {
-      tabConnection: {
-        tabTarget: toolbox.target,
-      },
-      toolbox,
-      panel,
-    };
-
-    const openLink = (link) => {
-      let parentDoc = toolbox.doc;
-      let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
-      let top = iframe.ownerDocument.defaultView.top;
-      top.openWebLinkIn(link, "tab");
-    };
-
-    const openSplitConsole = (err) => {
-      toolbox.openSplitConsole().then(() => {
-        toolbox.target.logErrorInPage(err, "har");
-      });
-    };
-
-    this.onRequestAdded = this.onRequestAdded.bind(this);
-    window.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
-
-    // Render the root Application component.
-    const sourceMapService = toolbox.sourceMapURLService;
-    const app = App({
-      actions,
-      connector,
-      openLink,
-      openSplitConsole,
-      sourceMapService
-    });
-    render(Provider({ store }, app), this.mount);
-
-    // Connect to the Firefox backend by default.
-    return connector.connectFirefox(connection, actions, store.getState);
-  },
-
-  destroy() {
-    unmountComponentAtNode(this.mount);
-    window.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
-    return connector.disconnect();
-  },
-
-  // Support for WebExtensions API
-
-  /**
-   * Support for `devtools.network.getHAR` (get collected data as HAR)
-   */
-  getHar() {
-    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
-    let state = store.getState();
-
-    let options = {
-      connector,
-      items: getSortedRequests(state),
-      // Always generate HAR log even if there are no requests.
-      forceExport: true,
-    };
-
-    return HarExporter.getHar(options);
-  },
-
-  /**
-   * Support for `devtools.network.onRequestFinished`. A hook for
-   * every finished HTTP request used by WebExtensions API.
-   */
-  onRequestAdded(requestId) {
-    let listeners = this.toolbox.getRequestFinishedListeners();
-    if (!listeners.size) {
-      return;
-    }
-
-    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
-    let options = {
-      connector,
-      includeResponseBodies: false,
-      items: [getDisplayedRequestById(store.getState(), requestId)],
-    };
-
-    // Build HAR for specified request only.
-    HarExporter.getHar(options).then(har => {
-      let harEntry = har.log.entries[0];
-      delete harEntry.pageref;
-      listeners.forEach(listener => listener({
-        harEntry,
-        requestId,
-      }));
-    });
-  },
-
-  /**
-   * Support for `Request.getContent` WebExt API (lazy loading response body)
-   */
-  fetchResponseContent(requestId) {
-    return connector.requestData(requestId, "responseContent");
-  },
-
-  /**
-   * Selects the specified request in the waterfall and opens the details view.
-   * This is a firefox toolbox specific API, which providing an ability to inspect
-   * a network request directly from other internal toolbox panel.
-   *
-   * @param {string} requestId The actor ID of the request to inspect.
-   * @return {object} A promise resolved once the task finishes.
-   */
-  inspectRequest(requestId) {
-    // Look for the request in the existing ones or wait for it to appear, if
-    // the network monitor is still loading.
-    return new Promise((resolve) => {
-      let request = null;
-      let inspector = () => {
-        request = getDisplayedRequestById(store.getState(), requestId);
-        if (!request) {
-          // Reset filters so that the request is visible.
-          actions.toggleRequestFilterType("all");
-          request = getDisplayedRequestById(store.getState(), requestId);
-        }
-
-        // If the request was found, select it. Otherwise this function will be
-        // called again once new requests arrive.
-        if (request) {
-          window.off(EVENTS.REQUEST_ADDED, inspector);
-          actions.selectRequest(request.id);
-          resolve();
-        }
-      };
-
-      inspector();
-
-      if (!request) {
-        window.on(EVENTS.REQUEST_ADDED, inspector);
-      }
-    });
-  }
-};
-
-// Implement support for:
-// chrome://devtools/content/netmonitor/index.html?type=tab&id=1234 URLs
-// where 1234 is the tab id, you can retrieve from about:debugging#tabs links.
-// Simply copy the id from about:devtools-toolbox?type=tab&id=1234 URLs.
 
 // URL constructor doesn't support chrome: scheme
 let href = window.location.href.replace(/chrome:/, "http://");
 let url = new window.URL(href);
 
 // If query parameters are given in a chrome tab, the inspector
 // is running in standalone.
 if (window.location.protocol === "chrome:" && url.search.length > 1) {
@@ -217,14 +84,20 @@ if (window.location.protocol === "chrome
       // Create a fake toolbox object
       let toolbox = {
         target,
         viewSourceInDebugger() {
           throw new Error("toolbox.viewSourceInDebugger is not implement from a tab");
         }
       };
 
-      window.Netmonitor.bootstrap({ toolbox });
+      let api = new NetMonitorAPI();
+      await api.connect(toolbox);
+      let app = window.initialize(api);
+      app.bootstrap({
+        toolbox,
+        document: window.document,
+      });
     } catch (err) {
       window.alert("Unable to start the network monitor:" + err);
     }
   })();
 }
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -9,22 +9,32 @@ function NetMonitorPanel(iframeWindow, t
   this.toolbox = toolbox;
 }
 
 NetMonitorPanel.prototype = {
   async open() {
     if (!this.toolbox.target.isRemote) {
       await this.toolbox.target.makeRemote();
     }
-    await this.panelWin.Netmonitor.bootstrap({
+
+    // Reuse an existing Network monitor API object if available.
+    // It could have been created for WE API before Net panel opens.
+    let api = await this.toolbox.getNetMonitorAPI();
+    let app = this.panelWin.initialize(api);
+
+    // Connect the application object to the UI.
+    await app.bootstrap({
       toolbox: this.toolbox,
-      panel: this,
+      document: this.panelWin.document,
     });
+
+    // Ready to go!
     this.emit("ready");
     this.isReady = true;
+
     return this;
   },
 
   async destroy() {
     await this.panelWin.Netmonitor.destroy();
     this.emit("destroyed");
     return this;
   },
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/api.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { Connector } = require("./connector/index");
+const { configureStore } = require("./create-store");
+const { EVENTS } = require("./constants");
+const Actions = require("./actions/index");
+
+const {
+  getDisplayedRequestById,
+  getSortedRequests
+} = require("./selectors/index");
+
+/**
+ * API object for NetMonitor panel (like a facade). This object can be
+ * consumed by other panels, WebExtension API, etc.
+ *
+ * This object doesn't depend on the panel UI and can be created
+ * and used even if the Network panel UI doesn't exist.
+ */
+function NetMonitorAPI() {
+  EventEmitter.decorate(this);
+
+  // Connector to the backend.
+  this.connector = new Connector();
+
+  // Configure store/state object.
+  this.store = configureStore(this.connector);
+
+  // List of listeners for `devtools.network.onRequestFinished` WebExt API
+  this._requestFinishedListeners = new Set();
+
+  // Bind event handlers
+  this.onRequestAdded = this.onRequestAdded.bind(this);
+  this.actions = bindActionCreators(Actions, this.store.dispatch);
+}
+
+NetMonitorAPI.prototype = {
+  async connect(toolbox) {
+    // Bail out if already connected.
+    if (this.toolbox) {
+      return;
+    }
+
+    this.toolbox = toolbox;
+
+    // Register listener for new requests (utilized by WebExtension API).
+    this.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
+
+    // Initialize connection to the backend. Pass `this` as the owner,
+    // so this object can receive all emitted events.
+    const connection = {
+      tabConnection: {
+        tabTarget: toolbox.target,
+      },
+      toolbox,
+      owner: this,
+    };
+
+    await this.connectBackend(this.connector, connection, this.actions,
+      this.store.getState);
+  },
+
+  /**
+   * Clean up (unmount from DOM, remove listeners, disconnect).
+   */
+  async destroy() {
+    this.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
+
+    await this.connector.disconnect();
+
+    if (this.harExportConnector) {
+      await this.harExportConnector.disconnect();
+    }
+  },
+
+  /**
+   * Connect to the Firefox backend by default.
+   *
+   * As soon as connections to different back-ends is supported
+   * this function should be responsible for picking the right API.
+   */
+  async connectBackend(connector, connection, actions, getState) {
+    // The connection might happen during Toolbox initialization
+    // so make sure the target is ready.
+    await connection.tabConnection.tabTarget.makeRemote();
+    return connector.connectFirefox(connection, actions, getState);
+  },
+
+  // HAR
+
+  /**
+   * Support for `devtools.network.getHAR` (get collected data as HAR)
+   */
+  async getHar() {
+    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+    let state = this.store.getState();
+
+    let options = {
+      connector: this.connector,
+      items: getSortedRequests(state),
+    };
+
+    return HarExporter.getHar(options);
+  },
+
+  /**
+   * Support for `devtools.network.onRequestFinished`. A hook for
+   * every finished HTTP request used by WebExtensions API.
+   */
+  async onRequestAdded(requestId) {
+    if (!this._requestFinishedListeners.size) {
+      return;
+    }
+
+    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+
+    let connector = await this.getHarExportConnector();
+    let request = getDisplayedRequestById(this.store.getState(), requestId);
+    if (!request) {
+      console.error("HAR: request not found " + requestId);
+      return;
+    }
+
+    let options = {
+      connector,
+      includeResponseBodies: false,
+      items: [request],
+    };
+
+    let har = await HarExporter.getHar(options);
+
+    // There is page so remove the page reference.
+    let harEntry = har.log.entries[0];
+    delete harEntry.pageref;
+
+    this._requestFinishedListeners.forEach(listener => listener({
+      harEntry,
+      requestId,
+    }));
+  },
+
+  /**
+   * Support for `Request.getContent` WebExt API (lazy loading response body)
+   */
+  async fetchResponseContent(requestId) {
+    return this.connector.requestData(requestId, "responseContent");
+  },
+
+  /**
+   * Add listener for `onRequestFinished` events.
+   *
+   * @param {Object} listener
+   *        The listener to be called it's expected to be
+   *        a function that takes ({harEntry, requestId})
+   *        as first argument.
+   */
+  addRequestFinishedListener: function(listener) {
+    this._requestFinishedListeners.add(listener);
+  },
+
+  removeRequestFinishedListener: function(listener) {
+    this._requestFinishedListeners.delete(listener);
+  },
+
+  hasRequestFinishedListeners: function() {
+    return this._requestFinishedListeners.size > 0;
+  },
+
+  /**
+   * Separate connector for HAR export.
+   */
+  async getHarExportConnector() {
+    if (this.harExportConnector) {
+      return this.harExportConnector;
+    }
+
+    const connection = {
+      tabConnection: {
+        tabTarget: this.toolbox.target,
+      },
+      toolbox: this.toolbox,
+    };
+
+    this.harExportConnector = new Connector();
+    await this.connectBackend(this.harExportConnector, connection);
+    return this.harExportConnector;
+  },
+};
+
+exports.NetMonitorAPI = NetMonitorAPI;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/app.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const App = createFactory(require("./components/App"));
+const { EVENTS } = require("./constants");
+
+const {
+  getDisplayedRequestById,
+} = require("./selectors/index");
+
+/**
+ * Global App object for Network panel. This object depends
+ * on the UI and can't be created independently.
+ *
+ * This object can be consumed by other panels (e.g. Console
+ * is using inspectRequest), by the Launchpad (bootstrap), etc.
+ *
+ * @param {Object} api An existing API object to be reused.
+ */
+function NetMonitorApp(api) {
+  this.api = api;
+}
+
+NetMonitorApp.prototype = {
+  async bootstrap({ toolbox, document }) {
+    // Get the root element for mounting.
+    this.mount = document.querySelector("#mount");
+
+    const openLink = (link) => {
+      let parentDoc = toolbox.doc;
+      let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
+      let top = iframe.ownerDocument.defaultView.top;
+      top.openUILinkIn(link, "tab");
+    };
+
+    const openSplitConsole = (err) => {
+      toolbox.openSplitConsole().then(() => {
+        toolbox.target.logErrorInPage(err, "har");
+      });
+    };
+
+    let {
+      actions,
+      connector,
+      store,
+    } = this.api;
+
+    const sourceMapService = toolbox.sourceMapURLService;
+    const app = App({
+      actions,
+      connector,
+      openLink,
+      openSplitConsole,
+      sourceMapService
+    });
+
+    // Render the root Application component.
+    render(Provider({ store: store }, app), this.mount);
+  },
+
+  /**
+   * Clean up (unmount from DOM, remove listeners, disconnect).
+   */
+  async destroy() {
+    unmountComponentAtNode(this.mount);
+
+    // Make sure to destroy the API object. It's usually destroyed
+    // in the Toolbox destroy method, but we need it here for case
+    // where the Network panel is initialized without the toolbox
+    // and running in a tab (see initialize.js for details).
+    await this.api.destroy();
+  },
+
+  /**
+   * Selects the specified request in the waterfall and opens the details view.
+   * This is a firefox toolbox specific API, which providing an ability to inspect
+   * a network request directly from other internal toolbox panel.
+   *
+   * @param {string} requestId The actor ID of the request to inspect.
+   * @return {object} A promise resolved once the task finishes.
+   */
+  async inspectRequest(requestId) {
+    let {
+      actions,
+      store,
+    } = this.api;
+
+    // Look for the request in the existing ones or wait for it to appear,
+    // if the network monitor is still loading.
+    return new Promise((resolve) => {
+      let request = null;
+      let inspector = () => {
+        request = getDisplayedRequestById(store.getState(), requestId);
+        if (!request) {
+          // Reset filters so that the request is visible.
+          actions.toggleRequestFilterType("all");
+          request = getDisplayedRequestById(store.getState(), requestId);
+        }
+
+        // If the request was found, select it. Otherwise this function will be
+        // called again once new requests arrive.
+        if (request) {
+          this.api.off(EVENTS.REQUEST_ADDED, inspector);
+          actions.selectRequest(request.id);
+          resolve();
+        }
+      };
+
+      inspector();
+
+      if (!request) {
+        this.api.on(EVENTS.REQUEST_ADDED, inspector);
+      }
+    });
+  }
+};
+
+exports.NetMonitorApp = NetMonitorApp;
--- a/devtools/client/netmonitor/src/connector/chrome-connector.js
+++ b/devtools/client/netmonitor/src/connector/chrome-connector.js
@@ -93,9 +93,9 @@ class ChromeConnector {
     // TODO : implement.
   }
 
   viewSourceInDebugger() {
     // TODO : implement.
   }
 }
 
-module.exports = new ChromeConnector();
+module.exports = ChromeConnector;
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -30,56 +30,72 @@ class FirefoxConnector {
     this.requestData = this.requestData.bind(this);
     this.getTimingMarker = this.getTimingMarker.bind(this);
 
     // Internals
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
   }
 
+  /**
+   * Connect to the backend.
+   *
+   * @param {Object} connection object with e.g. reference to the Toolbox.
+   * @param {Object} actions (optional) is used to fire Redux actions to update store.
+   * @param {Object} getState (optional) is used to get access to the state.
+   */
   async connect(connection, actions, getState) {
     this.actions = actions;
     this.getState = getState;
     this.tabTarget = connection.tabConnection.tabTarget;
     this.toolbox = connection.toolbox;
-    this.panel = connection.panel;
+
+    // The owner object (NetMonitorAPI) received all events.
+    this.owner = connection.owner;
 
     this.webConsoleClient = this.tabTarget.activeConsole;
 
     this.dataProvider = new FirefoxDataProvider({
       webConsoleClient: this.webConsoleClient,
       actions: this.actions,
+      owner: this.owner,
     });
 
     await this.addListeners();
 
     // Listener for `will-navigate` event is (un)registered outside
     // of the `addListeners` and `removeListeners` methods since
     // these are used to pause/resume the connector.
     // Paused network panel should be automatically resumed when page
     // reload, so `will-navigate` listener needs to be there all the time.
-    this.tabTarget.on("will-navigate", this.willNavigate);
-    this.tabTarget.on("navigate", this.navigate);
+    if (this.tabTarget) {
+      this.tabTarget.on("will-navigate", this.willNavigate);
+      this.tabTarget.on("navigate", this.navigate);
+    }
 
-    this.displayCachedEvents();
+    // Displaying cache events is only intended for the UI panel.
+    if (this.actions) {
+      this.displayCachedEvents();
+    }
   }
 
   async disconnect() {
-    this.actions.batchReset();
+    if (this.actions) {
+      this.actions.batchReset();
+    }
 
     await this.removeListeners();
 
     if (this.tabTarget) {
       this.tabTarget.off("will-navigate");
       this.tabTarget = null;
     }
 
     this.webConsoleClient = null;
     this.dataProvider = null;
-    this.panel = null;
   }
 
   async pause() {
     await this.removeListeners();
   }
 
   async resume() {
     await this.addListeners();
@@ -127,53 +143,58 @@ class FirefoxConnector {
     }
   }
 
   enableActions(enable) {
     this.dataProvider.enableActions(enable);
   }
 
   willNavigate() {
-    if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
-      this.actions.batchReset();
-      this.actions.clearRequests();
-    } else {
-      // If the log is persistent, just clear all accumulated timing markers.
-      this.actions.clearTimingMarkers();
+    if (this.actions) {
+      if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
+        this.actions.batchReset();
+        this.actions.clearRequests();
+      } else {
+        // If the log is persistent, just clear all accumulated timing markers.
+        this.actions.clearTimingMarkers();
+      }
     }
 
     // Resume is done automatically on page reload/navigation.
-    let state = this.getState();
-    if (!state.requests.recording) {
-      this.actions.toggleRecording();
+    if (this.actions && this.getState) {
+      let state = this.getState();
+      if (!state.requests.recording) {
+        this.actions.toggleRecording();
+      }
     }
   }
 
   navigate() {
     if (this.dataProvider.isPayloadQueueEmpty()) {
       this.onReloaded();
       return;
     }
     let listener = () => {
       if (this.dataProvider && !this.dataProvider.isPayloadQueueEmpty()) {
         return;
       }
-      window.off(EVENTS.PAYLOAD_READY, listener);
+      this.owner.off(EVENTS.PAYLOAD_READY, listener);
       // Netmonitor may already be destroyed,
       // so do not try to notify the listeners
       if (this.dataProvider) {
         this.onReloaded();
       }
     };
-    window.on(EVENTS.PAYLOAD_READY, listener);
+    this.owner.on(EVENTS.PAYLOAD_READY, listener);
   }
 
   onReloaded() {
-    if (this.panel) {
-      this.panel.emit("reloaded");
+    let panel = this.toolbox.getPanel("netmonitor");
+    if (panel) {
+      panel.emit("reloaded");
     }
   }
 
   /**
    * Display any network events already in the cache.
    */
   displayCachedEvents() {
     for (let networkInfo of this.webConsoleClient.getNetworkEvents()) {
@@ -199,30 +220,37 @@ class FirefoxConnector {
   onDocLoadingMarker(marker) {
     // Translate marker into event similar to newer "docEvent" event sent by the console
     // actor
     let event = {
       name: marker.name == "document::DOMContentLoaded" ?
             "dom-interactive" : "dom-complete",
       time: marker.unixTime / 1000
     };
-    this.actions.addTimingMarker(event);
-    window.emit(EVENTS.TIMELINE_EVENT, event);
+
+    if (this.actions) {
+      this.actions.addTimingMarker(event);
+    }
+
+    this.emit(EVENTS.TIMELINE_EVENT, event);
   }
 
   /**
    * The "DOMContentLoaded" and "Load" events sent by the console actor.
    *
    * Only used by FF60+.
    *
    * @param {object} marker
    */
   onDocEvent(event) {
-    this.actions.addTimingMarker(event);
-    window.emit(EVENTS.TIMELINE_EVENT, event);
+    if (this.actions) {
+      this.actions.addTimingMarker(event);
+    }
+
+    this.emit(EVENTS.TIMELINE_EVENT, event);
   }
 
   /**
    * Send a HTTP request data payload
    *
    * @param {object} data data payload would like to sent to backend
    * @param {function} callback callback will be invoked after the request finished
    */
@@ -366,14 +394,32 @@ class FirefoxConnector {
    * @param {object} request network request instance
    * @param {string} type NetworkEventUpdate type
    */
   requestData(request, type) {
     return this.dataProvider.requestData(request, type);
   }
 
   getTimingMarker(name) {
+    if (!this.getState) {
+      return -1;
+    }
+
     let state = this.getState();
     return getDisplayedTimingMarker(state, name);
   }
+
+  /**
+   * Fire events for the owner object.
+   */
+  emit(type, data) {
+    if (this.owner) {
+      this.owner.emit(type, data);
+    }
+
+    // Consumed mainly by tests.
+    if (typeof window != "undefined") {
+      window.emit(type, data);
+    }
+  }
 }
 
-module.exports = new FirefoxConnector();
+module.exports = FirefoxConnector;
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -13,21 +13,29 @@ const { fetchHeaders } = require("../uti
  * This object is responsible for fetching additional HTTP
  * data from the backend over RDP protocol.
  *
  * The object also keeps track of RDP requests in-progress,
  * so it's possible to determine whether all has been fetched
  * or not.
  */
 class FirefoxDataProvider {
-  constructor({webConsoleClient, actions}) {
+  /**
+   * Constructor for data provider
+   *
+   * @param {Object} webConcoleClient represents the client object for Console actor.
+   * @param {Object} actions set of actions fired during data fetching process
+   * @params {Object} owner all events are fired on this object
+   */
+  constructor({webConsoleClient, actions, owner}) {
     // Options
     this.webConsoleClient = webConsoleClient;
-    this.actions = actions;
+    this.actions = actions || {};
     this.actionsEnabled = true;
+    this.owner = owner;
 
     // Internal properties
     this.payloadQueue = new Map();
 
     // Map[key string => Promise] used by `requestData` to prevent requesting the same
     // request data twice.
     this.lazyRequestData = new Map();
 
@@ -79,17 +87,17 @@ class FirefoxDataProvider {
         // FF59+ supports fetching the traces lazily via requestData.
         stacktrace: cause.stacktrace,
 
         fromCache,
         fromServiceWorker,
       }, true);
     }
 
-    emit(EVENTS.REQUEST_ADDED, id);
+    this.emit(EVENTS.REQUEST_ADDED, id);
   }
 
   /**
    * Update a network request if it already exists in application state.
    *
    * @param {string} id request id
    * @param {object} data data payload will be updated to application state
    */
@@ -309,17 +317,17 @@ class FirefoxDataProvider {
       fromCache,
       fromServiceWorker,
       isXHR,
       method,
       startedDateTime,
       url,
     });
 
-    emit(EVENTS.NETWORK_EVENT, actor);
+    this.emit(EVENTS.NETWORK_EVENT, actor);
   }
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param {object} packet the message received from the server.
    * @param {object} networkInfo the network request information.
    */
@@ -336,17 +344,17 @@ class FirefoxDataProvider {
         this.pushRequestToQueue(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
           headersSize: networkInfo.response.headersSize
         });
-        emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
+        this.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         break;
       case "responseContent":
         this.pushRequestToQueue(actor, {
           contentSize: networkInfo.response.bodySize,
           transferredSize: networkInfo.response.transferredSize,
           mimeType: networkInfo.response.content.mimeType,
         });
         break;
@@ -362,17 +370,17 @@ class FirefoxDataProvider {
     }
 
     // This available field helps knowing when/if updateType property is arrived
     // and can be requested via `requestData`
     this.pushRequestToQueue(actor, { [`${updateType}Available`]: true });
 
     this.onPayloadDataReceived(actor);
 
-    emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
+    this.emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
   }
 
   /**
    * Notify actions when messages from onNetworkEventUpdate are done, networkEventUpdate
    * messages contain initial network info for each updateType and then we can invoke
    * requestData to fetch its corresponded data lazily.
    * Once all updateTypes of networkEventUpdate message are arrived, we flush merged
    * request payload from pending queue and then update component.
@@ -388,17 +396,17 @@ class FirefoxDataProvider {
     this.payloadQueue.delete(actor);
 
     if (this.actionsEnabled && this.actions.updateRequest) {
       await this.actions.updateRequest(actor, payload, true);
     }
 
     // This event is fired only once per request, once all the properties are fetched
     // from `onNetworkEventUpdate`. There should be no more RDP requests after this.
-    emit(EVENTS.PAYLOAD_READY, actor);
+    this.emit(EVENTS.PAYLOAD_READY, actor);
   }
 
   /**
    * Public connector API to lazily request HTTP details from the backend.
    *
    * The method focus on:
    * - calling the right actor method,
    * - emitting an event to tell we start fetching some request data,
@@ -458,17 +466,17 @@ class FirefoxDataProvider {
     // Calculate real name of the client getter.
     let clientMethodName = `get${method.charAt(0).toUpperCase()}${method.slice(1)}`;
     // The name of the callback that processes request response
     let callbackMethodName = `on${method.charAt(0).toUpperCase()}${method.slice(1)}`;
     // And the event to fire before updating this data
     let updatingEventName = `UPDATING_${method.replace(/([A-Z])/g, "_$1").toUpperCase()}`;
 
     // Emit event that tell we just start fetching some data
-    emit(EVENTS[updatingEventName], actor);
+    this.emit(EVENTS[updatingEventName], actor);
 
     let response = await new Promise((resolve, reject) => {
       // Do a RDP request to fetch data from the actor.
       if (typeof this.webConsoleClient[clientMethodName] === "function") {
         // Make sure we fetch the real actor data instead of cloned actor
         // e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
         this.webConsoleClient[clientMethodName](actor.replace("-clone", ""), (res) => {
           if (res.error) {
@@ -495,131 +503,136 @@ class FirefoxDataProvider {
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestHeaders(response) {
     let payload = await this.updateRequest(response.from, {
       requestHeaders: response
     });
-    emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+    this.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
     return payload.requestHeaders;
   }
 
   /**
    * Handles additional information received for a "responseHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseHeaders(response) {
     let payload = await this.updateRequest(response.from, {
       responseHeaders: response
     });
-    emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+    this.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
     return payload.responseHeaders;
   }
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestCookies(response) {
     let payload = await this.updateRequest(response.from, {
       requestCookies: response
     });
-    emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
+    this.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
     return payload.requestCookies;
   }
 
   /**
    * Handles additional information received for a "requestPostData" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestPostData(response) {
     let payload = await this.updateRequest(response.from, {
       requestPostData: response
     });
-    emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+    this.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
     return payload.requestPostData;
   }
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onSecurityInfo(response) {
     let payload = await this.updateRequest(response.from, {
       securityInfo: response.securityInfo
     });
-    emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+    this.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
     return payload.securityInfo;
   }
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseCookies(response) {
     let payload = await this.updateRequest(response.from, {
       responseCookies: response
     });
-    emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
+    this.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     return payload.responseCookies;
   }
 
   /**
    * Handles additional information received via "getResponseContent" request.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseContent(response) {
     let payload = await this.updateRequest(response.from, {
       // We have to ensure passing mimeType as fetchResponseContent needs it from
       // updateRequest. It will convert the LongString in `response.content.text` to a
       // string.
       mimeType: response.content.mimeType,
       responseContent: response,
     });
-    emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+    this.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
     return payload.responseContent;
   }
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onEventTimings(response) {
     let payload = await this.updateRequest(response.from, {
       eventTimings: response
     });
-    emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+    this.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     return payload.eventTimings;
   }
 
   /**
    * Handles information received for a "stackTrace" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onStackTrace(response) {
     let payload = await this.updateRequest(response.from, {
       stacktrace: response.stacktrace
     });
-    emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
+    this.emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
     return payload.stacktrace;
   }
-}
 
-/**
- * Guard 'emit' to avoid exception in non-window environment.
- */
-function emit(type, data) {
-  if (typeof window != "undefined") {
-    window.emit(type, data);
+  /**
+   * Fire events for the owner object.
+   */
+  emit(type, data) {
+    if (this.owner) {
+      this.owner.emit(type, data);
+    }
+
+    // Consumed mainly by tests.
+    if (typeof window != "undefined") {
+      window.emit(type, data);
+    }
   }
 }
 
 module.exports = FirefoxDataProvider;
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -49,22 +49,24 @@ class Connector {
     }
   }
 
   disconnect() {
     this.connector && this.connector.disconnect();
   }
 
   connectChrome(connection, actions, getState) {
-    this.connector = require("./chrome-connector");
+    let ChromeConnector = require("./chrome-connector");
+    this.connector = new ChromeConnector();
     return this.connector.connect(connection, actions, getState);
   }
 
   connectFirefox(connection, actions, getState) {
-    this.connector = require("./firefox-connector");
+    let FirefoxConnector = require("./firefox-connector");
+    this.connector = new FirefoxConnector();
     return this.connector.connect(connection, actions, getState);
   }
 
   pause() {
     return this.connector.pause();
   }
 
   resume() {
--- a/devtools/client/netmonitor/src/har/har-exporter.js
+++ b/devtools/client/netmonitor/src/har/har-exporter.js
@@ -121,17 +121,19 @@ const HarExporter = {
 
   /**
    * Get HAR data as JSON object.
    *
    * @param Object options
    *        Configuration object, see save() for detailed description.
    */
   getHar: function(options) {
-    return this.fetchHarData(options).then(JSON.parse);
+    return this.fetchHarData(options).then(data => {
+      return data ? JSON.parse(data) : null;
+    });
   },
 
   // Helpers
 
   fetchHarData: function(options) {
     // Generate page ID
     options.id = options.id || uid++;
 
--- a/devtools/client/netmonitor/src/moz.build
+++ b/devtools/client/netmonitor/src/moz.build
@@ -10,11 +10,13 @@ DIRS += [
     'middleware',
     'reducers',
     'selectors',
     'utils',
     'widgets',
 ]
 
 DevToolsModules(
+    'api.js',
+    'app.js',
     'constants.js',
     'create-store.js',
 )