Bug 1134073 - Part 2: Show network request cause and stacktrace in netmonitor UI r=ochameau a=lizzard l10n=flod
authorJarda Snajdr <jsnajdr@gmail.com>
Thu, 09 Jun 2016 16:17:22 -0500
changeset 341614 7dab350597e637eb8134a58532f7a4597ced328d
parent 341613 6ead63ddad790f6e6ca6d5c976696cbaf4c40194
child 341615 4253296f212b3cca52992ec5f6cd824b13153abd
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau, lizzard
bugs1134073
milestone49.0a2
Bug 1134073 - Part 2: Show network request cause and stacktrace in netmonitor UI r=ochameau a=lizzard l10n=flod MozReview-Commit-ID: HvsVEbHGvv1
devtools/client/locales/en-US/netmonitor.dtd
devtools/client/netmonitor/netmonitor-controller.js
devtools/client/netmonitor/netmonitor-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/panel.js
devtools/client/themes/netmonitor.css
--- a/devtools/client/locales/en-US/netmonitor.dtd
+++ b/devtools/client/locales/en-US/netmonitor.dtd
@@ -34,16 +34,20 @@
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.file): This is the label displayed
   -  in the network table toolbar, above the "file" column. -->
 <!ENTITY netmonitorUI.toolbar.file        "File">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.domain): This is the label displayed
   -  in the network table toolbar, above the "domain" column. -->
 <!ENTITY netmonitorUI.toolbar.domain      "Domain">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.cause): This is the label displayed
+  -  in the network table toolbar, above the "cause" column. -->
+<!ENTITY netmonitorUI.toolbar.cause        "Cause">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.type): This is the label displayed
   -  in the network table toolbar, above the "type" column. -->
 <!ENTITY netmonitorUI.toolbar.type        "Type">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.transferred): This is the label displayed
   -  in the network table toolbar, above the "transferred" column, which is the
   -  compressed / encoded size. -->
 <!ENTITY netmonitorUI.toolbar.transferred "Transferred">
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -428,16 +428,23 @@ var NetMonitorController = {
 
   /**
    * Getter that tells if the server can do network performance statistics.
    * @type boolean
    */
   get supportsPerfStats() {
     return this.tabClient &&
            (this.tabClient.traits.reconfigure || !this._target.isApp);
+  },
+
+  /**
+   * Open a given source in Debugger
+   */
+  viewSourceInDebugger(sourceURL, sourceLine) {
+    return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine);
   }
 };
 
 /**
  * Functions handling target-related lifetime events.
  */
 function TargetEventsHandler() {
   this._onTabNavigated = this._onTabNavigated.bind(this);
@@ -624,22 +631,24 @@ NetworkEventsHandler.prototype = {
    * @param object networkInfo
    *        The network request information.
    */
   _onNetworkEvent: function (type, networkInfo) {
     let { actor,
       startedDateTime,
       request: { method, url },
       isXHR,
+      cause,
       fromCache,
       fromServiceWorker
     } = networkInfo;
 
     NetMonitorView.RequestsMenu.addRequest(
-      actor, startedDateTime, method, url, isXHR, fromCache, fromServiceWorker
+      actor, startedDateTime, method, url, isXHR, cause, fromCache,
+        fromServiceWorker
     );
     window.emit(EVENTS.NETWORK_EVENT, actor);
   },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -25,17 +25,19 @@ const {LocalizationHelper} = require("de
 const {PrefsHelper} = require("devtools/client/shared/prefs");
 const {ViewHelpers, Heritage, WidgetMethods, setNamedTimeout} =
   require("devtools/client/shared/widgets/view-helpers");
 
 /**
  * Localization convenience methods.
  */
 const NET_STRINGS_URI = "chrome://devtools/locale/netmonitor.properties";
+const WEBCONSOLE_STRINGS_URI = "chrome://devtools/locale/webconsole.properties";
 var L10N = new LocalizationHelper(NET_STRINGS_URI);
+const WEBCONSOLE_L10N = new LocalizationHelper(WEBCONSOLE_STRINGS_URI);
 
 // ms
 const WDA_DEFAULT_VERIFY_INTERVAL = 50;
 
 // Use longer timeout during testing as the tests need this process to succeed
 // and two seconds is quite short on slow debug builds. The timeout here should
 // be at least equal to the general mochitest timeout of 45 seconds so that this
 // never gets hit during testing.
@@ -56,16 +58,18 @@ const HTML_NS = "http://www.w3.org/1999/
 const EPSILON = 0.001;
 // 100 KB in bytes
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
 // ms
 const RESIZE_REFRESH_RATE = 50;
 // ms
 const REQUESTS_REFRESH_RATE = 50;
 const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft";
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
 // px
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
 // px
 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
 // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
 // px
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
@@ -97,16 +101,41 @@ const CONTENT_MIME_TYPE_MAPPINGS = {
   "/xml": Editor.modes.html,
   "/atom": Editor.modes.html,
   "/soap": Editor.modes.html,
   "/vnd.mpeg.dash.mpd": Editor.modes.html,
   "/rdf": Editor.modes.css,
   "/rss": Editor.modes.css,
   "/css": Editor.modes.css
 };
+const LOAD_CAUSE_STRINGS = {
+  [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+  [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+  [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+  [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+  [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+  [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+  [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+  [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+  [Ci.nsIContentPolicy.TYPE_REFRESH]: "refresh",
+  [Ci.nsIContentPolicy.TYPE_XBL]: "xbl",
+  [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+  [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+  [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+  [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+  [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+  [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+  [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+  [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+  [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+  [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+  [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+  [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+  [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest"
+};
 const DEFAULT_EDITOR_CONFIG = {
   mode: Editor.modes.text,
   readOnly: true,
   lineNumbers: true
 };
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   // ms
@@ -426,16 +455,30 @@ RequestsMenuView.prototype = Heritage.ex
 
     this.widget = new SideMenuWidget($("#requests-menu-contents"));
     this._splitter = $("#network-inspector-view-splitter");
     this._summary = $("#requests-menu-network-summary-button");
     this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
     this.userInputTimer = Cc["@mozilla.org/timer;1"]
       .createInstance(Ci.nsITimer);
 
+    // Create a tooltip for the newly appended network request item.
+    this.tooltip = new Tooltip(document, {
+      closeOnEvents: [{
+        emitter: $("#requests-menu-contents"),
+        event: "scroll",
+        useCapture: true
+      }]
+    });
+    this.tooltip.startTogglingOnHover(this.widget, this._onHover, {
+      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+      interactive: true
+    });
+    this.tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
+
     Prefs.filters.forEach(type => this.filterOn(type));
     this.sortContents(this._byTiming);
 
     this.allowFocusOnRightClick = true;
     this.maintainSelectionVisible = true;
 
     this.widget.addEventListener("select", this._onSelect, false);
     this.widget.addEventListener("swap", this._onSwap, false);
@@ -632,25 +675,30 @@ RequestsMenuView.prototype = Heritage.ex
    *        A string representation of when the request was started, which
    *        can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
    * @param string method
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string url
    *        Specifies the request's url.
    * @param boolean isXHR
    *        True if this request was initiated via XHR.
+   * @param object cause
+   *        Specifies the request's cause. Has the following properties:
+   *        - type: nsContentPolicyType constant
+   *        - loadingDocumentUri: URI of the request origin
+   *        - stacktrace: JS stacktrace of the request
    * @param boolean fromCache
    *        Indicates if the result came from the browser cache
    * @param boolean fromServiceWorker
    *        Indicates if the request has been intercepted by a Service Worker
    */
-  addRequest: function (id, startedDateTime, method, url, isXHR, fromCache,
-    fromServiceWorker) {
-    this._addQueue.push([id, startedDateTime, method, url, isXHR, fromCache,
-      fromServiceWorker]);
+  addRequest: function (id, startedDateTime, method, url, isXHR, cause,
+    fromCache, fromServiceWorker) {
+    this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
+      fromCache, fromServiceWorker]);
 
     // Lazy updating is disabled in some tests.
     if (!this.lazyUpdate) {
       return void this._flushRequests();
     }
 
     this._flushRequestsTask.arm();
     return undefined;
@@ -880,17 +928,18 @@ RequestsMenuView.prototype = Heritage.ex
   /**
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
   cloneSelectedRequest: function () {
     let selected = this.selectedItem.attachment;
 
     // Create the element node for the network request item.
-    let menuView = this._createMenuView(selected.method, selected.url);
+    let menuView = this._createMenuView(selected.method, selected.url,
+      selected.cause);
 
     // Append a network request item to this container.
     let newItem = this.push([menuView], {
       attachment: Object.create(selected, {
         isCustom: { value: true }
       })
     });
 
@@ -1447,29 +1496,16 @@ RequestsMenuView.prototype = Heritage.ex
       } else {
         requestTarget.setAttribute("odd", "");
         requestTarget.removeAttribute("even");
       }
     }
   },
 
   /**
-   * Refreshes the toggling anchor for the specified item's tooltip.
-   *
-   * @param object item
-   *        The network request item in this container.
-   */
-  refreshTooltip: function (item) {
-    let tooltip = item.attachment.tooltip;
-    tooltip.hide();
-    tooltip.startTogglingOnHover(item.target, this._onHover);
-    tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
-  },
-
-  /**
    * Attaches security icon click listener for the given request menu item.
    *
    * @param object item
    *        The network request item to attach the listener to.
    */
   attachSecurityIconClickListener: function ({ target }) {
     let icon = $(".requests-security-state-icon", target);
     icon.addEventListener("click", this._onSecurityIconClick);
@@ -1505,52 +1541,42 @@ RequestsMenuView.prototype = Heritage.ex
     // Prevent displaying any updates received after the target closed.
     if (NetMonitorView._isDestroyed) {
       return;
     }
 
     let widget = NetMonitorView.RequestsMenu.widget;
     let isScrolledToBottom = widget.isScrolledToBottom();
 
-    for (let [id, startedDateTime, method, url, isXHR, fromCache,
+    for (let [id, startedDateTime, method, url, isXHR, cause, fromCache,
       fromServiceWorker] of this._addQueue) {
       // Convert the received date/time string to a unix timestamp.
       let unixTime = Date.parse(startedDateTime);
 
       // Create the element node for the network request item.
-      let menuView = this._createMenuView(method, url);
+      let menuView = this._createMenuView(method, url, cause);
 
       // Remember the first and last event boundaries.
       this._registerFirstRequestStart(unixTime);
       this._registerLastRequestEnd(unixTime);
 
       // Append a network request item to this container.
       let requestItem = this.push([menuView, id], {
         attachment: {
           startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
           startedMillis: unixTime,
           method: method,
           url: url,
           isXHR: isXHR,
+          cause: cause,
           fromCache: fromCache,
           fromServiceWorker: fromServiceWorker
         }
       });
 
-      // Create a tooltip for the newly appended network request item.
-      requestItem.attachment.tooltip = new Tooltip(document, {
-        closeOnEvents: [{
-          emitter: $("#requests-menu-contents"),
-          event: "scroll",
-          useCapture: true
-        }]
-      });
-
-      this.refreshTooltip(requestItem);
-
       if (id == this._preferredItemId) {
         this.selectedItem = requestItem;
       }
 
       window.emit(EVENTS.REQUEST_ADDED, id);
     }
 
     if (isScrolledToBottom && this._addQueue.length) {
@@ -1749,31 +1775,36 @@ RequestsMenuView.prototype = Heritage.ex
 
   /**
    * Customization function for creating an item's UI.
    *
    * @param string method
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string url
    *        Specifies the request's url.
+   * @param object cause
+   *        Specifies the request's cause. Has two properties:
+   *        - type: nsContentPolicyType constant
+   *        - uri: URI of the request origin
    * @return nsIDOMNode
    *         The network request view.
    */
-  _createMenuView: function (method, url) {
+  _createMenuView: function (method, url, cause) {
     let template = $("#requests-menu-item-template");
     let fragment = document.createDocumentFragment();
 
-    this.updateMenuView(template, "method", method);
-    this.updateMenuView(template, "url", url);
-
     // Flatten the DOM by removing one redundant box (the template container).
     for (let node of template.childNodes) {
       fragment.appendChild(node.cloneNode(true));
     }
 
+    this.updateMenuView(fragment, "method", method);
+    this.updateMenuView(fragment, "url", url);
+    this.updateMenuView(fragment, "cause", cause);
+
     return fragment;
   },
 
   /**
    * Get a human-readable string from a number of bytes, with the B, KB, MB, or
    * GB value. Note that the transition between abbreviations is by 1000 rather
    * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
    * more awkward than 0.99 MB"
@@ -1895,16 +1926,30 @@ RequestsMenuView.prototype = Heritage.ex
         codeNode.setAttribute("value", value.status);
         break;
       }
       case "statusText": {
         let node = $(".requests-menu-status", target);
         node.setAttribute("tooltiptext", value);
         break;
       }
+      case "cause": {
+        let labelNode = $(".requests-menu-cause-label", target);
+        let text = LOAD_CAUSE_STRINGS[value.type] || "unknown";
+        labelNode.setAttribute("value", text);
+        if (value.loadingDocumentUri) {
+          labelNode.setAttribute("tooltiptext", value.loadingDocumentUri);
+        }
+
+        let stackNode = $(".requests-menu-cause-stack", target);
+        if (value.stacktrace && value.stacktrace.length > 0) {
+          stackNode.removeAttribute("hidden");
+        }
+        break;
+      }
       case "contentSize": {
         let node = $(".requests-menu-size", target);
 
         let text = this.getFormattedSize(value);
 
         node.setAttribute("value", text);
         node.setAttribute("tooltiptext", text);
         break;
@@ -2225,21 +2270,16 @@ RequestsMenuView.prototype = Heritage.ex
     }
   },
 
   /**
    * The swap listener for this container.
    * Called when two items switch places, when the contents are sorted.
    */
   _onSwap: function ({ detail: [firstItem, secondItem] }) {
-    // Sorting will create new anchor nodes for all the swapped request items
-    // in this container, so it's necessary to refresh the Tooltip instances.
-    this.refreshTooltip(firstItem);
-    this.refreshTooltip(secondItem);
-
     // Reattach click listener to the security icons
     this.attachSecurityIconClickListener(firstItem);
     this.attachSecurityIconClickListener(secondItem);
   },
 
   /**
    * The predicate used when deciding whether a popup should be shown
    * over a request item or not.
@@ -2247,36 +2287,100 @@ RequestsMenuView.prototype = Heritage.ex
    * @param nsIDOMNode target
    *        The element node currently being hovered.
    * @param object tooltip
    *        The current tooltip instance.
    * @return {Promise}
    */
   _onHover: Task.async(function* (target, tooltip) {
     let requestItem = this.getItemForElement(target);
-    if (!requestItem || !requestItem.attachment.responseContent) {
+    if (!requestItem) {
       return false;
     }
 
     let hovered = requestItem.attachment;
-    let { mimeType, text, encoding } = hovered.responseContent.content;
-
-    if (mimeType && mimeType.includes("image/") && (
-      target.classList.contains("requests-menu-icon") ||
-      target.classList.contains("requests-menu-file"))) {
-      let string = yield gNetwork.getString(text);
-      let anchor = $(".requests-menu-icon", requestItem.target);
-      let src = formDataURI(mimeType, encoding, string);
-
-      tooltip.setImageContent(src, {
-        maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM
-      });
-      return anchor;
+    if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) {
+      return this._setTooltipImageContent(tooltip, requestItem);
+    } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) {
+      return this._setTooltipStackTraceContent(tooltip, requestItem);
+    }
+
+    return false;
+  }),
+
+  _setTooltipImageContent: Task.async(function* (tooltip, requestItem) {
+    let { mimeType, text, encoding } = requestItem.attachment.responseContent.content;
+
+    if (!mimeType || !mimeType.includes("image/")) {
+      return false;
+    }
+
+    let string = yield gNetwork.getString(text);
+    let anchor = $(".requests-menu-icon", requestItem.target);
+    let src = formDataURI(mimeType, encoding, string);
+
+    tooltip.setImageContent(src, {
+      maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM
+    });
+
+    return anchor;
+  }),
+
+  _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) {
+    let {stacktrace} = requestItem.attachment.cause;
+
+    if (!stacktrace || stacktrace.length == 0) {
+      return false;
     }
-    return false;
+
+    let doc = tooltip.doc;
+    let el = doc.createElement("vbox");
+    el.className = "requests-menu-stack-trace";
+
+    for (let f of stacktrace) {
+      let { functionName, filename, lineNumber, columnNumber } = f;
+
+      let frameEl = doc.createElement("hbox");
+      frameEl.className = "requests-menu-stack-frame devtools-monospace";
+
+      let funcEl = doc.createElement("label");
+      funcEl.className = "requests-menu-stack-frame-function-name";
+      funcEl.setAttribute("value",
+        functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction"));
+      frameEl.appendChild(funcEl);
+
+      let fileEl = doc.createElement("label");
+      fileEl.className = "requests-menu-stack-frame-file-name";
+      // Parse a stack frame in format "url -> url"
+      let sourceUrl = filename.split(" -> ").pop();
+      fileEl.setAttribute("value", sourceUrl);
+      fileEl.setAttribute("tooltiptext", sourceUrl);
+      fileEl.setAttribute("crop", "start");
+      frameEl.appendChild(fileEl);
+
+      let lineEl = doc.createElement("label");
+      lineEl.className = "requests-menu-stack-frame-line";
+      lineEl.setAttribute("value", `:${lineNumber}:${columnNumber}`);
+      frameEl.appendChild(lineEl);
+
+      frameEl.addEventListener("click", () => {
+        // avoid an ugly visual artefact when the view is switched to debugger and the
+        // tooltip is hidden only after a delay - the tooltip is moved outside the browser
+        // window.
+        tooltip.hide();
+        NetMonitorController.viewSourceInDebugger(filename, lineNumber);
+      }, false);
+
+      el.appendChild(frameEl);
+    }
+
+    tooltip.content = el;
+    tooltip.panel.setAttribute("wide", "");
+
+    return true;
   }),
 
   /**
    * A handler that opens the security tab in the details view if secure or
    * broken security indicator is clicked.
    */
   _onSecurityIconClick: function (e) {
     let state = this.selectedItem.attachment.securityState;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -216,16 +216,27 @@
                 <button id="requests-menu-domain-button"
                         class="requests-menu-header-button requests-menu-security-and-domain"
                         data-key="domain"
                         label="&netmonitorUI.toolbar.domain;"
                         crop="end"
                         flex="1">
                 </button>
               </hbox>
+              <hbox id="requests-menu-cause-header-box"
+                    class="requests-menu-header requests-menu-cause"
+                    align="center">
+                <button id="requests-menu-cause-button"
+                        class="requests-menu-header-button requests-menu-cause"
+                        data-key="cause"
+                        label="&netmonitorUI.toolbar.cause;"
+                        crop="end"
+                        flex="1">
+                </button>
+              </hbox>
               <hbox id="requests-menu-type-header-box"
                     class="requests-menu-header requests-menu-type"
                     align="center">
                 <button id="requests-menu-type-button"
                         class="requests-menu-header-button requests-menu-type"
                         data-key="type"
                         label="&netmonitorUI.toolbar.type;"
                         crop="end"
@@ -318,16 +329,20 @@
               </hbox>
               <hbox class="requests-menu-subitem requests-menu-security-and-domain"
                     align="center">
                 <image class="requests-security-state-icon" />
                 <label class="plain requests-menu-domain"
                        crop="end"
                        flex="1"/>
               </hbox>
+              <hbox class="requests-menu-subitem requests-menu-cause" align="center">
+                <label class="requests-menu-cause-stack" value="JS" hidden="true"/>
+                <label class="plain requests-menu-cause-label" flex="1" crop="end"/>
+              </hbox>
               <label class="plain requests-menu-subitem requests-menu-type"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-transferred"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-size"
                      crop="end"/>
               <hbox class="requests-menu-subitem requests-menu-waterfall"
                     align="center"
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -11,16 +11,17 @@ const { Task } = require("devtools/share
 
 function NetMonitorPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
 
   this._view = this.panelWin.NetMonitorView;
   this._controller = this.panelWin.NetMonitorController;
   this._controller._target = this.target;
+  this._controller._toolbox = this._toolbox;
 
   EventEmitter.decorate(this);
 }
 
 exports.NetMonitorPanel = NetMonitorPanel;
 
 NetMonitorPanel.prototype = {
   /**
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -241,16 +241,35 @@
 
 .requests-menu-type,
 .requests-menu-size {
   max-width: 6em;
   text-align: center;
   width: 8vw;
 }
 
+.requests-menu-cause {
+  max-width: 8em;
+  width: 8vw;
+}
+
+.requests-menu-cause-stack {
+  background-color: var(--theme-body-color-alt);
+  color: var(--theme-body-background);
+  font-size: 8px;
+  font-weight: bold;
+  line-height: 10px;
+  border-radius: 3px;
+  padding: 0 2px;
+  margin: 0;
+  margin-inline-end: 3px;
+  -moz-user-select: none;
+  cursor: pointer;
+}
+
 .requests-menu-transferred {
   max-width: 8em;
   text-align: center;
   width: 8vw;
 }
 
 /* Network requests table: status codes */
 
@@ -671,16 +690,50 @@
   background-color: var(--theme-selection-background-semitransparent);
 }
 
 .requests-menu-filter-button:not(:active)[checked] {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
+/* Requests menu stacktrace tooltip */
+.requests-menu-stack-trace {
+  max-height: 400px;
+  width: 586px;
+  overflow-y: auto;
+}
+
+.requests-menu-stack-frame {
+  color: var(--theme-body-color-alt);
+  cursor: pointer;
+  display: flex;
+}
+
+.requests-menu-stack-frame:hover {
+  background-color: var(--theme-selection-background-semitransparent);
+}
+
+.requests-menu-stack-frame-function-name {
+  color: var(--theme-highlight-blue);
+  cursor: inherit;
+  flex-grow: 1;
+}
+
+.requests-menu-stack-frame-file-name {
+  cursor: inherit;
+  margin-inline-end: 0;
+}
+
+.requests-menu-stack-frame-line {
+  color: var(--theme-highlight-orange);
+  cursor: inherit;
+  margin-inline-start: 0;
+}
+
 /* Performance analysis buttons */
 
 #requests-menu-network-summary-button {
   background: none;
   box-shadow: none;
   border-color: transparent;
   list-style-image: url(images/profiler-stopwatch.svg);
   padding-inline-end: 0;