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',
'screenshot.js',
'source.js',
'stacktraces.js',
'storage.js',
'string.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,
actions: this.actions,
owner: this.owner,
resourceWatcher: this.toolbox.resourceWatcher,
});
// Register target listeners if we switched to a new top level one

View File

@ -27,17 +27,21 @@ class FirefoxDataProvider {
* Constructor for data provider
*
* @param {Object} webConsoleFront represents the client object for Console actor.
* @param {Object} actions set of actions fired during data fetching process
* @params {Object} owner all events are fired on this object
* @param {Object} actions set of actions fired during data fetching process.
* @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
this.webConsoleFront = webConsoleFront;
this.actions = actions || {};
this.actionsEnabled = true;
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();
// Map of the stacktrace information keyed by the actor id's
this.stackTraceRequestInfoByActorID = new Map();
// Internal properties
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) {
this.stackTraces.set(resource.channelId, resource);
this.stackTraces.set(resource.resourceId, resource);
}
/**
@ -345,15 +372,31 @@ class FirefoxDataProvider {
referrerPolicy,
blockedReason,
blockingExtension,
channelId,
resourceId,
stacktraceResourceId,
} = resource;
// Check if a stacktrace resource exists for this network event
if (this.stackTraces.has(channelId)) {
const { stacktrace, lastFrame } = this.stackTraces.get(channelId);
cause.stacktraceAvailable = stacktrace;
// Check if a stacktrace resource exists for this network resource.
// The stacktrace event is expected to happen before the network
// event so any neccesary stacktrace info should be available.
if (this.stackTraces.has(stacktraceResourceId)) {
const {
stacktraceAvailable,
lastFrame,
targetFront,
} = this.stackTraces.get(stacktraceResourceId);
cause.stacktraceAvailable = stacktraceAvailable;
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
@ -388,7 +431,7 @@ class FirefoxDataProvider {
referrerPolicy,
blockedReason,
blockingExtension,
channelId,
resourceId,
mimeType: resource?.content?.mimeType,
contentSize: bodySize,
...responseProps,
@ -638,7 +681,15 @@ class FirefoxDataProvider {
let response = await new Promise((resolve, reject) => {
// 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
// e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
this.webConsoleFront[clientMethodName](

View File

@ -67,7 +67,7 @@ function Messages(initialState = {}) {
const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
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(),
messageFilterText: "",
// Default filter type is "all",
@ -88,14 +88,14 @@ function Messages(initialState = {}) {
/**
* 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) {
if (!action.request) {
return state;
}
const { id, cause, channelId, isEventStream } = action.request;
const { id, cause, resourceId, isEventStream } = action.request;
const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
let currentChannelType = null;
let columnsKey = "columns";
@ -113,7 +113,7 @@ function setCurrentChannel(state, action) {
currentChannelType === state.currentChannelType
? { ...state.columns }
: { ...state[columnsKey] },
currentChannelId: channelId,
currentChannelId: resourceId,
currentChannelType,
currentRequestId: id,
// Default filter text is empty string for a new connection

View File

@ -159,7 +159,7 @@ function getRequestById(state, id) {
function getRequestByChannelId(state, channelId) {
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
*/
async function performPausedRequest(connector, tab, monitor) {
const wait = connector.connector.webConsoleFront.once("serverNetworkEvent");
await SpecialPowers.spawn(tab.linkedBrowser, [SIMPLE_SJS], async function(
url
) {
await content.wrappedJSObject.performRequests(url);
});
await wait;
}

View File

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

View File

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

View File

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

View File

@ -387,19 +387,34 @@ class WebConsoleUI {
}
if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
this.netEventStackTraces.set(resource.channelId, resource);
this.netEventStackTraces.set(resource.resourceId, resource);
continue;
}
if (resource.resourceType === TYPES.NETWORK_EVENT) {
// Add the stacktrace
if (this.netEventStackTraces.has(resource.channelId)) {
const { stacktrace, lastFrame } = this.netEventStackTraces.get(
resource.channelId
);
resource.cause.stacktraceAvailable = stacktrace;
if (this.netEventStackTraces.has(resource.resourceId)) {
const {
stacktraceAvailable,
lastFrame,
targetFront,
} = this.netEventStackTraces.get(resource.resourceId);
resource.cause.stacktraceAvailable = stacktraceAvailable;
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 {Toolbox} toolbox
* @param {Document} document
*
*/
constructor(parentNode, webConsoleUI, toolbox, document) {
EventEmitter.decorate(this);
@ -89,6 +90,7 @@ class WebConsoleWrapper {
updateRequest: (id, data) => this.batchedRequestUpdates({ id, data }),
},
webConsoleFront,
resourceWatcher: this.hud.resourceWatcher,
});
return new Promise(resolve => {

View File

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

View File

@ -70,10 +70,12 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
this._isXHR = networkEvent.isXHR;
this._cause = networkEvent.cause;
this._cause.stacktraceAvailable = !!(
this._stackTrace &&
(typeof this._stackTrace == "boolean" || this._stackTrace.length)
);
// Lets remove the last frame here as
// it is passed from the the server by the NETWORK_EVENT_STACKTRACE
// resource type. This done here for backward compatibility.
if (this._cause.lastFrame) {
delete this._cause.lastFrame;
}
this._fromCache = networkEvent.fromCache;
this._fromServiceWorker = networkEvent.fromServiceWorker;
@ -81,7 +83,7 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
networkEvent.isThirdPartyTrackingResource;
this._referrerPolicy = networkEvent.referrerPolicy;
this._channelId = networkEvent.channelId;
this._serial = networkEvent.serial;
this._blockedReason = networkEvent.blockedReason;
this._blockingExtension = networkEvent.blockingExtension;
@ -119,7 +121,9 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
referrerPolicy: this._referrerPolicy,
blockedReason: this._blockedReason,
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: [],
};
},
@ -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.
******************************************************************/
@ -491,10 +478,8 @@ const NetworkEventActor = protocol.ActorClassWithSpec(networkEventSpec, {
if (this.isDestroyed()) {
return;
}
this._onEventUpdate("responseCache", {
responseCache: content.responseCache,
});
this._response.responseCache = content.responseCache;
this._onEventUpdate("responseCache", {});
},
/**

View File

@ -837,6 +837,7 @@ NetworkObserver.prototype = {
}
if (wsChannel) {
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",
NETWORK_EVENT: "network-event",
STYLESHEET: "stylesheet",
NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
};
exports.TYPES = TYPES;
@ -49,6 +50,9 @@ const FrameTargetResources = augmentResourceDictionary({
[TYPES.STYLESHEET]: {
path: "devtools/server/actors/resources/stylesheets",
},
[TYPES.NETWORK_EVENT_STACKTRACE]: {
path: "devtools/server/actors/resources/network-events-stacktraces",
},
});
const ParentProcessResources = augmentResourceDictionary({
[TYPES.NETWORK_EVENT]: {

View File

@ -15,6 +15,7 @@ DevToolsModules(
'document-event.js',
'error-messages.js',
'index.js',
'network-events-stacktraces.js',
'network-events.js',
'platform-messages.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.
* - onUpdated: optional function
* 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.watcherActor = watcherActor;
this.onNetworkEventAvailable = onAvailable;
this.onNetworkEventUpdated = onUpdated;
this.onNeworkEventDestroyed = onDestroyed;
this.listener = new NetworkObserver(
{ browserId: watcherActor.browserId },
@ -58,7 +55,7 @@ class NetworkEventWatcher {
this,
{
onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
onNetworkEventDestroy: this.onNeworkEventDestroyed,
onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this),
},
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.
*

View File

@ -275,6 +275,14 @@ const ActorRegistry = {
constructor: "ManifestActor",
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.NETWORK_EVENT]:
enableServerWatcher && hasBrowserElement,
[Resources.TYPES.NETWORK_EVENT_STACKTRACE]:
enableServerWatcher && hasBrowserElement,
[Resources.TYPES.STYLESHEET]:
enableServerWatcher && hasBrowserElement,
},

View File

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

View File

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

View File

@ -280,7 +280,7 @@ class ResourceWatcher {
// ...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
// (See bug 1626647)
if (this._hasWatcherSupport(resourceType)) {
if (this.hasWatcherSupport(resourceType)) {
continue;
}
await this._watchResourcesForTarget(targetFront, resourceType);
@ -601,7 +601,7 @@ class ResourceWatcher {
);
}
_hasWatcherSupport(resourceType) {
hasWatcherSupport(resourceType) {
return this.watcher?.traits?.resources?.[resourceType];
}
@ -632,7 +632,7 @@ class ResourceWatcher {
// If the server supports the Watcher API and the Watcher supports
// this resource type, use this API
if (this._hasWatcherSupport(resourceType)) {
if (this.hasWatcherSupport(resourceType)) {
await this.watcher.watchResources([resourceType]);
return;
}
@ -712,7 +712,7 @@ class ResourceWatcher {
// If the server supports the Watcher API and the Watcher supports
// this resource type, use this API
if (this._hasWatcherSupport(resourceType)) {
if (this.hasWatcherSupport(resourceType)) {
this.watcher.unwatchResources([resourceType]);
return;
}

View File

@ -14,7 +14,7 @@ const TEST_URI = `${URL_ROOT_SSL}network_document.html`;
const REQUEST_STUB = {
code: `await fetch("/request_post_0.html", { method: "POST" });`,
expected: {
stacktrace: true,
stacktraceAvailable: true,
lastFrame: {
filename:
"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",
front: "devtools/client/fronts/source",
},
{
types: ["stacktraces"],
spec: "devtools/shared/specs/stacktraces",
front: "devtools/client/fronts/stacktraces",
},
{
types: [
"cookies",

View File

@ -45,6 +45,7 @@ DevToolsModules(
'root.js',
'screenshot.js',
'source.js',
'stacktraces.js',
'storage.js',
'string.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;