Bug 1005755 - Stop using Connector as a singleton; r=rickychien draft
authorJan Odvarko <odvarko@gmail.com>
Mon, 16 Oct 2017 14:42:25 +0200
changeset 680823 f1392a9c909464d812e868dc3760e2f0d69fc8ae
parent 680822 69bcf8ba90b96408bee68ddddb74c2b347110831
child 680824 d65b1bee3b9647b3f761728ae342ff275a6a33bb
child 680829 e5bf4ae910ccd0a7d50c83d297a5852373fd5559
push id84645
push userjodvarko@mozilla.com
push dateMon, 16 Oct 2017 12:45:59 +0000
reviewersrickychien
bugs1005755
milestone58.0a1
Bug 1005755 - Stop using Connector as a singleton; r=rickychien MozReview-Commit-ID: tQKFiKDozA
devtools/client/netmonitor/index.html
devtools/client/netmonitor/index.js
devtools/client/netmonitor/src/actions/requests.js
devtools/client/netmonitor/src/actions/ui.js
devtools/client/netmonitor/src/components/app.js
devtools/client/netmonitor/src/components/custom-request-panel.js
devtools/client/netmonitor/src/components/monitor-panel.js
devtools/client/netmonitor/src/components/network-details-panel.js
devtools/client/netmonitor/src/components/request-list-content.js
devtools/client/netmonitor/src/components/request-list-empty-notice.js
devtools/client/netmonitor/src/components/request-list.js
devtools/client/netmonitor/src/components/stack-trace-panel.js
devtools/client/netmonitor/src/components/statistics-panel.js
devtools/client/netmonitor/src/components/status-bar.js
devtools/client/netmonitor/src/components/tabbox-panel.js
devtools/client/netmonitor/src/connector/chrome-connector.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
devtools/client/netmonitor/src/middleware/recording.js
devtools/client/netmonitor/src/request-list-context-menu.js
devtools/client/netmonitor/src/request-list-tooltip.js
devtools/client/netmonitor/src/utils/create-store.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/webconsole.js
--- a/devtools/client/netmonitor/index.html
+++ b/devtools/client/netmonitor/index.html
@@ -9,36 +9,45 @@
     <link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/netmonitor.css"/>
     <script src="chrome://devtools/content/shared/theme-switching.js"></script>
   </head>
   <body class="theme-body" role="application">
     <div id="mount"></div>
     <script>
       "use strict";
 
-      const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
+      const { BrowserLoader } = Components.utils.import(
+        "resource://devtools/client/shared/browser-loader.js", {});
       const require = window.windowRequire = BrowserLoader({
         baseURI: "resource://devtools/client/netmonitor/",
         window,
       }).require;
 
       const EventEmitter = require("devtools/shared/old-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/utils/create-store");
-      const store = configureStore();
-      const actions = bindActionCreators(require("./src/actions/index"), store.dispatch);
-      const { onFirefoxConnect, onDisconnect } = require("./src/connector/index");
+      const App = createFactory(require("./src/components/app"));
+      const { getDisplayedRequestById } = require("./src/selectors/index");
+      const { EVENTS } = require("./src/constants");
 
       // 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);
+
       // Inject to global window for testing
       window.store = store;
+      window.connector = connector;
 
       window.Netmonitor = {
         bootstrap({ toolbox }) {
           this.mount = document.querySelector("#mount");
 
           const connection = {
             tabConnection: {
               tabTarget: toolbox.target,
@@ -48,27 +57,67 @@
 
           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 App = createFactory(require("./src/components/app"));
+          // Render the root Application component.
           const sourceMapService = toolbox.sourceMapURLService;
-          const app = App({ sourceMapService, openLink });
+          const app = App({ connector, openLink, sourceMapService });
           render(Provider({ store }, app), this.mount);
-          return onFirefoxConnect(connection, actions, store.getState);
+
+          // Connect to the Firefox backend by default.
+          return connector.connectFirefox(connection, actions, store.getState);
         },
 
         destroy() {
           unmountComponentAtNode(this.mount);
-          return onDisconnect();
+          return connector.disconnect();
         },
+
+        /**
+         * 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
--- a/devtools/client/netmonitor/index.js
+++ b/devtools/client/netmonitor/index.js
@@ -8,17 +8,16 @@
  * This script is the entry point of devtools-launchpad. Make netmonitor possible
  * to run on standalone browser tab without chrome privilege.
  * See README.md for more information.
  */
 const React = require("react");
 const ReactDOM = require("react-dom");
 const { bindActionCreators } = require("redux");
 const { bootstrap, renderRoot } = require("devtools-launchpad");
-const EventEmitter = require("devtools-modules/src/utils/event-emitter");
 const { Services: { appinfo, pref }} = require("devtools-modules");
 
 // Initialize preferences as early as possible
 pref("devtools.theme", "light");
 pref("devtools.cache.disabled", false);
 pref("devtools.netmonitor.enabled", true);
 pref("devtools.netmonitor.filters", "[\"all\"]");
 pref("devtools.netmonitor.visibleColumns",
@@ -34,25 +33,27 @@ pref("devtools.netmonitor.har.jsonpCallb
 pref("devtools.netmonitor.har.includeResponseBodies", true);
 pref("devtools.netmonitor.har.compress", false);
 pref("devtools.netmonitor.har.forceExport", false);
 pref("devtools.netmonitor.har.pageLoadedTimeout", 1500);
 pref("devtools.netmonitor.har.enableAutoExportToFile", false);
 pref("devtools.netmonitor.persistlog", false);
 pref("devtools.styleeditor.enabled", true);
 
-const { configureStore } = require("./src/utils/create-store");
-
 require("./src/assets/styles/netmonitor.css");
 
+const EventEmitter = require("devtools-modules/src/utils/event-emitter");
 EventEmitter.decorate(window);
+
+const { configureStore } = require("./src/utils/create-store");
 const App = require("./src/components/app");
-const store = configureStore();
+const { Connector } = require("./src/connector/index");
+const connector = new Connector();
+const store = configureStore(connector);
 const actions = bindActionCreators(require("./src/actions"), store.dispatch);
-const { onConnect } = require("./src/connector");
 
 // Inject to global window for testing
 window.store = store;
 
 /**
  * Stylesheet links in devtools xhtml files are using chrome or resource URLs.
  * Rewrite the href attribute to remove the protocol. web-server.js contains redirects
  * to map CSS urls to the proper file. Supports urls using:
@@ -75,11 +76,12 @@ window.addEventListener("DOMContentLoade
     document.documentElement.setAttribute("platform", "win");
   }
 });
 
 bootstrap(React, ReactDOM).then((connection) => {
   if (!connection) {
     return;
   }
-  renderRoot(React, ReactDOM, App, store);
-  onConnect(connection, actions, store.getState);
+
+  renderRoot(React, ReactDOM, App, store, {connector});
+  connector.connect(connection, actions, store.getState);
 });
--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -1,15 +1,14 @@
 /* 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 { sendHTTPRequest } = require("../connector/index");
 const {
   ADD_REQUEST,
   CLEAR_REQUESTS,
   CLONE_SELECTED_REQUEST,
   REMOVE_SELECTED_CUSTOM_REQUEST,
   SEND_CUSTOM_REQUEST,
   TOGGLE_RECORDING,
   UPDATE_REQUEST,
@@ -42,17 +41,17 @@ function cloneSelectedRequest() {
   return {
     type: CLONE_SELECTED_REQUEST
   };
 }
 
 /**
  * Send a new HTTP request using the data in the custom request form.
  */
-function sendCustomRequest() {
+function sendCustomRequest(connector) {
   return (dispatch, getState) => {
     const selected = getSelectedRequest(getState());
 
     if (!selected) {
       return;
     }
 
     // Send a new HTTP request using the data in the custom request form
@@ -63,17 +62,17 @@ function sendCustomRequest() {
     };
     if (selected.requestHeaders) {
       data.headers = selected.requestHeaders.headers;
     }
     if (selected.requestPostData) {
       data.body = selected.requestPostData.postData.text;
     }
 
-    sendHTTPRequest(data, (response) => {
+    connector.sendHTTPRequest(data, (response) => {
       return dispatch({
         type: SEND_CUSTOM_REQUEST,
         id: response.eventActor.actor,
       });
     });
   };
 }
 
--- a/devtools/client/netmonitor/src/actions/ui.js
+++ b/devtools/client/netmonitor/src/actions/ui.js
@@ -10,17 +10,16 @@ const {
   ENABLE_PERSISTENT_LOGS,
   DISABLE_BROWSER_CACHE,
   OPEN_STATISTICS,
   RESET_COLUMNS,
   SELECT_DETAILS_PANEL_TAB,
   TOGGLE_COLUMN,
   WATERFALL_RESIZE,
 } = require("../constants");
-const { triggerActivity } = require("../connector/index");
 
 /**
  * Change network details panel.
  *
  * @param {boolean} open - expected network details panel open state
  */
 function openNetworkDetails(open) {
   return {
@@ -51,21 +50,22 @@ function disableBrowserCache(disabled) {
     type: DISABLE_BROWSER_CACHE,
     disabled,
   };
 }
 
 /**
  * Change performance statistics panel open state.
  *
+ * @param {Object} connector - connector object to the backend
  * @param {boolean} visible - expected performance statistics panel open state
  */
-function openStatistics(open) {
+function openStatistics(connector, open) {
   if (open) {
-    triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+    connector.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
   }
   return {
     type: OPEN_STATISTICS,
     open,
   };
 }
 
 /**
@@ -134,19 +134,19 @@ function togglePersistentLogs() {
 function toggleBrowserCache() {
   return (dispatch, getState) =>
     dispatch(disableBrowserCache(!getState().ui.browserCacheDisabled));
 }
 
 /**
  * Toggle performance statistics panel.
  */
-function toggleStatistics() {
+function toggleStatistics(connector) {
   return (dispatch, getState) =>
-    dispatch(openStatistics(!getState().ui.statisticsOpen));
+    dispatch(openStatistics(connector, !getState().ui.statisticsOpen));
 }
 
 module.exports = {
   openNetworkDetails,
   enablePersistentLogs,
   disableBrowserCache,
   openStatistics,
   resetColumns,
--- a/devtools/client/netmonitor/src/components/app.js
+++ b/devtools/client/netmonitor/src/components/app.js
@@ -12,36 +12,47 @@ const {
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 // Components
 const MonitorPanel = createFactory(require("./monitor-panel"));
 const StatisticsPanel = createFactory(require("./statistics-panel"));
 
 const { div } = DOM;
 
-/*
+/**
  * App component
  * The top level component for representing main panel
  */
 function App({
+  connector,
   openLink,
   sourceMapService,
   statisticsOpen,
 }) {
   return (
     div({ className: "network-monitor" },
-      !statisticsOpen ? MonitorPanel({ openLink, sourceMapService }) : StatisticsPanel()
+      !statisticsOpen ? MonitorPanel({
+        connector,
+        sourceMapService,
+        openLink,
+      }) : StatisticsPanel({
+        connector
+      })
     )
   );
 }
 
 App.displayName = "App";
 
 App.propTypes = {
+  // The backend connector object.
+  connector: PropTypes.object.isRequired,
+  // Callback for opening links in the UI
   openLink: PropTypes.func,
   // Service to enable the source map feature.
   sourceMapService: PropTypes.object,
+  // True if the stats panel is opened.
   statisticsOpen: PropTypes.bool.isRequired,
 };
 
 module.exports = connect(
   (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
 )(App);
--- a/devtools/client/netmonitor/src/components/custom-request-panel.js
+++ b/devtools/client/netmonitor/src/components/custom-request-panel.js
@@ -143,16 +143,17 @@ function CustomRequestPanel({
       ),
     )
   );
 }
 
 CustomRequestPanel.displayName = "CustomRequestPanel";
 
 CustomRequestPanel.propTypes = {
+  connector: PropTypes.object.isRequired,
   removeSelectedCustomRequest: PropTypes.func.isRequired,
   request: PropTypes.object,
   sendCustomRequest: PropTypes.func.isRequired,
   updateRequest: PropTypes.func.isRequired,
 };
 
 /**
  * Parse a text representation of a name[divider]value list with
@@ -244,14 +245,14 @@ function updateCustomRequestFields(evt, 
   if (data) {
     // All updateRequest batch mode should be disabled to make UI editing in sync
     updateRequest(request.id, data, false);
   }
 }
 
 module.exports = connect(
   (state) => ({ request: getSelectedRequest(state) }),
-  (dispatch) => ({
+  (dispatch, props) => ({
     removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
-    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
+    sendCustomRequest: () => dispatch(Actions.sendCustomRequest(props.connector)),
     updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
   })
 )(CustomRequestPanel);
--- a/devtools/client/netmonitor/src/components/monitor-panel.js
+++ b/devtools/client/netmonitor/src/components/monitor-panel.js
@@ -9,41 +9,40 @@ const {
   createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 const Actions = require("../actions/index");
-const { getLongString } = require("../connector/index");
 const { getFormDataSections } = require("../utils/request-utils");
 const { getSelectedRequest } = require("../selectors/index");
 
 // Components
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 const NetworkDetailsPanel = createFactory(require("./network-details-panel"));
 const RequestList = createFactory(require("./request-list"));
 const Toolbar = createFactory(require("./toolbar"));
 const { div } = DOM;
 const MediaQueryList = window.matchMedia("(min-width: 700px)");
 
-/*
+/**
  * Monitor panel component
  * The main panel for displaying various network request information
  */
 const MonitorPanel = createClass({
   displayName: "MonitorPanel",
 
   propTypes: {
+    connector: PropTypes.object.isRequired,
     isEmpty: PropTypes.bool.isRequired,
     networkDetailsOpen: PropTypes.bool.isRequired,
     openNetworkDetails: PropTypes.func.isRequired,
     request: PropTypes.object,
-    // Service to enable the source map feature.
     sourceMapService: PropTypes.object,
     openLink: PropTypes.func,
     updateRequest: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     return {
       isVerticalSpliter: MediaQueryList.matches,
@@ -67,17 +66,17 @@ const MonitorPanel = createClass({
     } = request;
 
     if (!formDataSections && requestHeaders &&
         requestHeadersFromUploadStream && requestPostData) {
       getFormDataSections(
         requestHeaders,
         requestHeadersFromUploadStream,
         requestPostData,
-        getLongString,
+        this.props.connector.getLongString,
       ).then((newFormDataSections) => {
         updateRequest(
           request.id,
           { formDataSections: newFormDataSections },
           true,
         );
       });
     }
@@ -101,16 +100,17 @@ const MonitorPanel = createClass({
   onLayoutChange() {
     this.setState({
       isVerticalSpliter: MediaQueryList.matches,
     });
   },
 
   render() {
     let {
+      connector,
       isEmpty,
       networkDetailsOpen,
       sourceMapService,
       openLink
     } = this.props;
 
     let initialWidth = Services.prefs.getIntPref(
         "devtools.netmonitor.panes-network-details-width");
@@ -121,19 +121,20 @@ const MonitorPanel = createClass({
         Toolbar(),
         SplitBox({
           className: "devtools-responsive-container",
           initialWidth: `${initialWidth}px`,
           initialHeight: `${initialHeight}px`,
           minSize: "50px",
           maxSize: "80%",
           splitterSize: "1px",
-          startPanel: RequestList({ isEmpty }),
+          startPanel: RequestList({ isEmpty, connector }),
           endPanel: networkDetailsOpen && NetworkDetailsPanel({
             ref: "endPanel",
+            connector,
             sourceMapService,
             openLink,
           }),
           endPanelCollapsed: !networkDetailsOpen,
           endPanelControl: true,
           vert: this.state.isVerticalSpliter,
         }),
       )
--- a/devtools/client/netmonitor/src/components/network-details-panel.js
+++ b/devtools/client/netmonitor/src/components/network-details-panel.js
@@ -14,20 +14,21 @@ const Actions = require("../actions/inde
 const { getSelectedRequest } = require("../selectors/index");
 
 // Components
 const CustomRequestPanel = createFactory(require("./custom-request-panel"));
 const TabboxPanel = createFactory(require("./tabbox-panel"));
 
 const { div } = DOM;
 
-/*
+/**
  * Network details panel component
  */
 function NetworkDetailsPanel({
+  connector,
   activeTabId,
   cloneSelectedRequest,
   request,
   selectTab,
   sourceMapService,
   openLink,
 }) {
   if (!request) {
@@ -35,37 +36,39 @@ function NetworkDetailsPanel({
   }
 
   return (
     div({ className: "network-details-panel" },
       !request.isCustom ?
         TabboxPanel({
           activeTabId,
           cloneSelectedRequest,
+          connector,
+          openLink,
           request,
           selectTab,
           sourceMapService,
-          openLink,
         }) :
         CustomRequestPanel({
+          connector,
           request,
         })
     )
   );
 }
 
 NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
 
 NetworkDetailsPanel.propTypes = {
+  connector: PropTypes.object.isRequired,
   activeTabId: PropTypes.string,
   cloneSelectedRequest: PropTypes.func.isRequired,
   open: PropTypes.bool,
   request: PropTypes.object,
   selectTab: PropTypes.func.isRequired,
-  // Service to enable the source map feature.
   sourceMapService: PropTypes.object,
   openLink: PropTypes.func,
 };
 
 module.exports = connect(
   (state) => ({
     activeTabId: state.ui.detailsPanelSelectedTab,
     request: getSelectedRequest(state),
--- a/devtools/client/netmonitor/src/components/request-list-content.js
+++ b/devtools/client/netmonitor/src/components/request-list-content.js
@@ -30,36 +30,39 @@ const REQUESTS_TOOLTIP_TOGGLE_DELAY = 50
 
 /**
  * Renders the actual contents of the request list.
  */
 const RequestListContent = createClass({
   displayName: "RequestListContent",
 
   propTypes: {
+    connector: PropTypes.object.isRequired,
     columns: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     displayedRequests: PropTypes.object.isRequired,
     firstRequestStartedMillis: PropTypes.number.isRequired,
     fromCache: PropTypes.bool,
     onCauseBadgeMouseDown: PropTypes.func.isRequired,
     onItemMouseDown: PropTypes.func.isRequired,
     onSecurityIconMouseDown: PropTypes.func.isRequired,
     onSelectDelta: PropTypes.func.isRequired,
     onThumbnailMouseDown: PropTypes.func.isRequired,
     onWaterfallMouseDown: PropTypes.func.isRequired,
     scale: PropTypes.number,
     selectedRequestId: PropTypes.string,
   },
 
   componentWillMount() {
-    const { dispatch } = this.props;
+    const { dispatch, connector } = this.props;
     this.contextMenu = new RequestListContextMenu({
       cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
+      getTabTarget: connector.getTabTarget,
+      getLongString: connector.getLongString,
+      openStatistics: (open) => dispatch(Actions.openStatistics(connector, open)),
     });
     this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
   },
 
   componentDidMount() {
     // Set the CSS variables for waterfall scaling
     this.setScalingStyles();
 
@@ -151,18 +154,19 @@ const RequestListContent = createClass({
     if (!itemId) {
       return false;
     }
     let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
     if (!requestItem) {
       return false;
     }
 
+    let { connector } = this.props;
     if (requestItem.responseContent && target.closest(".requests-list-icon")) {
-      return setTooltipImageContent(tooltip, itemEl, requestItem);
+      return setTooltipImageContent(connector, tooltip, itemEl, requestItem);
     }
 
     return false;
   },
 
   /**
    * Scroll listener for the requests menu view.
    */
--- a/devtools/client/netmonitor/src/components/request-list-empty-notice.js
+++ b/devtools/client/netmonitor/src/components/request-list-empty-notice.js
@@ -7,17 +7,16 @@
 const {
   createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const Actions = require("../actions/index");
-const { triggerActivity } = require("../connector/index");
 const { ACTIVITY_TYPE } = require("../constants");
 const { L10N } = require("../utils/l10n");
 const { getPerformanceAnalysisURL } = require("../utils/mdn-utils");
 
 // Components
 const MDNLink = createFactory(require("./mdn-link"));
 
 const { button, div, span } = DOM;
@@ -32,16 +31,17 @@ const PERFORMANCE_NOTICE_3 = L10N.getStr
 /**
  * UI displayed when the request list is empty. Contains instructions on reloading
  * the page and on triggering performance analysis of the page.
  */
 const RequestListEmptyNotice = createClass({
   displayName: "RequestListEmptyNotice",
 
   propTypes: {
+    connector: PropTypes.object.isRequired,
     onReloadClick: PropTypes.func.isRequired,
     onPerfClick: PropTypes.func.isRequired,
   },
 
   render() {
     return div(
       {
         className: "request-list-empty-notice",
@@ -70,13 +70,14 @@ const RequestListEmptyNotice = createCla
         MDNLink({ url: getPerformanceAnalysisURL() })
       )
     );
   }
 });
 
 module.exports = connect(
   undefined,
-  dispatch => ({
-    onPerfClick: () => dispatch(Actions.openStatistics(true)),
-    onReloadClick: () => triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
+  (dispatch, props) => ({
+    onPerfClick: () => dispatch(Actions.openStatistics(props.connector, true)),
+    onReloadClick: () => props.connector.triggerActivity(
+      ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
   })
 )(RequestListEmptyNotice);
--- a/devtools/client/netmonitor/src/components/request-list.js
+++ b/devtools/client/netmonitor/src/components/request-list.js
@@ -16,25 +16,29 @@ const RequestListEmptyNotice = createFac
 const RequestListHeader = createFactory(require("./request-list-header"));
 const StatusBar = createFactory(require("./status-bar"));
 
 const { div } = DOM;
 
 /**
  * Request panel component
  */
-function RequestList({ isEmpty }) {
+function RequestList({
+  connector,
+  isEmpty,
+}) {
   return (
     div({ className: "request-list-container" },
       RequestListHeader(),
-      isEmpty ? RequestListEmptyNotice() : RequestListContent(),
-      StatusBar(),
+      isEmpty ? RequestListEmptyNotice({connector}) : RequestListContent({connector}),
+      StatusBar({connector}),
     )
   );
 }
 
 RequestList.displayName = "RequestList";
 
 RequestList.propTypes = {
+  connector: PropTypes.object.isRequired,
   isEmpty: PropTypes.bool.isRequired,
 };
 
 module.exports = RequestList;
--- a/devtools/client/netmonitor/src/components/stack-trace-panel.js
+++ b/devtools/client/netmonitor/src/components/stack-trace-panel.js
@@ -4,44 +4,50 @@
 
 "use strict";
 
 const {
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
-const { viewSourceInDebugger } = require("../connector/index");
 
 const { div } = DOM;
 
 // Components
 const StackTrace = createFactory(require("devtools/client/shared/components/StackTrace"));
 
+/**
+ * This component represents a side panel responsible for
+ * rendering stack-trace info for selected request.
+ */
 function StackTracePanel({
+  connector,
   openLink,
   request,
   sourceMapService,
 }) {
   let { stacktrace } = request.cause;
 
   return (
     div({ className: "panel-container" },
       StackTrace({
         stacktrace,
-        onViewSourceInDebugger: ({ url, line }) => viewSourceInDebugger(url, line),
+        onViewSourceInDebugger: ({ url, line }) => {
+          return connector.viewSourceInDebugger(url, line);
+        },
         sourceMapService,
         openLink,
       }),
     )
   );
 }
 
 StackTracePanel.displayName = "StackTracePanel";
 
 StackTracePanel.propTypes = {
+  connector: PropTypes.object.isRequired,
   request: PropTypes.object.isRequired,
-  // Service to enable the source map feature.
   sourceMapService: PropTypes.object,
   openLink: PropTypes.func,
 };
 
 module.exports = StackTracePanel;
--- a/devtools/client/netmonitor/src/components/statistics-panel.js
+++ b/devtools/client/netmonitor/src/components/statistics-panel.js
@@ -39,16 +39,17 @@ const CHARTS_CACHE_DISABLED = L10N.getSt
  * Statistics panel component
  * Performance analysis tool which shows you how long the browser takes to
  * download the different parts of your site.
  */
 const StatisticsPanel = createClass({
   displayName: "StatisticsPanel",
 
   propTypes: {
+    connector: PropTypes.object.isRequired,
     closeStatistics: PropTypes.func.isRequired,
     enableRequestFilterTypeOnly: PropTypes.func.isRequired,
     requests: PropTypes.object,
   },
 
   getInitialState() {
     return {
       isVerticalSpliter: MediaQueryList.matches,
@@ -297,14 +298,14 @@ const StatisticsPanel = createClass({
     );
   }
 });
 
 module.exports = connect(
   (state) => ({
     requests: state.requests.requests.valueSeq(),
   }),
-  (dispatch) => ({
-    closeStatistics: () => dispatch(Actions.openStatistics(false)),
+  (dispatch, props) => ({
+    closeStatistics: () => dispatch(Actions.openStatistics(props.connector, false)),
     enableRequestFilterTypeOnly: (label) =>
       dispatch(Actions.enableRequestFilterTypeOnly(label)),
   })
 )(StatisticsPanel);
--- a/devtools/client/netmonitor/src/components/status-bar.js
+++ b/devtools/client/netmonitor/src/components/status-bar.js
@@ -83,26 +83,27 @@ function StatusBar({ summary, openStatis
         }, `load: ${getFormattedTime(load)}`),
     )
   );
 }
 
 StatusBar.displayName = "StatusBar";
 
 StatusBar.propTypes = {
+  connector: PropTypes.object.isRequired,
   openStatistics: PropTypes.func.isRequired,
   summary: PropTypes.object.isRequired,
   timingMarkers: PropTypes.object.isRequired,
 };
 
 module.exports = connect(
   (state) => ({
     summary: getDisplayedRequestsSummary(state),
     timingMarkers: {
       DOMContentLoaded:
         getDisplayedTimingMarker(state, "firstDocumentDOMContentLoadedTimestamp"),
       load: getDisplayedTimingMarker(state, "firstDocumentLoadTimestamp"),
     },
   }),
-  (dispatch) => ({
-    openStatistics: () => dispatch(Actions.openStatistics(true)),
+  (dispatch, props) => ({
+    openStatistics: () => dispatch(Actions.openStatistics(props.connector, true)),
   }),
 )(StatusBar);
--- a/devtools/client/netmonitor/src/components/tabbox-panel.js
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.js
@@ -26,27 +26,28 @@ const TimingsPanel = createFactory(requi
 const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
 const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
 const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
 const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
 const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
 const STACK_TRACE_TITLE = L10N.getStr("netmonitor.tab.stackTrace");
 const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
 
-/*
+/**
  * Tabbox panel component
  * Display the network request details
  */
 function TabboxPanel({
   activeTabId,
-  cloneSelectedRequest = ()=>{},
+  cloneSelectedRequest = () => {},
+  connector,
+  openLink,
   request,
   selectTab,
   sourceMapService,
-  openLink,
 }) {
   if (!request) {
     return null;
   }
 
   return (
     Tabbar({
       activeTabId,
@@ -85,17 +86,17 @@ function TabboxPanel({
       },
         TimingsPanel({ request }),
       ),
       request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
       TabPanel({
         id: PANELS.STACK_TRACE,
         title: STACK_TRACE_TITLE,
       },
-        StackTracePanel({ request, sourceMapService, openLink }),
+        StackTracePanel({ request, sourceMapService, openLink, connector }),
       ),
       request.securityState && request.securityState !== "insecure" &&
       TabPanel({
         id: PANELS.SECURITY,
         title: SECURITY_TITLE,
       },
         SecurityPanel({ request, openLink }),
       ),
@@ -103,16 +104,16 @@ function TabboxPanel({
   );
 }
 
 TabboxPanel.displayName = "TabboxPanel";
 
 TabboxPanel.propTypes = {
   activeTabId: PropTypes.string,
   cloneSelectedRequest: PropTypes.func,
+  connector: PropTypes.object.isRequired,
+  openLink: PropTypes.func,
   request: PropTypes.object,
   selectTab: PropTypes.func.isRequired,
-  // Service to enable the source map feature.
   sourceMapService: PropTypes.object,
-  openLink: PropTypes.func,
 };
 
 module.exports = connect()(TabboxPanel);
--- a/devtools/client/netmonitor/src/connector/chrome-connector.js
+++ b/devtools/client/netmonitor/src/connector/chrome-connector.js
@@ -30,16 +30,24 @@ class ChromeConnector {
     this.connector.setup(tabConnection, this.actions);
     this.connector.willNavigate(this.willNavigate);
   }
 
   async disconnect() {
     this.connector.disconnect();
   }
 
+  pause() {
+    this.disconnect();
+  }
+
+  resume() {
+    this.setup();
+  }
+
   /**
    * currently all events are about "navigation" is not support on CDP
    */
   willNavigate() {
     this.actions.batchReset();
     this.actions.clearRequests();
   }
 
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -2,31 +2,29 @@
  * 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 Services = require("Services");
 const { TimelineFront } = require("devtools/shared/fronts/timeline");
 const { ACTIVITY_TYPE, EVENTS } = require("../constants");
-const { getDisplayedRequestById } = require("../selectors/index");
 const FirefoxDataProvider = require("./firefox-data-provider");
 
 class FirefoxConnector {
   constructor() {
     // Public methods
     this.connect = this.connect.bind(this);
     this.disconnect = this.disconnect.bind(this);
     this.willNavigate = this.willNavigate.bind(this);
     this.displayCachedEvents = this.displayCachedEvents.bind(this);
     this.onDocLoadingMarker = this.onDocLoadingMarker.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
-    this.inspectRequest = this.inspectRequest.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
 
     // Internals
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
   }
 
@@ -38,22 +36,17 @@ class FirefoxConnector {
 
     this.webConsoleClient = this.tabTarget.activeConsole;
 
     this.dataProvider = new FirefoxDataProvider({
       webConsoleClient: this.webConsoleClient,
       actions: this.actions,
     });
 
-    this.tabTarget.on("will-navigate", this.willNavigate);
-    this.tabTarget.on("close", this.disconnect);
-    this.webConsoleClient.on("networkEvent",
-      this.dataProvider.onNetworkEvent);
-    this.webConsoleClient.on("networkEventUpdate",
-      this.dataProvider.onNetworkEventUpdate);
+    this.addListeners();
 
     // Don't start up waiting for timeline markers if the server isn't
     // recent enough to emit the markers we're interested in.
     if (this.tabTarget.getTrait("documentLoadingMarkers")) {
       this.timelineFront = new TimelineFront(this.tabTarget.client, this.tabTarget.form);
       this.timelineFront.on("doc-loading", this.onDocLoadingMarker);
       await this.timelineFront.start({ withDocLoadingEvents: true });
     }
@@ -66,26 +59,48 @@ class FirefoxConnector {
 
     // The timeline front wasn't initialized and started if the server wasn't
     // recent enough to emit the markers we were interested in.
     if (this.tabTarget.getTrait("documentLoadingMarkers") && this.timelineFront) {
       this.timelineFront.off("doc-loading", this.onDocLoadingMarker);
       await this.timelineFront.destroy();
     }
 
-    this.tabTarget.off("will-navigate");
-    this.tabTarget.off("close");
+    this.removeListeners();
+
     this.tabTarget = null;
-    this.webConsoleClient.off("networkEvent");
-    this.webConsoleClient.off("networkEventUpdate");
     this.webConsoleClient = null;
     this.timelineFront = null;
     this.dataProvider = null;
   }
 
+  pause() {
+    this.removeListeners();
+  }
+
+  resume() {
+    this.addListeners();
+  }
+
+  addListeners() {
+    this.tabTarget.on("will-navigate", this.willNavigate);
+    this.tabTarget.on("close", this.disconnect);
+    this.webConsoleClient.on("networkEvent",
+      this.dataProvider.onNetworkEvent);
+    this.webConsoleClient.on("networkEventUpdate",
+      this.dataProvider.onNetworkEventUpdate);
+  }
+
+  removeListeners() {
+    this.tabTarget.off("will-navigate");
+    this.tabTarget.off("close");
+    this.webConsoleClient.off("networkEvent");
+    this.webConsoleClient.off("networkEventUpdate");
+  }
+
   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();
     }
@@ -210,52 +225,16 @@ class FirefoxConnector {
           performReload: false,
         }).then(standBy);
     }
     this.currentActivity = ACTIVITY_TYPE.NONE;
     return Promise.reject(new Error("Invalid activity type"));
   }
 
   /**
-   * Selects the specified request in the waterfall and opens the details view.
-   *
-   * @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(this.getState(), requestId);
-        if (!request) {
-          // Reset filters so that the request is visible.
-          this.actions.toggleRequestFilterType("all");
-          request = getDisplayedRequestById(this.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);
-          this.actions.selectRequest(request.id);
-          resolve();
-        }
-      };
-
-      inspector();
-
-      if (!request) {
-        window.on(EVENTS.REQUEST_ADDED, inspector);
-      }
-    });
-  }
-
-  /**
    * Fetches the network information packet from actor server
    *
    * @param {string} id request id
    * @return {object} networkInfo data packet
    */
   getNetworkRequest(id) {
     return this.dataProvider.getNetworkRequest(id);
   }
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -1,85 +1,103 @@
 /* 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";
-let connector = {};
+
+/**
+ * Generic connector wrapper object that is responsible for
+ * instantiating specific connector implementation according
+ * to the client type.
+ */
+class Connector {
+  constructor() {
+    this.connector = null;
 
-function onConnect(connection, actions, getState) {
-  if (!connection || !connection.tab) {
-    return;
+    // Bind public API
+    this.connect = this.connect.bind(this);
+    this.disconnect = this.disconnect.bind(this);
+    this.connectChrome = this.connectChrome.bind(this);
+    this.connectFirefox = this.connectFirefox.bind(this);
+    this.getLongString = this.getLongString.bind(this);
+    this.getNetworkRequest = this.getNetworkRequest.bind(this);
+    this.getTabTarget = this.getTabTarget.bind(this);
+    this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
+    this.setPreferences = this.setPreferences.bind(this);
+    this.triggerActivity = this.triggerActivity.bind(this);
+    this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
+  }
+
+  // Connect/Disconnect API
+
+  connect(connection, actions, getState) {
+    if (!connection || !connection.tab) {
+      return;
+    }
+
+    let { clientType } = connection.tab;
+    switch (clientType) {
+      case "chrome":
+        this.connectChrome(connection, actions, getState);
+        break;
+      case "firefox":
+        this.connectFirefox(connection, actions, getState);
+        break;
+      default:
+        throw Error(`Unknown client type - ${clientType}`);
+    }
   }
 
-  let { clientType } = connection.tab;
-  switch (clientType) {
-    case "chrome":
-      onChromeConnect(connection, actions, getState);
-      break;
-    case "firefox":
-      onFirefoxConnect(connection, actions, getState);
-      break;
-    default:
-      throw Error(`Unknown client type - ${clientType}`);
+  disconnect() {
+    this.connector && this.connector.disconnect();
+  }
+
+  connectChrome(connection, actions, getState) {
+    this.connector = require("./chrome-connector");
+    this.connector.connect(connection, actions, getState);
+  }
+
+  connectFirefox(connection, actions, getState) {
+    this.connector = require("./firefox-connector");
+    this.connector.connect(connection, actions, getState);
+  }
+
+  pause() {
+    this.connector.pause();
+  }
+
+  resume() {
+    this.connector.resume();
+  }
+
+  // Public API
+
+  getLongString() {
+    return this.connector.getLongString(...arguments);
+  }
+
+  getNetworkRequest() {
+    return this.connector.getNetworkRequest(...arguments);
+  }
+
+  getTabTarget() {
+    return this.connector.getTabTarget();
+  }
+
+  sendHTTPRequest() {
+    return this.connector.sendHTTPRequest(...arguments);
+  }
+
+  setPreferences() {
+    return this.connector.setPreferences(...arguments);
+  }
+
+  triggerActivity() {
+    return this.connector.triggerActivity(...arguments);
+  }
+
+  viewSourceInDebugger() {
+    return this.connector.viewSourceInDebugger(...arguments);
   }
 }
 
-function onDisconnect() {
-  connector && connector.disconnect();
-}
-
-function onChromeConnect(connection, actions, getState) {
-  connector = require("./chrome-connector");
-  connector.connect(connection, actions, getState);
-}
-
-function onFirefoxConnect(connection, actions, getState) {
-  connector = require("./firefox-connector");
-  connector.connect(connection, actions, getState);
-}
-
-function inspectRequest() {
-  return connector.inspectRequest(...arguments);
-}
-
-function getLongString() {
-  return connector.getLongString(...arguments);
-}
-
-function getNetworkRequest() {
-  return connector.getNetworkRequest(...arguments);
-}
-
-function getTabTarget() {
-  return connector.getTabTarget();
-}
-
-function sendHTTPRequest() {
-  return connector.sendHTTPRequest(...arguments);
-}
-
-function setPreferences() {
-  return connector.setPreferences(...arguments);
-}
-
-function triggerActivity() {
-  return connector.triggerActivity(...arguments);
-}
-
-function viewSourceInDebugger() {
-  return connector.viewSourceInDebugger(...arguments);
-}
-
-module.exports = {
-  onConnect,
-  onChromeConnect,
-  onFirefoxConnect,
-  onDisconnect,
-  getLongString,
-  getNetworkRequest,
-  getTabTarget,
-  inspectRequest,
-  sendHTTPRequest,
-  setPreferences,
-  triggerActivity,
-  viewSourceInDebugger,
-};
+module.exports.Connector = Connector;
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -11,20 +11,19 @@ add_task(function* () {
 });
 
 function* throttleUploadTest(actuallyThrottle) {
   let { tab, monitor } = yield initNetMonitor(
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
 
-  let { store, windowRequire } = monitor.panelWin;
+  let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
-  let { setPreferences } =
-    windowRequire("devtools/client/netmonitor/src/connector/index");
+  let setPreferences = () => connector.setPreferences;
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
 
   store.dispatch(Actions.batchEnable(false));
 
   const size = 4096;
   const uploadSize = actuallyThrottle ? size / 3 : 0;
 
--- a/devtools/client/netmonitor/src/middleware/recording.js
+++ b/devtools/client/netmonitor/src/middleware/recording.js
@@ -3,22 +3,39 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   TOGGLE_RECORDING,
 } = require("../constants");
 
+const {
+  getRecordingState,
+} = require("../selectors/index");
+
 /**
  * Start/stop HTTP traffic recording.
+ *
+ * The UI state of the toolbar toggle button is stored in UI
+ * reducer and the backend connection is managed here in the
+ * middleware.
  */
-function recordingMiddleware(store) {
-  return next => action => {
+function recordingMiddleware(connector) {
+  return store => next => action => {
     const res = next(action);
+
+    // Pause/resume HTTP monitoring according to
+    // the user action.
     if (action.type === TOGGLE_RECORDING) {
-      // TODO connect/disconnect the backend.
+      let recording = getRecordingState(store.getState());
+      if (recording) {
+        connector.resume();
+      } else {
+        connector.pause();
+      }
     }
+
     return res;
   };
 }
 
 module.exports = recordingMiddleware;
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -6,36 +6,36 @@
 
 const Services = require("Services");
 const { Curl } = require("devtools/client/shared/curl");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const { saveAs } = require("devtools/client/shared/file-saver");
 const { copyString } = require("devtools/shared/platform/clipboard");
 const { HarExporter } = require("./har/har-exporter");
 const {
-  getLongString,
-  getTabTarget,
-} = require("./connector/index");
-const {
   getSelectedRequest,
   getSortedRequests,
 } = require("./selectors/index");
 const { L10N } = require("./utils/l10n");
 const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
 const {
   getUrlQuery,
   parseQueryString,
   getUrlBaseName,
 } = require("./utils/request-utils");
 
 function RequestListContextMenu({
   cloneSelectedRequest,
+  getLongString,
+  getTabTarget,
   openStatistics,
 }) {
   this.cloneSelectedRequest = cloneSelectedRequest;
+  this.getLongString = getLongString;
+  this.getTabTarget = getTabTarget;
   this.openStatistics = openStatistics;
 }
 
 RequestListContextMenu.prototype = {
   get selectedRequest() {
     // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
     // Remove window.store
     return getSelectedRequest(window.store.getState());
@@ -234,25 +234,25 @@ RequestListContextMenu.prototype = {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
     win.openUILinkIn(this.selectedRequest.url, "tab", { relatedToCurrent: true });
   },
 
   /**
    * Opens selected item in the debugger
    */
   openInDebugger() {
-    let toolbox = gDevTools.getToolbox(getTabTarget());
+    let toolbox = gDevTools.getToolbox(this.getTabTarget());
     toolbox.viewSourceInDebugger(this.selectedRequest.url, 0);
   },
 
   /**
    * Opens selected item in the style editor
    */
   openInStyleEditor() {
-    let toolbox = gDevTools.getToolbox(getTabTarget());
+    let toolbox = gDevTools.getToolbox(this.getTabTarget());
     toolbox.viewSourceInStyleEditor(this.selectedRequest.url, 0);
   },
 
   /**
    * Copy the request url from the currently selected item.
    */
   copyUrl() {
     copyString(this.selectedRequest.url);
@@ -382,20 +382,20 @@ RequestListContextMenu.prototype = {
     // FIXME: This will not work in launchpad
     // document.execCommand(‘cut’/‘copy’) was denied because it was not called from
     // inside a short running user-generated event handler.
     // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
     return HarExporter.save(this.getDefaultHarOptions());
   },
 
   getDefaultHarOptions() {
-    let form = getTabTarget().form;
+    let form = this.getTabTarget().form;
     let title = form.title || form.url;
 
     return {
-      getString: getLongString,
+      getString: this.getLongString,
       items: this.sortedRequests,
       title: title
     };
   }
 };
 
 module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/src/request-list-tooltip.js
+++ b/devtools/client/netmonitor/src/request-list-tooltip.js
@@ -3,29 +3,28 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   setImageTooltip,
   getImageDimensions,
 } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
-const { getLongString } = require("./connector/index");
 const { formDataURI } = require("./utils/request-utils");
 
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
 
-async function setTooltipImageContent(tooltip, itemEl, requestItem) {
+async function setTooltipImageContent(connector, tooltip, itemEl, requestItem) {
   let { mimeType, text, encoding } = requestItem.responseContent.content;
 
   if (!mimeType || !mimeType.includes("image/")) {
     return false;
   }
 
-  let string = await getLongString(text);
+  let string = await connector.getLongString(text);
   let src = formDataURI(mimeType, encoding, string);
   let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
   let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
   let options = { maxDim, naturalWidth, naturalHeight };
   setImageTooltip(tooltip, tooltip.doc, src, options);
 
   return itemEl.querySelector(".requests-list-icon");
 }
--- a/devtools/client/netmonitor/src/utils/create-store.js
+++ b/devtools/client/netmonitor/src/utils/create-store.js
@@ -1,58 +1,90 @@
 /* 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 Services = require("Services");
 const { applyMiddleware, createStore } = require("devtools/client/shared/vendor/redux");
+
+// Middleware
 const batching = require("../middleware/batching");
 const prefs = require("../middleware/prefs");
 const thunk = require("../middleware/thunk");
+const recording = require("../middleware/recording");
+
+// Reducers
 const rootReducer = require("../reducers/index");
 const { FilterTypes, Filters } = require("../reducers/filters");
 const { Requests } = require("../reducers/requests");
 const { Sort } = require("../reducers/sort");
 const { TimingMarkers } = require("../reducers/timing-markers");
 const { UI, Columns } = require("../reducers/ui");
 
-function configureStore() {
-  const getPref = (pref) => {
-    try {
-      return JSON.parse(Services.prefs.getCharPref(pref));
-    } catch (_) {
-      return [];
-    }
+/**
+ * Configure state and middleware for the Network monitor tool.
+ */
+function configureStore(connector) {
+  // Prepare initial state.
+  const initialState = {
+    filters: new Filters({
+      requestFilterTypes: getFilterState()
+    }),
+    requests: new Requests(),
+    sort: new Sort(),
+    timingMarkers: new TimingMarkers(),
+    ui: new UI({
+      columns: getColumnState()
+    }),
   };
 
-  let activeFilters = {};
-  let filters = getPref("devtools.netmonitor.filters");
-  filters.forEach((filter) => {
-    activeFilters[filter] = true;
-  });
+  // Prepare middleware.
+  let middleware = applyMiddleware(
+    thunk,
+    prefs,
+    batching,
+    recording(connector)
+  );
 
+  return createStore(rootReducer, initialState, middleware);
+}
+
+// Helpers
+
+/**
+ * Get column state from preferences.
+ */
+function getColumnState() {
   let columns = new Columns();
   let visibleColumns = getPref("devtools.netmonitor.visibleColumns");
 
   for (let [col] of columns) {
     columns = columns.withMutations((state) => {
       state.set(col, visibleColumns.includes(col));
     });
   }
 
-  const initialState = {
-    filters: new Filters({
-      requestFilterTypes: new FilterTypes(activeFilters)
-    }),
-    requests: new Requests(),
-    sort: new Sort(),
-    timingMarkers: new TimingMarkers(),
-    ui: new UI({
-      columns,
-    }),
-  };
+  return columns;
+}
 
-  return createStore(rootReducer, initialState, applyMiddleware(thunk, prefs, batching));
+/**
+ * Get filter state from preferences.
+ */
+function getFilterState() {
+  let activeFilters = {};
+  let filters = getPref("devtools.netmonitor.filters");
+  filters.forEach((filter) => {
+    activeFilters[filter] = true;
+  });
+  return new FilterTypes(activeFilters);
+}
+
+function getPref(pref) {
+  try {
+    return JSON.parse(Services.prefs.getCharPref(pref));
+  } catch (_) {
+    return [];
+  }
 }
 
 exports.configureStore = configureStore;
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -134,19 +134,17 @@ NewConsoleOutputWrapper.prototype = {
           frame.line
         ),
         onViewSourceInStyleEditor: frame => this.toolbox.viewSourceInStyleEditor(
           frame.url,
           frame.line
         ),
         openNetworkPanel: (requestId) => {
           return this.toolbox.selectTool("netmonitor").then((panel) => {
-            let { inspectRequest } = panel.panelWin.windowRequire(
-              "devtools/client/netmonitor/src/connector/index");
-            return inspectRequest(requestId);
+            return panel.panelWin.Netmonitor.inspectRequest(requestId);
           });
         },
         sourceMapService: this.toolbox ? this.toolbox.sourceMapURLService : null,
         highlightDomElement: (grip, options = {}) => {
           return this.toolbox.highlighterUtils
             ? this.toolbox.highlighterUtils.highlightDomValueGrip(grip, options)
             : null;
         },
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -1844,24 +1844,21 @@ WebConsoleFrame.prototype = {
    * Opens the network monitor and highlights the specified request.
    *
    * @param string requestId
    *        The actor ID of the network request.
    */
   openNetworkPanel: function (requestId) {
     let toolbox = gDevTools.getToolbox(this.owner.target);
     // The browser console doesn't have a toolbox.
-    if (!toolbox) {
-      return;
+    if (toolbox) {
+      return toolbox.selectTool("netmonitor").then(panel => {
+        return panel.panelWin.Netmonitor.inspectRequest(requestId);
+      });
     }
-    return toolbox.selectTool("netmonitor").then(panel => {
-      let { inspectRequest } = panel.panelWin.windowRequire(
-        "devtools/client/netmonitor/src/connector/index");
-      return inspectRequest(requestId);
-    });
   },
 
   /**
    * Handler for page location changes.
    *
    * @param string uri
    *        New page location.
    * @param string title