Bug 1418928 - requestCookies and responseCookies should be loaded lazily r?honza draft
authorRicky Chien <ricky060709@gmail.com>
Fri, 24 Nov 2017 14:57:10 +0800
changeset 703060 36d5dfd7397bde80dd400f8c816213b91a08e88d
parent 702994 b2c80e2b88b2a81075814c9e20c30963fa78595d
child 741652 9302dd1b6dffbb1d8d37cd8f2254c46eea56ed99
push id90690
push userbmo:rchien@mozilla.com
push dateFri, 24 Nov 2017 08:46:49 +0000
reviewershonza
bugs1418928
milestone59.0a1
Bug 1418928 - requestCookies and responseCookies should be loaded lazily r?honza MozReview-Commit-ID: bywCQWGqWI
devtools/client/netmonitor/src/components/CookiesPanel.js
devtools/client/netmonitor/src/components/RequestListColumnCookies.js
devtools/client/netmonitor/src/components/RequestListColumnSetCookies.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/RequestListItem.js
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/test/shared-head.js
--- a/devtools/client/netmonitor/src/components/CookiesPanel.js
+++ b/devtools/client/netmonitor/src/components/CookiesPanel.js
@@ -1,15 +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/. */
 
 "use strict";
 
-const { createFactory } = require("devtools/client/shared/vendor/react");
+const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { L10N } = require("../utils/l10n");
 const { sortObjectKeys } = require("../utils/sort-utils");
 
 // Component
 const PropertiesView = createFactory(require("./PropertiesView"));
 
@@ -23,79 +23,112 @@ const SECTION_NAMES = [
   RESPONSE_COOKIES,
   REQUEST_COOKIES,
 ];
 
 /*
  * Cookies panel component
  * This tab lists full details of any cookies sent with the request or response
  */
-function CookiesPanel({
-  request,
-  openLink,
-}) {
-  let {
-    requestCookies = { cookies: [] },
-    responseCookies = { cookies: [] },
-  } = request;
+class CookiesPanel extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      openLink: PropTypes.func,
+      request: PropTypes.object.isRequired,
+    };
+  }
 
-  requestCookies = requestCookies.cookies || requestCookies;
-  responseCookies = responseCookies.cookies || responseCookies;
+  componentDidMount() {
+    this.maybeFetchRequestCookies(this.props);
+    this.maybeFetchResponseCookies(this.props);
+  }
 
-  if (!requestCookies.length && !responseCookies.length) {
-    return div({ className: "empty-notice" },
-      COOKIES_EMPTY_TEXT
-    );
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchRequestCookies(this.props);
+    this.maybeFetchResponseCookies(this.props);
   }
 
-  let object = {};
-
-  if (responseCookies.length) {
-    object[RESPONSE_COOKIES] = sortObjectKeys(getProperties(responseCookies));
+  /**
+   * When switching to another request, lazily fetch request cookies
+   * from the backend. The panel will first be empty and then display the content.
+   */
+  maybeFetchRequestCookies(props) {
+    if (props.request.requestCookiesAvailable && !props.request.requestCookies) {
+      props.connector.requestData(props.request.id, "requestCookies");
+    }
   }
 
-  if (requestCookies.length) {
-    object[REQUEST_COOKIES] = sortObjectKeys(getProperties(requestCookies));
+  /**
+   * When switching to another request, lazily fetch response cookies
+   * from the backend. The panel will first be empty and then display the content.
+   */
+  maybeFetchResponseCookies(props) {
+    if (props.request.responseCookiesAvailable && !props.request.responseCookies) {
+      props.connector.requestData(props.request.id, "responseCookies");
+    }
   }
 
-  return (
-    div({ className: "panel-container" },
-      PropertiesView({
-        object,
-        filterPlaceHolder: COOKIES_FILTER_TEXT,
-        sectionNames: SECTION_NAMES,
-        openLink,
-      })
-    )
-  );
-}
-
-CookiesPanel.displayName = "CookiesPanel";
-
-CookiesPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-  openLink: PropTypes.func,
-};
+  /**
+   * Mapping array to dict for TreeView usage.
+   * Since TreeView only support Object(dict) format.
+   *
+   * @param {Object[]} arr - key-value pair array like cookies or params
+   * @returns {Object}
+   */
+  getProperties(arr) {
+    return arr.reduce((map, obj) => {
+      // Generally cookies object contains only name and value properties and can
+      // be rendered as name: value pair.
+      // When there are more properties in cookies object such as extra or path,
+      // We will pass the object to display these extra information
+      if (Object.keys(obj).length > 2) {
+        map[obj.name] = Object.assign({}, obj);
+        delete map[obj.name].name;
+      } else {
+        map[obj.name] = obj.value;
+      }
+      return map;
+    }, {});
+  }
 
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- *
- * @param {Object[]} arr - key-value pair array like cookies or params
- * @returns {Object}
- */
-function getProperties(arr) {
-  return arr.reduce((map, obj) => {
-    // Generally cookies object contains only name and value properties and can
-    // be rendered as name: value pair.
-    // When there are more properties in cookies object such as extra or path,
-    // We will pass the object to display these extra information
-    if (Object.keys(obj).length > 2) {
-      map[obj.name] = Object.assign({}, obj);
-      delete map[obj.name].name;
-    } else {
-      map[obj.name] = obj.value;
+  render() {
+    let {
+      request: {
+        requestCookies = { cookies: [] },
+        responseCookies = { cookies: [] },
+      },
+      openLink,
+    } = this.props;
+
+    requestCookies = requestCookies.cookies || requestCookies;
+    responseCookies = responseCookies.cookies || responseCookies;
+
+    if (!requestCookies.length && !responseCookies.length) {
+      return div({ className: "empty-notice" },
+        COOKIES_EMPTY_TEXT
+      );
     }
-    return map;
-  }, {});
+
+    let object = {};
+
+    if (responseCookies.length) {
+      object[RESPONSE_COOKIES] = sortObjectKeys(this.getProperties(responseCookies));
+    }
+
+    if (requestCookies.length) {
+      object[REQUEST_COOKIES] = sortObjectKeys(this.getProperties(requestCookies));
+    }
+
+    return (
+      div({ className: "panel-container" },
+        PropertiesView({
+          object,
+          filterPlaceHolder: COOKIES_FILTER_TEXT,
+          sectionNames: SECTION_NAMES,
+          openLink,
+        })
+      )
+    );
+  }
 }
 
 module.exports = CookiesPanel;
--- a/devtools/client/netmonitor/src/components/RequestListColumnCookies.js
+++ b/devtools/client/netmonitor/src/components/RequestListColumnCookies.js
@@ -8,28 +8,47 @@ const { Component } = require("devtools/
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const { div } = dom;
 
 class RequestListColumnCookies extends Component {
   static get propTypes() {
     return {
+      connector: PropTypes.object.isRequired,
       item: PropTypes.object.isRequired,
     };
   }
 
+  componentDidMount() {
+    this.maybeFetchRequestCookies(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchRequestCookies(nextProps);
+  }
+
   shouldComponentUpdate(nextProps) {
     let { requestCookies: currRequestCookies = { cookies: [] } } = this.props.item;
     let { requestCookies: nextRequestCookies = { cookies: [] } } = nextProps.item;
     currRequestCookies = currRequestCookies.cookies || currRequestCookies;
     nextRequestCookies = nextRequestCookies.cookies || nextRequestCookies;
     return currRequestCookies !== nextRequestCookies;
   }
 
+  /**
+   * When switching to another request, lazily fetch request cookies
+   * from the backend. The panel will first be empty and then display the content.
+   */
+  maybeFetchRequestCookies(props) {
+    if (props.item.requestCookiesAvailable && !props.requestCookies) {
+      props.connector.requestData(props.item.id, "requestCookies");
+    }
+  }
+
   render() {
     let { requestCookies = { cookies: [] } } = this.props.item;
     requestCookies = requestCookies.cookies || requestCookies;
     let requestCookiesLength = requestCookies.length > 0 ? requestCookies.length : "";
     return (
       div({
         className: "requests-list-column requests-list-cookies",
         title: requestCookiesLength
--- a/devtools/client/netmonitor/src/components/RequestListColumnSetCookies.js
+++ b/devtools/client/netmonitor/src/components/RequestListColumnSetCookies.js
@@ -8,28 +8,47 @@ const { Component } = require("devtools/
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const { div } = dom;
 
 class RequestListColumnSetCookies extends Component {
   static get propTypes() {
     return {
+      connector: PropTypes.object.isRequired,
       item: PropTypes.object.isRequired,
     };
   }
 
+  componentDidMount() {
+    this.maybeFetchResponseCookies(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchResponseCookies(nextProps);
+  }
+
   shouldComponentUpdate(nextProps) {
     let { responseCookies: currResponseCookies = { cookies: [] } } = this.props.item;
     let { responseCookies: nextResponseCookies = { cookies: [] } } = nextProps.item;
     currResponseCookies = currResponseCookies.cookies || currResponseCookies;
     nextResponseCookies = nextResponseCookies.cookies || nextResponseCookies;
     return currResponseCookies !== nextResponseCookies;
   }
 
+  /**
+   * When switching to another request, lazily fetch response cookies
+   * from the backend. The panel will first be empty and then display the content.
+   */
+  maybeFetchResponseCookies(props) {
+    if (props.item.responseCookiesAvailable && !props.responseCookies) {
+      props.connector.requestData(props.item.id, "responseCookies");
+    }
+  }
+
   render() {
     let { responseCookies = { cookies: [] } } = this.props.item;
     responseCookies = responseCookies.cookies || responseCookies;
     let responseCookiesLength = responseCookies.length > 0 ? responseCookies.length : "";
     return (
       div({
         className: "requests-list-column requests-list-set-cookies",
         title: responseCookiesLength
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -206,16 +206,17 @@ class RequestListContent extends Compone
    * scrolled to bottom, but allow scrolling up with the selection.
    */
   onFocusedNodeChange() {
     this.shouldScrollBottom = false;
   }
 
   render() {
     const {
+      connector,
       columns,
       displayedRequests,
       firstRequestStartedMillis,
       onCauseBadgeMouseDown,
       onItemMouseDown,
       onSecurityIconMouseDown,
       onWaterfallMouseDown,
       scale,
@@ -231,16 +232,17 @@ class RequestListContent extends Compone
             tabIndex: 0,
             onKeyDown: this.onKeyDown,
             style: { "--timings-scale": scale, "--timings-rev-scale": 1 / scale }
           },
             RequestListHeader(),
             displayedRequests.map((item, index) => RequestListItem({
               firstRequestStartedMillis,
               fromCache: item.status === "304" || item.fromCache,
+              connector,
               columns,
               item,
               index,
               isSelected: item.id === (selectedRequest && selectedRequest.id),
               key: item.id,
               onContextMenu: this.onContextMenu,
               onFocusedNodeChange: this.onFocusedNodeChange,
               onMouseDown: () => onItemMouseDown(item.id),
--- a/devtools/client/netmonitor/src/components/RequestListItem.js
+++ b/devtools/client/netmonitor/src/components/RequestListItem.js
@@ -52,31 +52,34 @@ const UPDATED_REQ_ITEM_PROPS = [
   "method",
   "url",
   "remoteAddress",
   "cause",
   "contentSize",
   "transferredSize",
   "startedMillis",
   "totalTime",
+  "requestCookies",
+  "responseCookies",
 ];
 
 const UPDATED_REQ_PROPS = [
   "firstRequestStartedMillis",
   "index",
   "isSelected",
   "waterfallWidth",
 ];
 
 /**
  * Render one row in the request list.
  */
 class RequestListItem extends Component {
   static get propTypes() {
     return {
+      connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
       item: PropTypes.object.isRequired,
       index: PropTypes.number.isRequired,
       isSelected: PropTypes.bool.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onContextMenu: PropTypes.func.isRequired,
@@ -106,16 +109,17 @@ class RequestListItem extends Component 
       if (this.props.onFocusedNodeChange) {
         this.props.onFocusedNodeChange();
       }
     }
   }
 
   render() {
     let {
+      connector,
       columns,
       item,
       index,
       isSelected,
       firstRequestStartedMillis,
       fromCache,
       onContextMenu,
       onMouseDown,
@@ -142,18 +146,18 @@ class RequestListItem extends Component 
         columns.get("file") && RequestListColumnFile({ item }),
         columns.get("protocol") && RequestListColumnProtocol({ item }),
         columns.get("scheme") && RequestListColumnScheme({ item }),
         columns.get("domain") && RequestListColumnDomain({ item,
                                                            onSecurityIconMouseDown }),
         columns.get("remoteip") && RequestListColumnRemoteIP({ item }),
         columns.get("cause") && RequestListColumnCause({ item, onCauseBadgeMouseDown }),
         columns.get("type") && RequestListColumnType({ item }),
-        columns.get("cookies") && RequestListColumnCookies({ item }),
-        columns.get("setCookies") && RequestListColumnSetCookies({ item }),
+        columns.get("cookies") && RequestListColumnCookies({ connector, item }),
+        columns.get("setCookies") && RequestListColumnSetCookies({ connector, item }),
         columns.get("transferred") && RequestListColumnTransferredSize({ item }),
         columns.get("contentSize") && RequestListColumnContentSize({ item }),
         columns.get("startTime") &&
           RequestListColumnStartTime({ item, firstRequestStartedMillis }),
         columns.get("endTime") &&
           RequestListColumnEndTime({ item, firstRequestStartedMillis }),
         columns.get("responseTime") &&
           RequestListColumnResponseTime({ item, firstRequestStartedMillis }),
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -64,17 +64,21 @@ function TabboxPanel({
           openLink,
           request,
         }),
       ),
       TabPanel({
         id: PANELS.COOKIES,
         title: COOKIES_TITLE,
       },
-        CookiesPanel({ request, openLink }),
+        CookiesPanel({
+          connector,
+          openLink,
+          request,
+        }),
       ),
       TabPanel({
         id: PANELS.PARAMS,
         title: PARAMS_TITLE,
       },
         ParamsPanel({ connector, openLink, request }),
       ),
       TabPanel({
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -260,26 +260,20 @@ class FirefoxDataProvider {
     }
 
     let { payload } = this.getRequestFromQueue(id);
 
     // The payload is ready when all values in the record are true.
     // Note that we never fetch response header/cookies for request with security issues.
     // Bug 1404917 should simplify this heuristic by making all these field be lazily
     // fetched, only on-demand.
-    return record.requestHeaders &&
-      record.requestCookies &&
-      record.eventTimings &&
+    return record.requestHeaders && record.eventTimings &&
       (
-        (record.responseHeaders && record.responseCookies) ||
-        payload.securityState === "broken" ||
-        (
-          !payload.status &&
-          payload.responseContentAvailable
-        )
+        record.responseHeaders || payload.securityState === "broken" ||
+        (!payload.status && payload.responseContentAvailable)
       );
   }
 
   /**
    * Merge upcoming networkEventUpdate payload into existing one.
    *
    * @param {string} id request id
    * @param {object} payload request data payload
@@ -342,19 +336,17 @@ class FirefoxDataProvider {
         url,
       },
       startedDateTime,
     } = networkInfo;
 
     // Create tracking record for this request.
     this.rdpRequestMap.set(actor, {
       requestHeaders: false,
-      requestCookies: false,
       responseHeaders: false,
-      responseCookies: false,
       eventTimings: false,
     });
 
     this.addRequest(actor, {
       cause,
       fromCache,
       fromServiceWorker,
       isXHR,
@@ -381,27 +373,25 @@ class FirefoxDataProvider {
     // When we pause and resume, we may receive `networkEventUpdate` for a request
     // that started during the pause and we missed its `networkEvent`.
     if (!this.rdpRequestMap.has(actor)) {
       return;
     }
 
     switch (updateType) {
       case "requestHeaders":
-      case "requestCookies":
       case "responseHeaders":
-      case "responseCookies":
         this.requestPayloadData(actor, updateType);
         break;
+      case "requestCookies":
+      case "responseCookies":
       case "requestPostData":
-        this.updateRequest(actor, {
-          // This field helps knowing when/if requestPostData property is available
-          // and can be requested via `requestData`
-          requestPostDataAvailable: true
-        });
+        // This field helps knowing when/if updateType property is available
+        // and can be requested via `requestData`
+        this.updateRequest(actor, { [`${updateType}Available`]: true });
         break;
       case "securityInfo":
         // Be careful, securityState can be undefined, for example for WebSocket requests
         // Also note that service worker don't have security info set.
         this.updateRequest(actor, {
           securityState: networkInfo.securityInfo
         });
         break;
@@ -586,22 +576,22 @@ class FirefoxDataProvider {
     });
   }
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
-  onRequestCookies(response) {
-    return this.updateRequest(response.from, {
+  async onRequestCookies(response) {
+    let payload = await this.updateRequest(response.from, {
       requestCookies: response
-    }).then(() => {
-      emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
     });
+    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) {
@@ -638,22 +628,22 @@ class FirefoxDataProvider {
     });
   }
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
-  onResponseCookies(response) {
-    return this.updateRequest(response.from, {
+  async onResponseCookies(response) {
+    let payload = await this.updateRequest(response.from, {
       responseCookies: response
-    }).then(() => {
-      emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
+    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) {
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -118,20 +118,22 @@ const UPDATE_PROPS = [
   "transferredSize",
   "totalTime",
   "eventTimings",
   "headersSize",
   "customQueryValue",
   "requestHeaders",
   "requestHeadersFromUploadStream",
   "requestCookies",
+  "requestCookiesAvailable",
   "requestPostData",
   "requestPostDataAvailable",
   "responseHeaders",
   "responseCookies",
+  "responseCookiesAvailable",
   "responseContent",
   "responseContentAvailable",
   "formDataSections",
   "stacktrace",
 ];
 
 const PANELS = {
   COOKIES: "cookies",
--- a/devtools/client/netmonitor/test/shared-head.js
+++ b/devtools/client/netmonitor/test/shared-head.js
@@ -17,21 +17,19 @@ async function waitForExistingRequests(m
     for (let request of requests) {
       // Ignore cloned request as we don't lazily fetch data for them
       // and have arbitrary number of field set.
       if (request.id.includes("-clone")) {
         continue;
       }
       // Do same check than FirefoxDataProvider.isRequestPayloadReady,
       // in order to ensure there is no more pending payload requests to be done.
-      if (!request.requestHeaders || !request.requestCookies ||
-          !request.eventTimings ||
-          ((!request.responseHeaders || !request.responseCookies) &&
-            request.securityState != "broken" &&
-            (!request.responseContentAvailable || request.status))) {
+      if (!request.requestHeaders || !request.eventTimings ||
+          (!request.responseHeaders && request.securityState !== "broken" &&
+          (!request.responseContentAvailable || request.status))) {
         return false;
       }
     }
     return true;
   }
   // If there is no request, we are good to go.
   if (getRequests().size == 0) {
     return;