Bug 1311177 - Implement the devtools.network.getHAR API method; r=jdescottes,rickychien,rpl
☠☠ backed out by db4cf9d2c11d ☠ ☠
authorJan Odvarko <odvarko@gmail.com>
Wed, 17 Jan 2018 13:32:42 +0100
changeset 454309 32daec7fd5b6bae0302ff3aa802ac5aca9d84663
parent 454308 8f0ed104ce8e5ed1c55a7a7cfa90a157d9a9f7f5
child 454310 856511eb563acda775c91512367c448d7712ab72
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes, rickychien, rpl
bugs1311177
milestone59.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 1311177 - Implement the devtools.network.getHAR API method; r=jdescottes,rickychien,rpl MozReview-Commit-ID: I9F4tGSwBrt
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,93 @@
 /* -*- 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);
+}
+
+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 +109,54 @@ 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");
+  await extension.awaitMessage("tabUpdated");
+  await extension.awaitMessage("onNavigatedFired");
+
+  // 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']