Bug 1134073 - Part 1: Collect information about request cause and stacktrace in netmonitor backend r=ochameau a=lizzard
authorJarda Snajdr <jsnajdr@gmail.com>
Thu, 09 Jun 2016 16:17:19 -0500
changeset 339600 6ead63ddad790f6e6ca6d5c976696cbaf4c40194
parent 339599 4f016f425e7544b228b4a099e78008b4f3b7c7cd
child 339601 7dab350597e637eb8134a58532f7a4597ced328d
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau, lizzard
bugs1134073
milestone49.0a2
Bug 1134073 - Part 1: Collect information about request cause and stacktrace in netmonitor backend r=ochameau a=lizzard MozReview-Commit-ID: 8JjoLfgOjeq
devtools/server/actors/webconsole.js
devtools/shared/webconsole/client.js
devtools/shared/webconsole/network-monitor.js
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -13,16 +13,17 @@ const { EnvironmentActor } = require("de
 const { ThreadActor } = require("devtools/server/actors/script");
 const { ObjectActor, LongStringActor, createValueGrip, stringIsLong } = require("devtools/server/actors/object");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const ErrorDocs = require("devtools/server/actors/errordocs");
 
 loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
+loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true);
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "ServerLoggingListener", "devtools/shared/webconsole/server-logger", true);
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
     "ConsoleAPIListener", "addWebConsoleCommands",
     "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
@@ -593,30 +594,34 @@ WebConsoleActor.prototype =
             this.consoleAPIListener =
               new ConsoleAPIListener(window, this);
             this.consoleAPIListener.init();
           }
           startedListeners.push(listener);
           break;
         case "NetworkActivity":
           if (!this.networkMonitor) {
+            // Create a StackTraceCollector that's going to be shared both by the
+            // NetworkMonitorChild (getting messages about requests from parent) and
+            // by the NetworkMonitor that directly watches service workers requests.
+            this.stackTraceCollector = new StackTraceCollector({ window, appId });
+            this.stackTraceCollector.init();
+
             if (appId || messageManager) {
               // Start a network monitor in the parent process to listen to
               // most requests than happen in parent
               this.networkMonitor =
                 new NetworkMonitorChild(appId, messageManager,
                                         this.parentActor.actorID, this);
               this.networkMonitor.init();
               // Spawn also one in the child to listen to service workers
-              this.networkMonitorChild = new NetworkMonitor({ window: window },
-                                                            this);
+              this.networkMonitorChild = new NetworkMonitor({ window }, this);
               this.networkMonitorChild.init();
-            }
-            else {
-              this.networkMonitor = new NetworkMonitor({ window: window }, this);
+            } else {
+              this.networkMonitor = new NetworkMonitor({ window }, this);
               this.networkMonitor.init();
             }
           }
           startedListeners.push(listener);
           break;
         case "FileActivity":
           if (this.window instanceof Ci.nsIDOMWindow) {
             if (!this.consoleProgressListener) {
@@ -695,16 +700,20 @@ WebConsoleActor.prototype =
           if (this.networkMonitor) {
             this.networkMonitor.destroy();
             this.networkMonitor = null;
           }
           if (this.networkMonitorChild) {
             this.networkMonitorChild.destroy();
             this.networkMonitorChild = null;
           }
+          if (this.stackTraceCollector) {
+            this.stackTraceCollector.destroy();
+            this.stackTraceCollector = null;
+          }
           stoppedListeners.push(listener);
           break;
         case "FileActivity":
           if (this.consoleProgressListener) {
             this.consoleProgressListener.stopMonitor(this.consoleProgressListener.
                                                      MONITOR_FILE_ACTIVITY);
             this.consoleProgressListener = null;
           }
@@ -1823,16 +1832,17 @@ NetworkEventActor.prototype =
   {
     return {
       actor: this.actorID,
       startedDateTime: this._startedDateTime,
       timeStamp: Date.parse(this._startedDateTime),
       url: this._request.url,
       method: this._request.method,
       isXHR: this._isXHR,
+      cause: this._cause,
       fromCache: this._fromCache,
       fromServiceWorker: this._fromServiceWorker,
       private: this._private,
     };
   },
 
   /**
    * Releases this actor from the pool.
@@ -1868,16 +1878,17 @@ NetworkEventActor.prototype =
    *
    * @param object aNetworkEvent
    *        The network event associated with this actor.
    */
   init: function NEA_init(aNetworkEvent)
   {
     this._startedDateTime = aNetworkEvent.startedDateTime;
     this._isXHR = aNetworkEvent.isXHR;
+    this._cause = aNetworkEvent.cause;
     this._fromCache = aNetworkEvent.fromCache;
     this._fromServiceWorker = aNetworkEvent.fromServiceWorker;
 
     for (let prop of ["method", "url", "httpVersion", "headersSize"]) {
       this._request[prop] = aNetworkEvent[prop];
     }
 
     this._discardRequestBody = aNetworkEvent.discardRequestBody;
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -95,16 +95,17 @@ WebConsoleClient.prototype = {
         discardRequestBody: true,
         discardResponseBody: true,
         startedDateTime: actor.startedDateTime,
         request: {
           url: actor.url,
           method: actor.method,
         },
         isXHR: actor.isXHR,
+        cause: actor.cause,
         response: {},
         timings: {},
         // track the list of network event updates
         updates: [],
         private: actor.private,
         fromCache: actor.fromCache,
         fromServiceWorker: actor.fromServiceWorker
       };
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -1,17 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* 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 {Cc, Ci, Cu, Cr} = require("chrome");
+const {Cc, Ci, Cm, Cu, Cr, components} = require("chrome");
 const Services = require("Services");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyRequireGetter(this, "NetworkHelper",
                          "devtools/shared/webconsole/network-helper");
 loader.lazyRequireGetter(this, "DevToolsUtils",
                          "devtools/shared/DevToolsUtils");
@@ -32,16 +32,246 @@ const HTTP_MOVED_PERMANENTLY = 301;
 const HTTP_FOUND = 302;
 const HTTP_SEE_OTHER = 303;
 const HTTP_TEMPORARY_REDIRECT = 307;
 
 // The maximum number of bytes a NetworkResponseListener can hold: 1 MB
 const RESPONSE_BODY_LIMIT = 1048576;
 
 /**
+ * Check if a given network request should be logged by a network monitor
+ * based on the specified filters.
+ *
+ * @param nsIHttpChannel channel
+ *        Request to check.
+ * @param filters
+ *        NetworkMonitor filters to match against.
+ * @return boolean
+ *         True if the network request should be logged, false otherwise.
+ */
+function matchRequest(channel, filters) {
+  // Log everything if no filter is specified
+  if (!filters.topFrame && !filters.window && !filters.appId) {
+    return true;
+  }
+
+  // Ignore requests from chrome or add-on code when we are monitoring
+  // content.
+  // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
+  // the DevToolsUtils.testing check. We will move to a better way to serve
+  // its needs in bug 1167188, where this check should be removed.
+  if (!DevToolsUtils.testing && channel.loadInfo &&
+      channel.loadInfo.loadingDocument === null &&
+      channel.loadInfo.loadingPrincipal ===
+      Services.scriptSecurityManager.getSystemPrincipal()) {
+    return false;
+  }
+
+  if (filters.window) {
+    // Since frames support, this.window may not be the top level content
+    // frame, so that we can't only compare with win.top.
+    let win = NetworkHelper.getWindowForRequest(channel);
+    while (win) {
+      if (win == filters.window) {
+        return true;
+      }
+      if (win.parent == win) {
+        break;
+      }
+      win = win.parent;
+    }
+  }
+
+  if (filters.topFrame) {
+    let topFrame = NetworkHelper.getTopFrameForRequest(channel);
+    if (topFrame && topFrame === filters.topFrame) {
+      return true;
+    }
+  }
+
+  if (filters.appId) {
+    let appId = NetworkHelper.getAppIdForRequest(channel);
+    if (appId && appId == filters.appId) {
+      return true;
+    }
+  }
+
+  // The following check is necessary because beacon channels don't come
+  // associated with a load group. Bug 1160837 will hopefully introduce a
+  // platform fix that will render the following code entirely useless.
+  if (channel.loadInfo &&
+      channel.loadInfo.externalContentPolicyType ==
+      Ci.nsIContentPolicy.TYPE_BEACON) {
+    let nonE10sMatch = filters.window &&
+        channel.loadInfo.loadingDocument === filters.window.document;
+    const loadingPrincipal = channel.loadInfo.loadingPrincipal;
+    let e10sMatch = filters.topFrame &&
+        filters.topFrame.contentPrincipal &&
+        filters.topFrame.contentPrincipal.equals(loadingPrincipal) &&
+        filters.topFrame.contentPrincipal.URI.spec == channel.referrer.spec;
+    let b2gMatch = filters.appId && loadingPrincipal.appId === filters.appId;
+    if (nonE10sMatch || e10sMatch || b2gMatch) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/**
+ * This is a nsIChannelEventSink implementation that monitors channel redirects and
+ * informs the registered StackTraceCollector about the old and new channels.
+ */
+const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
+const SINK_CLASS_ID = components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}");
+const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
+const SINK_CATEGORY_NAME = "net-channel-event-sinks";
+
+function ChannelEventSink() {
+  this.wrappedJSObject = this;
+  this.collectors = new Set();
+}
+
+ChannelEventSink.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
+
+  registerCollector(collector) {
+    this.collectors.add(collector);
+  },
+
+  unregisterCollector(collector) {
+    this.collectors.delete(collector);
+
+    if (this.collectors.size == 0) {
+      ChannelEventSinkFactory.unregister();
+    }
+  },
+
+  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+    for (let collector of this.collectors) {
+      try {
+        collector.onChannelRedirect(oldChannel, newChannel, flags);
+      } catch (ex) {
+        console.error("StackTraceCollector.onChannelRedirect threw an exception", ex);
+      }
+    }
+    callback.onRedirectVerifyCallback(Cr.NS_OK);
+  }
+};
+
+const ChannelEventSinkFactory = XPCOMUtils.generateSingletonFactory(ChannelEventSink);
+
+ChannelEventSinkFactory.register = function () {
+  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+  if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
+    return;
+  }
+
+  registrar.registerFactory(SINK_CLASS_ID,
+                            SINK_CLASS_DESCRIPTION,
+                            SINK_CONTRACT_ID,
+                            ChannelEventSinkFactory);
+
+  XPCOMUtils.categoryManager.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
+    SINK_CONTRACT_ID, false, true);
+};
+
+ChannelEventSinkFactory.unregister = function () {
+  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+  registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);
+
+  XPCOMUtils.categoryManager.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID,
+    false);
+};
+
+ChannelEventSinkFactory.getService = function () {
+  // Make sure the ChannelEventSink service is registered before accessing it
+  ChannelEventSinkFactory.register();
+
+  return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink).wrappedJSObject;
+};
+
+function StackTraceCollector(filters) {
+  this.filters = filters;
+  this.stacktracesById = new Map();
+}
+
+StackTraceCollector.prototype = {
+  init() {
+    Services.obs.addObserver(this, "http-on-opening-request", false);
+    ChannelEventSinkFactory.getService().registerCollector(this);
+  },
+
+  destroy() {
+    Services.obs.removeObserver(this, "http-on-opening-request");
+    ChannelEventSinkFactory.getService().unregisterCollector(this);
+  },
+
+  _saveStackTrace(channel, stacktrace) {
+    this.stacktracesById.set(channel.channelId, stacktrace);
+  },
+
+  observe(subject) {
+    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+    if (!matchRequest(channel, this.filters)) {
+      return;
+    }
+
+    // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be
+    // passed around through message managers etc.
+    let frame = components.stack;
+    let stacktrace = [];
+    if (frame && frame.caller) {
+      frame = frame.caller;
+      while (frame) {
+        stacktrace.push({
+          filename: frame.filename,
+          lineNumber: frame.lineNumber,
+          columnNumber: frame.columnNumber,
+          functionName: frame.name
+        });
+        if (frame.asyncCaller) {
+          frame = frame.asyncCaller;
+        } else {
+          frame = frame.caller;
+        }
+      }
+    }
+
+    this._saveStackTrace(channel, stacktrace);
+  },
+
+  onChannelRedirect(oldChannel, newChannel, flags) {
+    // We can be called with any nsIChannel, but are interested only in HTTP channels
+    try {
+      oldChannel.QueryInterface(Ci.nsIHttpChannel);
+      newChannel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      return;
+    }
+
+    let oldId = oldChannel.channelId;
+    let stacktrace = this.stacktracesById.get(oldId);
+    if (stacktrace) {
+      this.stacktracesById.delete(oldId);
+      this._saveStackTrace(newChannel, stacktrace);
+    }
+  },
+
+  getStackTrace(channelId) {
+    let trace = this.stacktracesById.get(channelId);
+    this.stacktracesById.delete(channelId);
+    return trace;
+  }
+};
+
+exports.StackTraceCollector = StackTraceCollector;
+
+/**
  * The network response listener implements the nsIStreamListener and
  * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature
  * to get the response body of the request.
  *
  * The code is mostly based on code listings from:
  *
  *   http://www.softwareishard.com/blog/firebug/
  *      nsitraceablechannel-intercept-http-traffic/
@@ -58,17 +288,16 @@ function NetworkResponseListener(owner, 
   this.owner = owner;
   this.receivedData = "";
   this.httpActivity = httpActivity;
   this.bodySize = 0;
   let channel = this.httpActivity.channel;
   this._wrappedNotificationCallbacks = channel.notificationCallbacks;
   channel.notificationCallbacks = this;
 }
-exports.NetworkResponseListener = NetworkResponseListener;
 
 NetworkResponseListener.prototype = {
   QueryInterface:
     XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
                            Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor,
                            Ci.nsISupports]),
 
   // nsIInterfaceRequestor implementation
@@ -457,56 +686,48 @@ NetworkResponseListener.prototype = {
  * requests. The nsIObserverService is also used for monitoring
  * http-on-examine-response notifications. All network request information is
  * routed to the remote Web Console.
  *
  * @constructor
  * @param object filters
  *        Object with the filters to use for network requests:
  *        - window (nsIDOMWindow): filter network requests by the associated
- *        window object.
+ *          window object.
  *        - appId (number): filter requests by the appId.
  *        - topFrame (nsIDOMElement): filter requests by their topFrameElement.
  *        Filters are optional. If any of these filters match the request is
  *        logged (OR is applied). If no filter is provided then all requests are
  *        logged.
  * @param object owner
  *        The network monitor owner. This object needs to hold:
  *        - onNetworkEvent(requestInfo, channel, networkMonitor).
- *        This method is invoked once for every new network request and it is
- *        given the following arguments: the initial network request
- *        information, and the channel. The third argument is the NetworkMonitor
- *        instance.
- *        onNetworkEvent() must return an object which holds several add*()
- *        methods which are used to add further network request/response
- *        information.
+ *          This method is invoked once for every new network request and it is
+ *          given the following arguments: the initial network request
+ *          information, and the channel. The third argument is the NetworkMonitor
+ *          instance. onNetworkEvent() must return an object which holds several add*()
+ *          methods which are used to add further network request/response
+ *          information.
+ *        - stackTraceCollector If the owner has this optional property, it will
+ *          be used as a StackTraceCollector by the NetworkMonitor.
  */
 function NetworkMonitor(filters, owner) {
-  if (filters) {
-    this.window = filters.window;
-    this.appId = filters.appId;
-    this.topFrame = filters.topFrame;
-  }
-  if (!this.window && !this.appId && !this.topFrame) {
-    this._logEverything = true;
-  }
+  this.filters = filters;
   this.owner = owner;
   this.openRequests = {};
   this.openResponses = {};
   this._httpResponseExaminer =
     DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this);
   this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this);
 }
+
 exports.NetworkMonitor = NetworkMonitor;
 
 NetworkMonitor.prototype = {
-  _logEverything: false,
-  window: null,
-  appId: null,
-  topFrame: null,
+  filters: null,
 
   httpTransactionCodes: {
     0x5001: "REQUEST_HEADER",
     0x5002: "REQUEST_BODY_SENT",
     0x5003: "RESPONSE_START",
     0x5004: "RESPONSE_HEADER",
     0x5005: "RESPONSE_COMPLETE",
     0x5006: "TRANSACTION_CLOSE",
@@ -561,17 +782,17 @@ NetworkMonitor.prototype = {
     // everything else only happens in the parent process
     Services.obs.addObserver(this._serviceWorkerRequest,
                              "service-worker-synthesized-response", false);
   },
 
   _serviceWorkerRequest: function (subject, topic, data) {
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
 
-    if (!this._matchRequest(channel)) {
+    if (!matchRequest(channel, this.filters)) {
       return;
     }
 
     this.interceptedChannels.add(subject);
 
     // On e10s, we never receive http-on-examine-cached-response, so fake one.
     if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
       this._httpResponseExaminer(channel, "http-on-examine-cached-response");
@@ -597,17 +818,17 @@ NetworkMonitor.prototype = {
         (topic != "http-on-examine-response" &&
          topic != "http-on-examine-cached-response") ||
         !(subject instanceof Ci.nsIHttpChannel)) {
       return;
     }
 
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
 
-    if (!this._matchRequest(channel)) {
+    if (!matchRequest(channel, this.filters)) {
       return;
     }
 
     let response = {
       id: gSequenceId(),
       channel: channel,
       headers: [],
       cookies: [],
@@ -752,94 +973,16 @@ NetworkMonitor.prototype = {
         this._onTransactionClose(httpActivity);
         break;
       default:
         break;
     }
   }),
 
   /**
-   * Check if a given network request should be logged by this network monitor
-   * instance based on the current filters.
-   *
-   * @private
-   * @param nsIHttpChannel channel
-   *        Request to check.
-   * @return boolean
-   *         True if the network request should be logged, false otherwise.
-   */
-  _matchRequest: function (channel) {
-    if (this._logEverything) {
-      return true;
-    }
-
-    // Ignore requests from chrome or add-on code when we are monitoring
-    // content.
-    // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
-    // the DevToolsUtils.testing check. We will move to a better way to serve
-    // its needs in bug 1167188, where this check should be removed.
-    if (!DevToolsUtils.testing && channel.loadInfo &&
-        channel.loadInfo.loadingDocument === null &&
-        channel.loadInfo.loadingPrincipal ===
-        Services.scriptSecurityManager.getSystemPrincipal()) {
-      return false;
-    }
-
-    if (this.window) {
-      // Since frames support, this.window may not be the top level content
-      // frame, so that we can't only compare with win.top.
-      let win = NetworkHelper.getWindowForRequest(channel);
-      while (win) {
-        if (win == this.window) {
-          return true;
-        }
-        if (win.parent == win) {
-          break;
-        }
-        win = win.parent;
-      }
-    }
-
-    if (this.topFrame) {
-      let topFrame = NetworkHelper.getTopFrameForRequest(channel);
-      if (topFrame && topFrame === this.topFrame) {
-        return true;
-      }
-    }
-
-    if (this.appId) {
-      let appId = NetworkHelper.getAppIdForRequest(channel);
-      if (appId && appId == this.appId) {
-        return true;
-      }
-    }
-
-    // The following check is necessary because beacon channels don't come
-    // associated with a load group. Bug 1160837 will hopefully introduce a
-    // platform fix that will render the following code entirely useless.
-    if (channel.loadInfo &&
-        channel.loadInfo.externalContentPolicyType ==
-        Ci.nsIContentPolicy.TYPE_BEACON) {
-      let nonE10sMatch = this.window &&
-          channel.loadInfo.loadingDocument === this.window.document;
-      const loadingPrincipal = channel.loadInfo.loadingPrincipal;
-      let e10sMatch = this.topFrame &&
-          this.topFrame.contentPrincipal &&
-          this.topFrame.contentPrincipal.equals(loadingPrincipal) &&
-          this.topFrame.contentPrincipal.URI.spec == channel.referrer.spec;
-      let b2gMatch = this.appId && loadingPrincipal.appId === this.appId;
-      if (nonE10sMatch || e10sMatch || b2gMatch) {
-        return true;
-      }
-    }
-
-    return false;
-  },
-
-  /**
    *
    */
   _createNetworkEvent: function (channel, { timestamp, extraStringData,
                                            fromCache, fromServiceWorker }) {
     let win = NetworkHelper.getWindowForRequest(channel);
     let httpActivity = this.createActivityObject(channel);
 
     // see _onRequestBodySent()
@@ -852,36 +995,51 @@ NetworkMonitor.prototype = {
       httpActivity.timings.REQUEST_HEADER = {
         first: timestamp,
         last: timestamp
       };
     }
 
     let event = {};
     event.method = channel.requestMethod;
+    event.channelId = channel.channelId;
     event.url = channel.URI.spec;
     event.private = httpActivity.private;
     event.headersSize = 0;
     event.startedDateTime =
       (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date())
       .toISOString();
     event.fromCache = fromCache;
     event.fromServiceWorker = fromServiceWorker;
     httpActivity.fromServiceWorker = fromServiceWorker;
 
     if (extraStringData) {
       event.headersSize = extraStringData.length;
     }
 
-    // Determine if this is an XHR request.
+    // Determine the cause and if this is an XHR request.
+    let causeType = channel.loadInfo.externalContentPolicyType;
+    let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+    let causeUri = loadingPrincipal ? loadingPrincipal.URI : null;
+    let stacktrace;
+    // If this is the parent process, there is no stackTraceCollector - the stack
+    // trace will be added in NetworkMonitorChild._onNewEvent.
+    if (this.owner.stackTraceCollector) {
+      stacktrace = this.owner.stackTraceCollector.getStackTrace(event.channelId);
+    }
+
+    event.cause = {
+      type: causeType,
+      loadingDocumentUri: causeUri ? causeUri.spec : null,
+      stacktrace
+    };
+
     httpActivity.isXHR = event.isXHR =
-      (channel.loadInfo.externalContentPolicyType ===
-       Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
-       channel.loadInfo.externalContentPolicyType ===
-       Ci.nsIContentPolicy.TYPE_FETCH);
+        (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
+         causeType === Ci.nsIContentPolicy.TYPE_FETCH);
 
     // Determine the HTTP version.
     let httpVersionMaj = {};
     let httpVersionMin = {};
     channel.QueryInterface(Ci.nsIHttpChannelInternal);
     channel.getRequestVersion(httpVersionMaj, httpVersionMin);
 
     event.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
@@ -927,22 +1085,21 @@ NetworkMonitor.prototype = {
    *
    * @private
    * @param nsIHttpChannel channel
    * @param number timestamp
    * @param string extraStringData
    * @return void
    */
   _onRequestHeader: function (channel, timestamp, extraStringData) {
-    if (!this._matchRequest(channel)) {
+    if (!matchRequest(channel, this.filters)) {
       return;
     }
 
-    this._createNetworkEvent(channel, { timestamp: timestamp,
-                                         extraStringData: extraStringData });
+    this._createNetworkEvent(channel, { timestamp, extraStringData });
   },
 
   /**
    * Create the empty HTTP activity object. This object is used for storing all
    * the request and response information.
    *
    * This is a HAR-like object. Conformance to the spec is not guaranteed at
    * this point.
@@ -1222,18 +1379,17 @@ NetworkMonitor.prototype = {
 
     Services.obs.removeObserver(this._serviceWorkerRequest,
                                 "service-worker-synthesized-response");
 
     this.interceptedChannels.clear();
     this.openRequests = {};
     this.openResponses = {};
     this.owner = null;
-    this.window = null;
-    this.topFrame = null;
+    this.filters = null;
   },
 };
 
 /**
  * The NetworkMonitorChild is used to proxy all of the network activity of the
  * child app process from the main process. The child WebConsoleActor creates an
  * instance of this object.
  *
@@ -1259,33 +1415,33 @@ function NetworkMonitorChild(appId, mess
   this.appId = appId;
   this.connID = connID;
   this.owner = owner;
   this._messageManager = messageManager;
   this._onNewEvent = this._onNewEvent.bind(this);
   this._onUpdateEvent = this._onUpdateEvent.bind(this);
   this._netEvents = new Map();
 }
+
 exports.NetworkMonitorChild = NetworkMonitorChild;
 
 NetworkMonitorChild.prototype = {
   appId: null,
   owner: null,
   _netEvents: null,
   _saveRequestAndResponseBodies: true,
 
   get saveRequestAndResponseBodies() {
     return this._saveRequestAndResponseBodies;
   },
 
   set saveRequestAndResponseBodies(val) {
     this._saveRequestAndResponseBodies = val;
 
     this._messageManager.sendAsyncMessage("debug:netmonitor:" + this.connID, {
-      appId: this.appId,
       action: "setPreferences",
       preferences: {
         saveRequestAndResponseBodies: this._saveRequestAndResponseBodies,
       },
     });
   },
 
   init: function () {
@@ -1297,16 +1453,23 @@ NetworkMonitorChild.prototype = {
     mm.sendAsyncMessage("debug:netmonitor:" + this.connID, {
       appId: this.appId,
       action: "start",
     });
   },
 
   _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) {
     let {id, event} = msg.data;
+
+    // Try to add stack trace to the event data received from parent
+    if (this.owner.stackTraceCollector) {
+      event.cause.stacktrace =
+        this.owner.stackTraceCollector.getStackTrace(event.channelId);
+    }
+
     let actor = this.owner.onNetworkEvent(event);
     this._netEvents.set(id, Cu.getWeakReference(actor));
   }),
 
   _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) {
     let {id, method, args} = msg.data;
     let weakActor = this._netEvents.get(id);
     let actor = weakActor ? weakActor.get() : null;
@@ -1443,21 +1606,22 @@ NetworkMonitorManager.prototype = {
   /**
    * Handler for "debug:monitor" messages received through the message manager
    * from the content process.
    *
    * @param object msg
    *        Message from the content.
    */
   onNetMonitorMessage: DevToolsUtils.makeInfallible(function (msg) {
-    let { action, appId } = msg.json;
+    let {action} = msg.json;
     // Pipe network monitor data from parent to child via the message manager.
     switch (action) {
       case "start":
         if (!this.netMonitor) {
+          let {appId} = msg.json;
           this.netMonitor = new NetworkMonitor({
             topFrame: this.frame,
             appId: appId,
           }, this);
           this.netMonitor.init();
         }
         break;