Bug 1134073 - Part 1: Collect information about request cause and stacktrace in netmonitor backend r=ochameau

MozReview-Commit-ID: BdrqblFyC66
This commit is contained in:
Jarda Snajdr 2016-06-01 01:03:00 +02:00
parent 0cb10b0230
commit d56d19a26c
3 changed files with 295 additions and 120 deletions

View File

@ -18,6 +18,7 @@ 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);
@ -598,6 +599,12 @@ WebConsoleActor.prototype =
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
@ -606,12 +613,10 @@ WebConsoleActor.prototype =
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();
}
}
@ -700,6 +705,10 @@ WebConsoleActor.prototype =
this.networkMonitorChild.destroy();
this.networkMonitorChild = null;
}
if (this.stackTraceCollector) {
this.stackTraceCollector.destroy();
this.stackTraceCollector = null;
}
stoppedListeners.push(listener);
break;
case "FileActivity":
@ -1828,6 +1837,7 @@ NetworkEventActor.prototype =
url: this._request.url,
method: this._request.method,
isXHR: this._isXHR,
cause: this._cause,
fromCache: this._fromCache,
fromServiceWorker: this._fromServiceWorker,
private: this._private,
@ -1873,6 +1883,7 @@ NetworkEventActor.prototype =
{
this._startedDateTime = aNetworkEvent.startedDateTime;
this._isXHR = aNetworkEvent.isXHR;
this._cause = aNetworkEvent.cause;
this._fromCache = aNetworkEvent.fromCache;
this._fromServiceWorker = aNetworkEvent.fromServiceWorker;

View File

@ -100,6 +100,7 @@ WebConsoleClient.prototype = {
method: actor.method,
},
isXHR: actor.isXHR,
cause: actor.cause,
response: {},
timings: {},
// track the list of network event updates

View File

@ -6,7 +6,7 @@
"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");
@ -36,6 +36,236 @@ 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
@ -63,7 +293,6 @@ function NetworkResponseListener(owner, httpActivity) {
this._wrappedNotificationCallbacks = channel.notificationCallbacks;
channel.notificationCallbacks = this;
}
exports.NetworkResponseListener = NetworkResponseListener;
NetworkResponseListener.prototype = {
QueryInterface:
@ -462,7 +691,7 @@ NetworkResponseListener.prototype = {
* @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
@ -471,23 +700,17 @@ NetworkResponseListener.prototype = {
* @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 = {};
@ -495,13 +718,11 @@ function NetworkMonitor(filters, owner) {
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",
@ -566,7 +787,7 @@ NetworkMonitor.prototype = {
_serviceWorkerRequest: function (subject, topic, data) {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (!this._matchRequest(channel)) {
if (!matchRequest(channel, this.filters)) {
return;
}
@ -602,7 +823,7 @@ NetworkMonitor.prototype = {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
if (!this._matchRequest(channel)) {
if (!matchRequest(channel, this.filters)) {
return;
}
@ -756,84 +977,6 @@ NetworkMonitor.prototype = {
}
}),
/**
* 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;
},
/**
*
*/
@ -857,6 +1000,7 @@ NetworkMonitor.prototype = {
let event = {};
event.method = channel.requestMethod;
event.channelId = channel.channelId;
event.url = channel.URI.spec;
event.private = httpActivity.private;
event.headersSize = 0;
@ -871,12 +1015,26 @@ NetworkMonitor.prototype = {
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 = {};
@ -932,12 +1090,11 @@ NetworkMonitor.prototype = {
* @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 });
},
/**
@ -1227,8 +1384,7 @@ NetworkMonitor.prototype = {
this.openRequests = {};
this.openResponses = {};
this.owner = null;
this.window = null;
this.topFrame = null;
this.filters = null;
},
};
@ -1264,6 +1420,7 @@ function NetworkMonitorChild(appId, messageManager, connID, owner) {
this._onUpdateEvent = this._onUpdateEvent.bind(this);
this._netEvents = new Map();
}
exports.NetworkMonitorChild = NetworkMonitorChild;
NetworkMonitorChild.prototype = {
@ -1280,7 +1437,6 @@ NetworkMonitorChild.prototype = {
this._saveRequestAndResponseBodies = val;
this._messageManager.sendAsyncMessage("debug:netmonitor:" + this.connID, {
appId: this.appId,
action: "setPreferences",
preferences: {
saveRequestAndResponseBodies: this._saveRequestAndResponseBodies,
@ -1302,6 +1458,13 @@ NetworkMonitorChild.prototype = {
_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));
}),
@ -1448,11 +1611,12 @@ NetworkMonitorManager.prototype = {
* 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,
@ -1460,7 +1624,6 @@ NetworkMonitorManager.prototype = {
this.netMonitor.init();
}
break;
case "setPreferences": {
let {preferences} = msg.json;
for (let key of Object.keys(preferences)) {