Bug 1311177 - Implement the devtools.network.getHAR API method; r=rpl,rickychien,jdescottes draft
authorJan Odvarko <odvarko@gmail.com>
Mon, 22 Jan 2018 18:30:46 +0100
changeset 723184 386e0cd1b01635667f344659ae4cbd1f5376037d
parent 723009 5faab9e619901b1513fd4ca137747231be550def
child 746787 5b0d9a97392d607822fe2e8af82ffa4ff4427887
push id96349
push userjodvarko@mozilla.com
push dateMon, 22 Jan 2018 17:31:19 +0000
reviewersrpl, rickychien, jdescottes
bugs1311177
milestone59.0a1
Bug 1311177 - Implement the devtools.network.getHAR API method; r=rpl,rickychien,jdescottes MozReview-Commit-ID: gUtGjbr0FQ
browser/components/extensions/ext-devtools-network.js
browser/components/extensions/schemas/devtools_network.json
browser/components/extensions/test/browser/browser_ext_devtools_network.js
devtools/client/framework/toolbox.js
devtools/client/netmonitor/initializer.js
devtools/client/netmonitor/src/har/har-builder-utils.js
devtools/client/netmonitor/src/har/har-builder.js
devtools/client/netmonitor/src/har/har-exporter.js
devtools/client/netmonitor/src/har/moz.build
--- a/browser/components/extensions/ext-devtools-network.js
+++ b/browser/components/extensions/ext-devtools-network.js
@@ -20,13 +20,17 @@ this.devtools_network = class extends Ex
               target.on("navigate", listener);
             });
             return () => {
               targetPromise.then(target => {
                 target.off("navigate", listener);
               });
             };
           }).api(),
+
+          getHAR: function() {
+            return context.devToolsToolbox.getHARFromNetMonitor();
+          },
         },
       },
     };
   }
 };
--- a/browser/components/extensions/schemas/devtools_network.json
+++ b/browser/components/extensions/schemas/devtools_network.json
@@ -40,17 +40,16 @@
             ]
           }
         ]
       }
     ],
     "functions": [
       {
         "name": "getHAR",
-        "unsupported": true,
         "type": "function",
         "description": "Returns HAR log that contains all known network requests.",
         "async": "callback",
         "parameters": [
           {
             "name": "callback",
             "type": "function",
             "description": "A function that receives the HAR log when the request completes.",
--- a/browser/components/extensions/test/browser/browser_ext_devtools_network.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_network.js
@@ -1,69 +1,102 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const {gDevTools} = require("devtools/client/framework/devtools");
 
+function background() {
+  browser.test.onMessage.addListener(msg => {
+    let code;
+    if (msg === "navigate") {
+      code = "window.wrappedJSObject.location.href = 'http://example.com/';";
+      browser.tabs.executeScript({code});
+    } else if (msg === "reload") {
+      code = "window.wrappedJSObject.location.reload(true);";
+      browser.tabs.executeScript({code});
+    }
+  });
+  browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+    if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
+      browser.test.sendMessage("tabUpdated");
+    }
+  });
+  browser.test.sendMessage("ready");
+}
+
+function devtools_page() {
+  let eventCount = 0;
+  let listener = url => {
+    eventCount++;
+    browser.test.assertEq("http://example.com/", url, "onNavigated received the expected url.");
+    browser.test.sendMessage("onNavigatedFired", eventCount);
+
+    if (eventCount === 2) {
+      eventCount = 0;
+      browser.devtools.network.onNavigated.removeListener(listener);
+    }
+  };
+  browser.devtools.network.onNavigated.addListener(listener);
+
+  let harLogCount = 0;
+  let harListener = async msg => {
+    if (msg !== "getHAR") {
+      return;
+    }
+
+    harLogCount++;
+
+    const harLog = await browser.devtools.network.getHAR();
+    browser.test.sendMessage("getHAR-result", harLog);
+
+    if (harLogCount === 2) {
+      harLogCount = 0;
+      browser.test.onMessage.removeListener(harListener);
+    }
+  };
+  browser.test.onMessage.addListener(harListener);
+}
+
+function waitForRequestAdded(toolbox) {
+  return new Promise(resolve => {
+    let netPanel = toolbox.getPanel("netmonitor");
+    netPanel.panelWin.once("NetMonitor:RequestAdded", () => {
+      resolve();
+    });
+  });
+}
+
+let extData = {
+  background,
+  manifest: {
+    permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
+    devtools_page: "devtools_page.html",
+  },
+  files: {
+    "devtools_page.html": `<!DOCTYPE html>
+      <html>
+        <head>
+          <meta charset="utf-8">
+          <script src="devtools_page.js"></script>
+        </head>
+        <body>
+        </body>
+      </html>`,
+    "devtools_page.js": devtools_page,
+  },
+};
+
+/**
+ * Test for `chrome.devtools.network.onNavigate()` API
+ */
 add_task(async function test_devtools_network_on_navigated() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
-
-  function background() {
-    browser.test.onMessage.addListener(msg => {
-      let code;
-      if (msg === "navigate") {
-        code = "window.wrappedJSObject.location.href = 'http://example.com/';";
-        browser.tabs.executeScript({code});
-      } else if (msg === "reload") {
-        code = "window.wrappedJSObject.location.reload(true);";
-        browser.tabs.executeScript({code});
-      }
-    });
-    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
-      if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
-        browser.test.sendMessage("tabUpdated");
-      }
-    });
-    browser.test.sendMessage("ready");
-  }
-
-  function devtools_page() {
-    let eventCount = 0;
-    let listener = url => {
-      eventCount++;
-      browser.test.assertEq("http://example.com/", url, "onNavigated received the expected url.");
-      if (eventCount === 2) {
-        browser.devtools.network.onNavigated.removeListener(listener);
-      }
-      browser.test.sendMessage("onNavigatedFired", eventCount);
-    };
-    browser.devtools.network.onNavigated.addListener(listener);
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
-      devtools_page: "devtools_page.html",
-    },
-    files: {
-      "devtools_page.html": `<!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="utf-8">
-            <script src="devtools_page.js"></script>
-          </head>
-          <body>
-          </body>
-        </html>`,
-      "devtools_page.js": devtools_page,
-    },
-  });
+  let extension = ExtensionTestUtils.loadExtension(extData);
 
   await extension.startup();
   await extension.awaitMessage("ready");
 
   let target = gDevTools.getTargetForTab(tab);
 
   await gDevTools.showToolbox(target, "webconsole");
   info("Developer toolbox opened.");
@@ -85,8 +118,60 @@ add_task(async function test_devtools_ne
   await gDevTools.closeToolbox(target);
 
   await target.destroy();
 
   await extension.unload();
 
   await BrowserTestUtils.removeTab(tab);
 });
+
+/**
+ * Test for `chrome.devtools.network.getHAR()` API
+ */
+add_task(async function test_devtools_network_get_har() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  let extension = ExtensionTestUtils.loadExtension(extData);
+
+  await extension.startup();
+  await extension.awaitMessage("ready");
+
+  let target = gDevTools.getTargetForTab(tab);
+
+  // Open the Toolbox
+  let toolbox = await gDevTools.showToolbox(target, "webconsole");
+  info("Developer toolbox opened.");
+
+  // Get HAR, it should be empty since the Net panel wasn't selected.
+  const getHAREmptyPromise = extension.awaitMessage("getHAR-result");
+  extension.sendMessage("getHAR");
+  const getHAREmptyResult = await getHAREmptyPromise;
+  is(getHAREmptyResult.log.entries.length, 0, "HAR log should be empty");
+
+  // Select the Net panel.
+  await toolbox.selectTool("netmonitor");
+
+  // Reload the page to collect some HTTP requests.
+  extension.sendMessage("navigate");
+
+  // Wait till the navigation is complete and request
+  // added into the net panel.
+  await Promise.all([
+    extension.awaitMessage("tabUpdated"),
+    extension.awaitMessage("onNavigatedFired"),
+    waitForRequestAdded(toolbox),
+  ]);
+
+  // Get HAR, it should not be empty now.
+  const getHARPromise = extension.awaitMessage("getHAR-result");
+  extension.sendMessage("getHAR");
+  const getHARResult = await getHARPromise;
+  is(getHARResult.log.entries.length, 1, "HAR log should not be empty");
+
+  // Shutdown
+  await gDevTools.closeToolbox(target);
+
+  await target.destroy();
+
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -64,16 +64,18 @@ loader.lazyRequireGetter(this, "ToolboxB
 loader.lazyRequireGetter(this, "SourceMapURLService",
   "devtools/client/framework/source-map-url-service", true);
 loader.lazyRequireGetter(this, "HUDService",
   "devtools/client/webconsole/hudservice", true);
 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.lazyGetter(this, "domNodeConstants", () => {
   return require("devtools/shared/dom-node-constants");
 });
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
@@ -3001,9 +3003,26 @@ Toolbox.prototype = {
 
   /**
    * Opens source in plain "view-source:".
    * @see devtools/client/shared/source-utils.js
    */
   viewSource: function (sourceURL, sourceLine) {
     return viewSource.viewSource(this, sourceURL, sourceLine);
   },
+
+  /**
+   * Returns data (HAR) collected by the Network panel.
+   */
+  getHARFromNetMonitor: 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 file in such case.
+    if (!netPanel) {
+      return Promise.resolve(buildHarLog(Services.appinfo));
+    }
+
+    // Use Netmonitor object to get the current HAR log.
+    return netPanel.panelWin.Netmonitor.getHar();
+  }
 };
--- a/devtools/client/netmonitor/initializer.js
+++ b/devtools/client/netmonitor/initializer.js
@@ -19,35 +19,38 @@ const require = window.windowRequire = B
 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 App = createFactory(require("./src/components/App"));
-const { getDisplayedRequestById } = require("./src/selectors/index");
 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);
 
 // Inject to global window for testing
 window.store = store;
 window.connector = connector;
 
 /**
  * Global Netmonitor object in this panel. This object can be consumed
  * by other panels (e.g. Console is using inspectRequest), by the
- * Launchpad (bootstrap), etc.
+ * Launchpad (bootstrap), WebExtension API (getHAR), etc.
  */
 window.Netmonitor = {
   bootstrap({ toolbox, panel }) {
     this.mount = document.querySelector("#mount");
 
     const connection = {
       tabConnection: {
         tabTarget: toolbox.target,
@@ -73,16 +76,35 @@ window.Netmonitor = {
   },
 
   destroy() {
     unmountComponentAtNode(this.mount);
     return connector.disconnect();
   },
 
   /**
+   * Returns list of requests currently available in the panel.
+   */
+  getHar() {
+    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+    let { getLongString, getTabTarget, requestData } = connector;
+    let { form: { title, url } } = getTabTarget();
+    let state = store.getState();
+
+    let options = {
+      getString: getLongString,
+      items: getSortedRequests(state),
+      requestData,
+      title: title || url,
+    };
+
+    return HarExporter.getHar(options);
+  },
+
+  /**
    * 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) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/har/har-builder-utils.js
@@ -0,0 +1,30 @@
+/* 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";
+
+/**
+ * Currently supported HAR version.
+ */
+const HAR_VERSION = "1.1";
+
+function buildHarLog(appInfo) {
+  return {
+    log: {
+      version: HAR_VERSION,
+      creator: {
+        name: appInfo.name,
+        version: appInfo.version
+      },
+      browser: {
+        name: appInfo.name,
+        version: appInfo.version
+      },
+      pages: [],
+      entries: [],
+    }
+  };
+}
+
+exports.buildHarLog = buildHarLog;
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -8,19 +8,18 @@ const Services = require("Services");
 const appInfo = Services.appinfo;
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const { CurlUtils } = require("devtools/client/shared/curl");
 const {
   getFormDataSections,
   getUrlQuery,
   parseQueryString,
 } = require("../utils/request-utils");
-
+const { buildHarLog } = require("./har-builder-utils");
 const L10N = new LocalizationHelper("devtools/client/locales/har.properties");
-const HAR_VERSION = "1.1";
 
 /**
  * This object is responsible for building HAR file. See HAR spec:
  * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
  * http://www.softwareishard.com/blog/har-12-spec/
  *
  * @param {Object} options configuration object
  *
@@ -50,48 +49,32 @@ HarBuilder.prototype = {
    *
    * @returns {Promise} A promise that resolves to the HAR object when
    * the entire build process is done.
    */
   build: async function () {
     this.promises = [];
 
     // Build basic structure for data.
-    let log = this.buildLog();
+    let log = buildHarLog(appInfo);
 
     // Build entries.
     for (let file of this._options.items) {
-      log.entries.push(await this.buildEntry(log, file));
+      log.log.entries.push(await this.buildEntry(log.log, file));
     }
 
     // Some data needs to be fetched from the backend during the
     // build process, so wait till all is done.
     await Promise.all(this.promises);
 
-    return { log };
+    return log;
   },
 
   // Helpers
 
-  buildLog: function () {
-    return {
-      version: HAR_VERSION,
-      creator: {
-        name: appInfo.name,
-        version: appInfo.version
-      },
-      browser: {
-        name: appInfo.name,
-        version: appInfo.version
-      },
-      pages: [],
-      entries: [],
-    };
-  },
-
   buildPage: function (file) {
     let page = {};
 
     // Page start time is set when the first request is processed
     // (see buildEntry)
     page.startedDateTime = 0;
     page.id = "page_" + this._options.id;
     page.title = this._options.title;
--- a/devtools/client/netmonitor/src/har/har-exporter.js
+++ b/devtools/client/netmonitor/src/har/har-exporter.js
@@ -114,16 +114,26 @@ const HarExporter = {
    */
   copy: function (options) {
     return this.fetchHarData(options).then(jsonString => {
       clipboardHelper.copyString(jsonString);
       return jsonString;
     });
   },
 
+  /**
+   * 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);
+  },
+
   // Helpers
 
   fetchHarData: function (options) {
     // Generate page ID
     options.id = options.id || uid++;
 
     // Set default generic HAR export options.
     options.jsonp = options.jsonp ||
--- a/devtools/client/netmonitor/src/har/moz.build
+++ b/devtools/client/netmonitor/src/har/moz.build
@@ -1,14 +1,15 @@
 # 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/.
 
 DevToolsModules(
     'har-automation.js',
+    'har-builder-utils.js',
     'har-builder.js',
     'har-collector.js',
     'har-exporter.js',
     'har-utils.js',
     'toolbox-overlay.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']