Bug 1663523 - [devtools] Add the network event stacktrace watcher r=ochameau,devtools-backward-compat-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D86809
This commit is contained in:
Hubert Boma Manilla 2020-10-19 01:19:39 +00:00
parent 61728de827
commit 3d1f26e2cd
29 changed files with 463 additions and 87 deletions

View File

@ -41,6 +41,7 @@ DevToolsModules(
'root.js', 'root.js',
'screenshot.js', 'screenshot.js',
'source.js', 'source.js',
'stacktraces.js',
'storage.js', 'storage.js',
'string.js', 'string.js',
'styles.js', 'styles.js',

View File

@ -0,0 +1,22 @@
/* 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 {
FrontClassWithSpec,
registerFront,
} = require("devtools/shared/protocol");
const { stackTracesSpec } = require("devtools/shared/specs/stacktraces");
class StackTracesFront extends FrontClassWithSpec(stackTracesSpec) {
constructor(client, targetFront, parentFront) {
super(client, targetFront, parentFront);
// Attribute name from which to retrieve the actorID out of the target actor's form
this.formAttributeName = "stacktracesActor";
}
}
exports.StackTracesFront = StackTracesFront;
registerFront(StackTracesFront);

View File

@ -140,6 +140,7 @@ class FirefoxConnector {
webConsoleFront: this.webConsoleFront, webConsoleFront: this.webConsoleFront,
actions: this.actions, actions: this.actions,
owner: this.owner, owner: this.owner,
resourceWatcher: this.toolbox.resourceWatcher,
}); });
// Register target listeners if we switched to a new top level one // Register target listeners if we switched to a new top level one

View File

@ -27,17 +27,21 @@ class FirefoxDataProvider {
* Constructor for data provider * Constructor for data provider
* *
* @param {Object} webConsoleFront represents the client object for Console actor. * @param {Object} webConsoleFront represents the client object for Console actor.
* @param {Object} actions set of actions fired during data fetching process * @param {Object} actions set of actions fired during data fetching process.
* @params {Object} owner all events are fired on this object * @param {Object} owner all events are fired on this object.
* @param {Object} resourceWatcher enables checking for watcher support
*/ */
constructor({ webConsoleFront, actions, owner }) { constructor({ webConsoleFront, actions, owner, resourceWatcher }) {
// Options // Options
this.webConsoleFront = webConsoleFront; this.webConsoleFront = webConsoleFront;
this.actions = actions || {}; this.actions = actions || {};
this.actionsEnabled = true; this.actionsEnabled = true;
this.owner = owner; this.owner = owner;
// Map of all stacktrace resources keyed by network event's channelId this.resourceWatcher = resourceWatcher;
// Map of all stacktrace resources keyed by network event's resourceId
this.stackTraces = new Map(); this.stackTraces = new Map();
// Map of the stacktrace information keyed by the actor id's
this.stackTraceRequestInfoByActorID = new Map();
// Internal properties // Internal properties
this.payloadQueue = new Map(); this.payloadQueue = new Map();
@ -322,8 +326,31 @@ class FirefoxDataProvider {
}); });
} }
/**
* Retrieve the stack-trace information for the given StackTracesActor.
*
* @param object actor
* - {Object} targetFront: the target front.
*
* - {String} resourceId: the resource id for the network request".
* @return {object}
*/
async _getStackTraceFromWatcher(actor) {
const stacktracesFront = await actor.targetFront.getFront("stacktraces");
const stacktrace = await stacktracesFront.getStackTrace(
actor.stacktraceResourceId
);
return { stacktrace };
}
/**
* The handler for when the network event stacktrace resource is available.
* The resource contains basic info, the actual stacktrace is fetched lazily
* using requestData.
* @param {object} resource The network event stacktrace resource
*/
onStackTraceAvailable(resource) { onStackTraceAvailable(resource) {
this.stackTraces.set(resource.channelId, resource); this.stackTraces.set(resource.resourceId, resource);
} }
/** /**
@ -345,15 +372,31 @@ class FirefoxDataProvider {
referrerPolicy, referrerPolicy,
blockedReason, blockedReason,
blockingExtension, blockingExtension,
channelId, resourceId,
stacktraceResourceId,
} = resource; } = resource;
// Check if a stacktrace resource exists for this network event // Check if a stacktrace resource exists for this network resource.
if (this.stackTraces.has(channelId)) { // The stacktrace event is expected to happen before the network
const { stacktrace, lastFrame } = this.stackTraces.get(channelId); // event so any neccesary stacktrace info should be available.
cause.stacktraceAvailable = stacktrace; if (this.stackTraces.has(stacktraceResourceId)) {
const {
stacktraceAvailable,
lastFrame,
targetFront,
} = this.stackTraces.get(stacktraceResourceId);
cause.stacktraceAvailable = stacktraceAvailable;
cause.lastFrame = lastFrame; cause.lastFrame = lastFrame;
this.stackTraces.delete(channelId); this.stackTraces.delete(stacktraceResourceId);
// We retrieve preliminary information about the stacktrace from the
// NETWORK_EVENT_STACKTRACE resource via `this.stackTraces` Map,
// The actual stacktrace is fetched lazily based on the actor id, using
// the targetFront and the stacktrace resource id therefore we
// map these for easy access.
this.stackTraceRequestInfoByActorID.set(actor, {
targetFront,
stacktraceResourceId,
});
} }
// For resources from the resource watcher cache no updates are going to be fired // For resources from the resource watcher cache no updates are going to be fired
@ -388,7 +431,7 @@ class FirefoxDataProvider {
referrerPolicy, referrerPolicy,
blockedReason, blockedReason,
blockingExtension, blockingExtension,
channelId, resourceId,
mimeType: resource?.content?.mimeType, mimeType: resource?.content?.mimeType,
contentSize: bodySize, contentSize: bodySize,
...responseProps, ...responseProps,
@ -638,7 +681,15 @@ class FirefoxDataProvider {
let response = await new Promise((resolve, reject) => { let response = await new Promise((resolve, reject) => {
// Do a RDP request to fetch data from the actor. // Do a RDP request to fetch data from the actor.
if (typeof this.webConsoleFront[clientMethodName] === "function") { if (
clientMethodName == "getStackTrace" &&
this.resourceWatcher.hasWatcherSupport(
this.resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE
)
) {
const requestInfo = this.stackTraceRequestInfoByActorID.get(actor);
resolve(this._getStackTraceFromWatcher(requestInfo));
} else if (typeof this.webConsoleFront[clientMethodName] === "function") {
// Make sure we fetch the real actor data instead of cloned actor // Make sure we fetch the real actor data instead of cloned actor
// e.g. CustomRequestPanel will clone a request with additional '-clone' actor id // e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
this.webConsoleFront[clientMethodName]( this.webConsoleFront[clientMethodName](

View File

@ -67,7 +67,7 @@ function Messages(initialState = {}) {
const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE; const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
return { return {
// Map with all requests (key = channelId, value = array of message objects) // Map with all requests (key = resourceId, value = array of message objects)
messages: new Map(), messages: new Map(),
messageFilterText: "", messageFilterText: "",
// Default filter type is "all", // Default filter type is "all",
@ -88,14 +88,14 @@ function Messages(initialState = {}) {
/** /**
* When a network request is selected, * When a network request is selected,
* set the current channelId affiliated with the connection. * set the current resourceId affiliated with the connection.
*/ */
function setCurrentChannel(state, action) { function setCurrentChannel(state, action) {
if (!action.request) { if (!action.request) {
return state; return state;
} }
const { id, cause, channelId, isEventStream } = action.request; const { id, cause, resourceId, isEventStream } = action.request;
const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE; const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
let currentChannelType = null; let currentChannelType = null;
let columnsKey = "columns"; let columnsKey = "columns";
@ -113,7 +113,7 @@ function setCurrentChannel(state, action) {
currentChannelType === state.currentChannelType currentChannelType === state.currentChannelType
? { ...state.columns } ? { ...state.columns }
: { ...state[columnsKey] }, : { ...state[columnsKey] },
currentChannelId: channelId, currentChannelId: resourceId,
currentChannelType, currentChannelType,
currentRequestId: id, currentRequestId: id,
// Default filter text is empty string for a new connection // Default filter text is empty string for a new connection

View File

@ -159,7 +159,7 @@ function getRequestById(state, id) {
function getRequestByChannelId(state, channelId) { function getRequestByChannelId(state, channelId) {
return [...state.requests.requests.values()].find( return [...state.requests.requests.values()].find(
r => r.channelId == channelId r => r.resourceId == channelId
); );
} }

View File

@ -82,11 +82,9 @@ async function performRequestAndWait(tab, monitor) {
* Execute simple GET request * Execute simple GET request
*/ */
async function performPausedRequest(connector, tab, monitor) { async function performPausedRequest(connector, tab, monitor) {
const wait = connector.connector.webConsoleFront.once("serverNetworkEvent");
await SpecialPowers.spawn(tab.linkedBrowser, [SIMPLE_SJS], async function( await SpecialPowers.spawn(tab.linkedBrowser, [SIMPLE_SJS], async function(
url url
) { ) {
await content.wrappedJSObject.performRequests(url); await content.wrappedJSObject.performRequests(url);
}); });
await wait;
} }

View File

@ -75,8 +75,10 @@ async function generateNetworkEventStubs() {
for (const resource of resources) { for (const resource of resources) {
if (resource.resourceType == resourceWatcher.TYPES.NETWORK_EVENT) { if (resource.resourceType == resourceWatcher.TYPES.NETWORK_EVENT) {
if (stacktraces.has(resource.channelId)) { if (stacktraces.has(resource.channelId)) {
const { stacktrace, lastFrame } = stacktraces.get(resource.channelId); const { stacktraceAvailable, lastFrame } = stacktraces.get(
resource.cause.stacktraceAvailable = stacktrace; resource.channelId
);
resource.cause.stacktraceAvailable = stacktraceAvailable;
resource.cause.lastFrame = lastFrame; resource.cause.lastFrame = lastFrame;
stacktraces.delete(resource.channelId); stacktraces.delete(resource.channelId);
} }
@ -121,6 +123,8 @@ async function generateNetworkEventStubs() {
const updateKey = `${key} update`; const updateKey = `${key} update`;
// make sure all the updates have been happened // make sure all the updates have been happened
if (updateCount >= noExpectedUpdates) { if (updateCount >= noExpectedUpdates) {
// make sure the network event stub contains all the updates
stubs.set(key, getCleanedPacket(key, getOrderedResource(resource)));
stubs.set( stubs.set(
updateKey, updateKey,
// We cannot ensure the form of the resource, some properties // We cannot ensure the form of the resource, some properties

View File

@ -288,6 +288,10 @@ function getCleanedPacket(key, packet) {
res.totalTime = existingPacket.totalTime; res.totalTime = existingPacket.totalTime;
} }
if (res.securityState && existingPacket.securityState) {
res.securityState = existingPacket.securityState;
}
if (res.actor && existingPacket.actor) { if (res.actor && existingPacket.actor) {
res.actor = existingPacket.actor; res.actor = existingPacket.actor;
} }

View File

@ -20,9 +20,7 @@ const {
const rawPackets = new Map(); const rawPackets = new Map();
rawPackets.set(`GET request`, { rawPackets.set(`GET request`, {
"resourceType": "network-event", "resourceType": "network-event",
"_type": "NetworkEvent",
"timeStamp": 1572867483805, "timeStamp": 1572867483805,
"node": null,
"actor": "server0.conn0.netEvent4", "actor": "server0.conn0.netEvent4",
"discardRequestBody": true, "discardRequestBody": true,
"discardResponseBody": false, "discardResponseBody": false,
@ -63,7 +61,6 @@ rawPackets.set(`GET request`, {
"private": false, "private": false,
"isThirdPartyTrackingResource": false, "isThirdPartyTrackingResource": false,
"referrerPolicy": "no-referrer-when-downgrade", "referrerPolicy": "no-referrer-when-downgrade",
"channelId": 265845590720515,
"updates": [ "updates": [
"eventTimings", "eventTimings",
"requestCookies", "requestCookies",
@ -80,9 +77,7 @@ rawPackets.set(`GET request`, {
rawPackets.set(`GET request update`, { rawPackets.set(`GET request update`, {
"resourceType": "network-event", "resourceType": "network-event",
"_type": "NetworkEvent",
"timeStamp": 1572867483805, "timeStamp": 1572867483805,
"node": null,
"actor": "server0.conn0.netEvent5", "actor": "server0.conn0.netEvent5",
"discardRequestBody": true, "discardRequestBody": true,
"discardResponseBody": false, "discardResponseBody": false,
@ -123,7 +118,6 @@ rawPackets.set(`GET request update`, {
"private": false, "private": false,
"isThirdPartyTrackingResource": false, "isThirdPartyTrackingResource": false,
"referrerPolicy": "no-referrer-when-downgrade", "referrerPolicy": "no-referrer-when-downgrade",
"channelId": 202499118071811,
"updates": [ "updates": [
"eventTimings", "eventTimings",
"requestCookies", "requestCookies",
@ -140,9 +134,7 @@ rawPackets.set(`GET request update`, {
rawPackets.set(`XHR GET request`, { rawPackets.set(`XHR GET request`, {
"resourceType": "network-event", "resourceType": "network-event",
"_type": "NetworkEvent",
"timeStamp": 1572867483805, "timeStamp": 1572867483805,
"node": null,
"actor": "server0.conn0.netEvent21", "actor": "server0.conn0.netEvent21",
"discardRequestBody": true, "discardRequestBody": true,
"discardResponseBody": false, "discardResponseBody": false,
@ -183,7 +175,6 @@ rawPackets.set(`XHR GET request`, {
"private": false, "private": false,
"isThirdPartyTrackingResource": false, "isThirdPartyTrackingResource": false,
"referrerPolicy": "no-referrer-when-downgrade", "referrerPolicy": "no-referrer-when-downgrade",
"channelId": 202499118071812,
"updates": [ "updates": [
"eventTimings", "eventTimings",
"requestCookies", "requestCookies",
@ -200,9 +191,7 @@ rawPackets.set(`XHR GET request`, {
rawPackets.set(`XHR GET request update`, { rawPackets.set(`XHR GET request update`, {
"resourceType": "network-event", "resourceType": "network-event",
"_type": "NetworkEvent",
"timeStamp": 1572867483805, "timeStamp": 1572867483805,
"node": null,
"actor": "server0.conn0.netEvent20", "actor": "server0.conn0.netEvent20",
"discardRequestBody": true, "discardRequestBody": true,
"discardResponseBody": false, "discardResponseBody": false,
@ -258,9 +247,7 @@ rawPackets.set(`XHR GET request update`, {
rawPackets.set(`XHR POST request`, { rawPackets.set(`XHR POST request`, {
"resourceType": "network-event", "resourceType": "network-event",
"_type": "NetworkEvent",
"timeStamp": 1572867483805, "timeStamp": 1572867483805,
"node": null,
"actor": "server0.conn0.netEvent36", "actor": "server0.conn0.netEvent36",
"discardRequestBody": true, "discardRequestBody": true,
"discardResponseBody": false, "discardResponseBody": false,
@ -301,7 +288,6 @@ rawPackets.set(`XHR POST request`, {
"private": false, "private": false,
"isThirdPartyTrackingResource": false, "isThirdPartyTrackingResource": false,
"referrerPolicy": "no-referrer-when-downgrade", "referrerPolicy": "no-referrer-when-downgrade",
"channelId": 265845590720517,
"updates": [ "updates": [
"eventTimings", "eventTimings",
"requestCookies", "requestCookies",
@ -318,9 +304,7 @@ rawPackets.set(`XHR POST request`, {
rawPackets.set(`XHR POST request update`, { rawPackets.set(`XHR POST request update`, {
"resourceType": "network-event", "resourceType": "network-event",
"_type": "NetworkEvent",
"timeStamp": 1572867483805, "timeStamp": 1572867483805,
"node": null,
"actor": "server0.conn0.netEvent36", "actor": "server0.conn0.netEvent36",
"discardRequestBody": true, "discardRequestBody": true,
"discardResponseBody": false, "discardResponseBody": false,

View File

@ -387,19 +387,34 @@ class WebConsoleUI {
} }
if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) { if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
this.netEventStackTraces.set(resource.channelId, resource); this.netEventStackTraces.set(resource.resourceId, resource);
continue; continue;
} }
if (resource.resourceType === TYPES.NETWORK_EVENT) { if (resource.resourceType === TYPES.NETWORK_EVENT) {
// Add the stacktrace // Add the stacktrace
if (this.netEventStackTraces.has(resource.channelId)) { if (this.netEventStackTraces.has(resource.resourceId)) {
const { stacktrace, lastFrame } = this.netEventStackTraces.get( const {
resource.channelId stacktraceAvailable,
); lastFrame,
resource.cause.stacktraceAvailable = stacktrace; targetFront,
} = this.netEventStackTraces.get(resource.resourceId);
resource.cause.stacktraceAvailable = stacktraceAvailable;
resource.cause.lastFrame = lastFrame; resource.cause.lastFrame = lastFrame;
this.netEventStackTraces.delete(resource.channelId); this.netEventStackTraces.delete(resource.resourceId);
if (
this.wrapper?.networkDataProvider?.stackTraceRequestInfoByActorID
) {
this.wrapper.networkDataProvider.stackTraceRequestInfoByActorID.set(
resource.actor,
{
targetFront,
resourceId: resource.resourceId,
}
);
}
} }
} }

View File

@ -60,6 +60,7 @@ class WebConsoleWrapper {
* @param {WebConsoleUI} webConsoleUI * @param {WebConsoleUI} webConsoleUI
* @param {Toolbox} toolbox * @param {Toolbox} toolbox
* @param {Document} document * @param {Document} document
*
*/ */
constructor(parentNode, webConsoleUI, toolbox, document) { constructor(parentNode, webConsoleUI, toolbox, document) {
EventEmitter.decorate(this); EventEmitter.decorate(this);
@ -89,6 +90,7 @@ class WebConsoleWrapper {
updateRequest: (id, data) => this.batchedRequestUpdates({ id, data }), updateRequest: (id, data) => this.batchedRequestUpdates({ id, data }),
}, },
webConsoleFront, webConsoleFront,
resourceWatcher: this.hud.resourceWatcher,
}); });
return new Promise(resolve => { return new Promise(resolve => {

View File

@ -17,5 +17,6 @@ DevToolsModules(
'network-observer.js', 'network-observer.js',
'network-response-listener.js', 'network-response-listener.js',
'stack-trace-collector.js', 'stack-trace-collector.js',
'stack-traces-actor.js',
'websocket-actor.js', 'websocket-actor.js',
) )

View File

@ -70,10 +70,12 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
this._isXHR = networkEvent.isXHR; this._isXHR = networkEvent.isXHR;
this._cause = networkEvent.cause; this._cause = networkEvent.cause;
this._cause.stacktraceAvailable = !!( // Lets remove the last frame here as
this._stackTrace && // it is passed from the the server by the NETWORK_EVENT_STACKTRACE
(typeof this._stackTrace == "boolean" || this._stackTrace.length) // resource type. This done here for backward compatibility.
); if (this._cause.lastFrame) {
delete this._cause.lastFrame;
}
this._fromCache = networkEvent.fromCache; this._fromCache = networkEvent.fromCache;
this._fromServiceWorker = networkEvent.fromServiceWorker; this._fromServiceWorker = networkEvent.fromServiceWorker;
@ -81,7 +83,7 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
networkEvent.isThirdPartyTrackingResource; networkEvent.isThirdPartyTrackingResource;
this._referrerPolicy = networkEvent.referrerPolicy; this._referrerPolicy = networkEvent.referrerPolicy;
this._channelId = networkEvent.channelId; this._channelId = networkEvent.channelId;
this._serial = networkEvent.serial;
this._blockedReason = networkEvent.blockedReason; this._blockedReason = networkEvent.blockedReason;
this._blockingExtension = networkEvent.blockingExtension; this._blockingExtension = networkEvent.blockingExtension;
@ -119,7 +121,9 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
referrerPolicy: this._referrerPolicy, referrerPolicy: this._referrerPolicy,
blockedReason: this._blockedReason, blockedReason: this._blockedReason,
blockingExtension: this._blockingExtension, blockingExtension: this._blockingExtension,
channelId: this._channelId, // For websocket requests the serial is used instead of the channel id.
stacktraceResourceId:
this._cause.type == "websocket" ? this._serial : this._channelId,
updates: [], updates: [],
}; };
}, },
@ -260,23 +264,6 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
}; };
}, },
/**
* The "getStackTrace" packet type handler.
*
* @return object
* The response packet - stack trace.
*/
async getStackTrace() {
const stacktrace = this._stackTrace;
if (stacktrace && typeof stacktrace == "boolean") {
this._stackTrace = [];
}
return {
stacktrace,
};
},
/** **************************************************************** /** ****************************************************************
* Listeners for new network event data coming from NetworkMonitor. * Listeners for new network event data coming from NetworkMonitor.
******************************************************************/ ******************************************************************/
@ -491,10 +478,8 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
if (this.isDestroyed()) { if (this.isDestroyed()) {
return; return;
} }
this._response.responseCache = content.responseCache;
this._onEventUpdate("responseCache", { this._onEventUpdate("responseCache", {});
responseCache: content.responseCache,
});
}, },
/** /**

View File

@ -837,6 +837,7 @@ NetworkObserver.prototype = {
} }
if (wsChannel) { if (wsChannel) {
event.url = wsChannel.URI.spec; event.url = wsChannel.URI.spec;
event.serial = wsChannel.serial;
} }
} }

View File

@ -0,0 +1,59 @@
/* 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 { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
const { stackTracesSpec } = require("devtools/shared/specs/stacktraces");
loader.lazyRequireGetter(
this,
"WebConsoleUtils",
"devtools/server/actors/webconsole/utils",
true
);
const {
TYPES: { NETWORK_EVENT_STACKTRACE },
getResourceWatcher,
} = require("devtools/server/actors/resources/index");
/**
* Manages the stacktraces of the network
*
* @constructor
*
*/
const StackTracesActor = ActorClassWithSpec(stackTracesSpec, {
initialize(conn, targetActor) {
Actor.prototype.initialize.call(this, conn);
this.targetActor = targetActor;
},
destroy(conn) {
Actor.prototype.destroy.call(this, conn);
},
/**
* The "getStackTrace" packet type handler.
*
* @return object
* The response packet - stack trace.
*/
getStackTrace(resourceId) {
const networkEventStackTraceWatcher = getResourceWatcher(
this.targetActor,
NETWORK_EVENT_STACKTRACE
);
if (!networkEventStackTraceWatcher) {
throw new Error("Not listening for network event stacktraces");
}
const stacktrace = networkEventStackTraceWatcher.getStackTrace(resourceId);
return {
stacktrace: WebConsoleUtils.removeFramesAboveDebuggerEval(stacktrace),
};
},
});
exports.StackTracesActor = StackTracesActor;

View File

@ -15,6 +15,7 @@ const TYPES = {
PLATFORM_MESSAGE: "platform-message", PLATFORM_MESSAGE: "platform-message",
NETWORK_EVENT: "network-event", NETWORK_EVENT: "network-event",
STYLESHEET: "stylesheet", STYLESHEET: "stylesheet",
NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
}; };
exports.TYPES = TYPES; exports.TYPES = TYPES;
@ -49,6 +50,9 @@ const FrameTargetResources = augmentResourceDictionary({
[TYPES.STYLESHEET]: { [TYPES.STYLESHEET]: {
path: "devtools/server/actors/resources/stylesheets", path: "devtools/server/actors/resources/stylesheets",
}, },
[TYPES.NETWORK_EVENT_STACKTRACE]: {
path: "devtools/server/actors/resources/network-events-stacktraces",
},
}); });
const ParentProcessResources = augmentResourceDictionary({ const ParentProcessResources = augmentResourceDictionary({
[TYPES.NETWORK_EVENT]: { [TYPES.NETWORK_EVENT]: {

View File

@ -15,6 +15,7 @@ DevToolsModules(
'document-event.js', 'document-event.js',
'error-messages.js', 'error-messages.js',
'index.js', 'index.js',
'network-events-stacktraces.js',
'network-events.js', 'network-events.js',
'platform-messages.js', 'platform-messages.js',
'stylesheets.js', 'stylesheets.js',

View File

@ -0,0 +1,203 @@
/* 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 {
TYPES: { NETWORK_EVENT_STACKTRACE },
} = require("devtools/server/actors/resources/index");
const { Ci, components } = require("chrome");
const Services = require("Services");
loader.lazyRequireGetter(
this,
"ChannelEventSinkFactory",
"devtools/server/actors/network-monitor/channel-event-sink",
true
);
loader.lazyRequireGetter(
this,
"matchRequest",
"devtools/server/actors/network-monitor/network-observer",
true
);
class NetworkEventStackTracesWatcher {
/**
* Start watching for all network event's stack traces related to a given Target actor.
*
* @param TargetActor targetActor
* The target actor from which we should observe the strack traces
* @param Object options
* Dictionary object with following attributes:
* - onAvailable: mandatory
* This will be called for each resource.
*/
async watch(targetActor, { onAvailable }) {
Services.obs.addObserver(this, "http-on-opening-request");
Services.obs.addObserver(this, "document-on-opening-request");
Services.obs.addObserver(this, "network-monitor-alternate-stack");
ChannelEventSinkFactory.getService().registerCollector(this);
this.targetActor = targetActor;
this.onStackTraceAvailable = onAvailable;
this.stacktraces = new Map();
}
/**
* Stop watching for network event's strack traces related to a given Target Actor.
*
* @param TargetActor targetActor
* The target actor from which we should stop observing the strack traces
*/
destroy(targetActor) {
Services.obs.removeObserver(this, "http-on-opening-request");
Services.obs.removeObserver(this, "document-on-opening-request");
Services.obs.removeObserver(this, "network-monitor-alternate-stack");
ChannelEventSinkFactory.getService().unregisterCollector(this);
}
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;
}
const oldId = oldChannel.channelId;
const stacktrace = this.stacktraces.get(oldId);
if (stacktrace) {
this._setStackTrace(newChannel.channelId, stacktrace);
}
}
observe(subject, topic, data) {
let channel, id;
try {
// We need to QI nsIHttpChannel in order to load the interface's
// methods / attributes for later code that could assume we are dealing
// with a nsIHttpChannel.
channel = subject.QueryInterface(Ci.nsIHttpChannel);
id = channel.channelId;
} catch (e1) {
try {
channel = subject.QueryInterface(Ci.nsIIdentChannel);
id = channel.channelId;
} catch (e2) {
// WebSocketChannels do not have IDs, so use the serial. When a WebSocket is
// opened in a content process, a channel is created locally but the HTTP
// channel for the connection lives entirely in the parent process. When
// the server code running in the parent sees that HTTP channel, it will
// look for the creation stack using the websocket's serial.
try {
channel = subject.QueryInterface(Ci.nsIWebSocketChannel);
id = channel.serial;
} catch (e3) {
// Channels which don't implement the above interfaces can appear here,
// such as nsIFileChannel. Ignore these channels.
return;
}
}
}
// XXX: is window the best filter to use?
if (!matchRequest(channel, { window: this.targetActor.window })) {
return;
}
if (this.stacktraces.has(id)) {
// We can get up to two stack traces for the same channel: one each from
// the two observer topics we are listening to. Use the first stack trace
// which is specified, and ignore any later one.
return;
}
const stacktrace = [];
switch (topic) {
case "http-on-opening-request":
case "document-on-opening-request": {
// The channel is being opened on the main thread, associate the current
// stack with it.
//
// Convert the nsIStackFrame XPCOM objects to a nice JSON that can be
// passed around through message managers etc.
let frame = components.stack;
if (frame?.caller) {
frame = frame.caller;
while (frame) {
stacktrace.push({
filename: frame.filename,
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
functionName: frame.name,
asyncCause: frame.asyncCause,
});
frame = frame.caller || frame.asyncCaller;
}
}
break;
}
case "network-monitor-alternate-stack": {
// An alternate stack trace is being specified for this channel.
// The topic data is the JSON for the saved frame stack we should use,
// so convert this into the expected format.
//
// This topic is used in the following cases:
//
// - The HTTP channel is opened asynchronously or on a different thread
// from the code which triggered its creation, in which case the stack
// from components.stack will be empty. The alternate stack will be
// for the point we want to associate with the channel.
//
// - The channel is not a nsIHttpChannel, and we will receive no
// opening request notification for it.
let frame = JSON.parse(data);
while (frame) {
stacktrace.push({
filename: frame.source,
lineNumber: frame.line,
columnNumber: frame.column,
functionName: frame.functionDisplayName,
asyncCause: frame.asyncCause,
});
frame = frame.parent || frame.asyncParent;
}
break;
}
default:
throw new Error("Unexpected observe() topic");
}
this._setStackTrace(id, stacktrace);
}
_setStackTrace(resourceId, stacktrace) {
this.stacktraces.set(resourceId, stacktrace);
this.onStackTraceAvailable([
{
resourceType: NETWORK_EVENT_STACKTRACE,
resourceId,
targetFront: this.targetFront,
stacktraceAvailable: stacktrace && stacktrace.length > 0,
lastFrame:
stacktrace && stacktrace.length > 0 ? stacktrace[0] : undefined,
},
]);
}
getStackTrace(id) {
let stacktrace = [];
if (this.stacktraces.has(id)) {
stacktrace = this.stacktraces.get(id);
this.stacktraces.delete(id);
}
return stacktrace;
}
}
module.exports = NetworkEventStackTracesWatcher;

View File

@ -30,15 +30,12 @@ class NetworkEventWatcher {
* This will be called for each resource. * This will be called for each resource.
* - onUpdated: optional function * - onUpdated: optional function
* This would be called multiple times for each resource. * This would be called multiple times for each resource.
* - onDestroyed: optional function
* This would be called multiple times for each resource.
*/ */
async watch(watcherActor, { onAvailable, onUpdated, onDestroyed }) { async watch(watcherActor, { onAvailable, onUpdated }) {
this.networkEvents = new Map(); this.networkEvents = new Map();
this.watcherActor = watcherActor; this.watcherActor = watcherActor;
this.onNetworkEventAvailable = onAvailable; this.onNetworkEventAvailable = onAvailable;
this.onNetworkEventUpdated = onUpdated; this.onNetworkEventUpdated = onUpdated;
this.onNeworkEventDestroyed = onDestroyed;
this.listener = new NetworkObserver( this.listener = new NetworkObserver(
{ browserId: watcherActor.browserId }, { browserId: watcherActor.browserId },
@ -58,7 +55,7 @@ class NetworkEventWatcher {
this, this,
{ {
onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this), onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
onNetworkEventDestroy: this.onNeworkEventDestroyed, onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this),
}, },
event event
); );
@ -141,6 +138,12 @@ class NetworkEventWatcher {
]); ]);
} }
onNetworkEventDestroy(channelId) {
if (this.networkEvents.has(channelId)) {
this.networkEvents.delete(channelId);
}
}
/** /**
* Stop watching for network event related to a given Watcher Actor. * Stop watching for network event related to a given Watcher Actor.
* *

View File

@ -275,6 +275,14 @@ const ActorRegistry = {
constructor: "ManifestActor", constructor: "ManifestActor",
type: { target: true }, type: { target: true },
}); });
this.registerModule(
"devtools/server/actors/network-monitor/stack-traces-actor",
{
prefix: "stacktraces",
constructor: "StackTracesActor",
type: { target: true },
}
);
}, },
/** /**

View File

@ -117,6 +117,8 @@ exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
[Resources.TYPES.PLATFORM_MESSAGE]: true, [Resources.TYPES.PLATFORM_MESSAGE]: true,
[Resources.TYPES.NETWORK_EVENT]: [Resources.TYPES.NETWORK_EVENT]:
enableServerWatcher && hasBrowserElement, enableServerWatcher && hasBrowserElement,
[Resources.TYPES.NETWORK_EVENT_STACKTRACE]:
enableServerWatcher && hasBrowserElement,
[Resources.TYPES.STYLESHEET]: [Resources.TYPES.STYLESHEET]:
enableServerWatcher && hasBrowserElement, enableServerWatcher && hasBrowserElement,
}, },

View File

@ -19,8 +19,8 @@ module.exports = async function({
onAvailable([ onAvailable([
{ {
resourceType: ResourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE, resourceType: ResourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE,
channelId: actor.channelId, resourceId: actor.channelId,
stacktrace: actor.cause.stacktraceAvailable, stacktraceAvailable: actor.cause.stacktraceAvailable,
lastFrame: actor.cause.lastFrame, lastFrame: actor.cause.lastFrame,
}, },
]); ]);

View File

@ -39,9 +39,7 @@ module.exports = async function({
const resource = { const resource = {
resourceId: actor.channelId, resourceId: actor.channelId,
resourceType: ResourceWatcher.TYPES.NETWORK_EVENT, resourceType: ResourceWatcher.TYPES.NETWORK_EVENT,
_type: "NetworkEvent",
timeStamp: actor.timeStamp, timeStamp: actor.timeStamp,
node: null,
actor: actor.actor, actor: actor.actor,
discardRequestBody: true, discardRequestBody: true,
discardResponseBody: true, discardResponseBody: true,
@ -61,7 +59,10 @@ module.exports = async function({
referrerPolicy: actor.referrerPolicy, referrerPolicy: actor.referrerPolicy,
blockedReason: actor.blockedReason, blockedReason: actor.blockedReason,
blockingExtension: actor.blockingExtension, blockingExtension: actor.blockingExtension,
channelId: actor.channelId, stacktraceResourceId:
actor.cause.type == "websocket"
? actor.url.replace(/^http/, "ws")
: actor.channelId,
updates: [], updates: [],
}; };

View File

@ -280,7 +280,7 @@ class ResourceWatcher {
// ...request existing resource and new one to come from this one target // ...request existing resource and new one to come from this one target
// *but* only do that for backward compat, where we don't have the watcher API // *but* only do that for backward compat, where we don't have the watcher API
// (See bug 1626647) // (See bug 1626647)
if (this._hasWatcherSupport(resourceType)) { if (this.hasWatcherSupport(resourceType)) {
continue; continue;
} }
await this._watchResourcesForTarget(targetFront, resourceType); await this._watchResourcesForTarget(targetFront, resourceType);
@ -601,7 +601,7 @@ class ResourceWatcher {
); );
} }
_hasWatcherSupport(resourceType) { hasWatcherSupport(resourceType) {
return this.watcher?.traits?.resources?.[resourceType]; return this.watcher?.traits?.resources?.[resourceType];
} }
@ -632,7 +632,7 @@ class ResourceWatcher {
// If the server supports the Watcher API and the Watcher supports // If the server supports the Watcher API and the Watcher supports
// this resource type, use this API // this resource type, use this API
if (this._hasWatcherSupport(resourceType)) { if (this.hasWatcherSupport(resourceType)) {
await this.watcher.watchResources([resourceType]); await this.watcher.watchResources([resourceType]);
return; return;
} }
@ -712,7 +712,7 @@ class ResourceWatcher {
// If the server supports the Watcher API and the Watcher supports // If the server supports the Watcher API and the Watcher supports
// this resource type, use this API // this resource type, use this API
if (this._hasWatcherSupport(resourceType)) { if (this.hasWatcherSupport(resourceType)) {
this.watcher.unwatchResources([resourceType]); this.watcher.unwatchResources([resourceType]);
return; return;
} }

View File

@ -14,7 +14,7 @@ const TEST_URI = `${URL_ROOT_SSL}network_document.html`;
const REQUEST_STUB = { const REQUEST_STUB = {
code: `await fetch("/request_post_0.html", { method: "POST" });`, code: `await fetch("/request_post_0.html", { method: "POST" });`,
expected: { expected: {
stacktrace: true, stacktraceAvailable: true,
lastFrame: { lastFrame: {
filename: filename:
"https://example.com/browser/devtools/shared/resources/tests/network_document.html", "https://example.com/browser/devtools/shared/resources/tests/network_document.html",

View File

@ -209,6 +209,11 @@ const Types = (exports.__TypesForTests = [
spec: "devtools/shared/specs/source", spec: "devtools/shared/specs/source",
front: "devtools/client/fronts/source", front: "devtools/client/fronts/source",
}, },
{
types: ["stacktraces"],
spec: "devtools/shared/specs/stacktraces",
front: "devtools/client/fronts/stacktraces",
},
{ {
types: [ types: [
"cookies", "cookies",

View File

@ -45,6 +45,7 @@ DevToolsModules(
'root.js', 'root.js',
'screenshot.js', 'screenshot.js',
'source.js', 'source.js',
'stacktraces.js',
'storage.js', 'storage.js',
'string.js', 'string.js',
'styles.js', 'styles.js',

View File

@ -0,0 +1,20 @@
/* 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 { generateActorSpec, RetVal, Arg } = require("devtools/shared/protocol");
const stackTracesSpec = generateActorSpec({
typeName: "stacktraces",
methods: {
getStackTrace: {
request: { resourceId: Arg(0) },
// stacktrace is an "array:string", but not always.
response: RetVal("json"),
},
},
});
exports.stackTracesSpec = stackTracesSpec;