Bug 1362036 - Implement http inspection in new console; r=nchevobbe

MozReview-Commit-ID: FhYePLM2T3O

--HG--
extra : rebase_source : 79f3cd0d99aee930b97ac1b0323e62bd9b6aa732
This commit is contained in:
Jan Odvarko 2017-08-30 15:16:39 +02:00
parent eb2fe0e37f
commit bc02e69656
21 changed files with 845 additions and 505 deletions

View File

@ -203,11 +203,11 @@ const HeadersPanel = createClass({
className: "headers-summary learn-more-link",
}),
button({
className: "devtools-button",
className: "devtools-button edit-and-resend-button",
onClick: cloneSelectedRequest,
}, EDIT_AND_RESEND),
button({
className: "devtools-button",
className: "devtools-button raw-headers-button",
onClick: this.toggleRawHeaders,
}, RAW_HEADERS),
)

View File

@ -11,7 +11,7 @@ const {
const { connect } = require("devtools/client/shared/vendor/react-redux");
const Actions = require("../actions/index");
const { L10N } = require("../utils/l10n");
const { getSelectedRequest } = require("../selectors/index");
const { PANELS } = require("../constants");
// Components
const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
@ -56,45 +56,45 @@ function TabboxPanel({
showAllTabsMenu: true,
},
TabPanel({
id: "headers",
id: PANELS.HEADERS,
title: HEADERS_TITLE,
},
HeadersPanel({ request, cloneSelectedRequest }),
),
TabPanel({
id: "cookies",
id: PANELS.COOKIES,
title: COOKIES_TITLE,
},
CookiesPanel({ request }),
),
TabPanel({
id: "params",
id: PANELS.PARAMS,
title: PARAMS_TITLE,
},
ParamsPanel({ request }),
),
TabPanel({
id: "response",
id: PANELS.RESPONSE,
title: RESPONSE_TITLE,
},
ResponsePanel({ request }),
),
TabPanel({
id: "timings",
id: PANELS.TIMINGS,
title: TIMINGS_TITLE,
},
TimingsPanel({ request }),
),
request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
TabPanel({
id: "stack-trace",
id: PANELS.STACK_TRACE,
title: STACK_TRACE_TITLE,
},
StackTracePanel({ request, sourceMapService }),
),
request.securityState && request.securityState !== "insecure" &&
TabPanel({
id: "security",
id: PANELS.SECURITY,
title: SECURITY_TITLE,
},
SecurityPanel({ request }),
@ -107,7 +107,7 @@ TabboxPanel.displayName = "TabboxPanel";
TabboxPanel.propTypes = {
activeTabId: PropTypes.string,
cloneSelectedRequest: PropTypes.func.isRequired,
cloneSelectedRequest: PropTypes.func,
request: PropTypes.object,
selectTab: PropTypes.func.isRequired,
// Service to enable the source map feature.
@ -116,8 +116,6 @@ TabboxPanel.propTypes = {
module.exports = connect(
(state) => ({
activeTabId: state.ui.detailsPanelSelectedTab,
request: getSelectedRequest(state),
}),
(dispatch) => ({
cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),

View File

@ -5,54 +5,29 @@
"use strict";
const Services = require("Services");
const { CurlUtils } = require("devtools/client/shared/curl");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const { ACTIVITY_TYPE, EVENTS } = require("../constants");
const { getDisplayedRequestById } = require("../selectors/index");
const { fetchHeaders, formDataURI } = require("../utils/request-utils");
const FirefoxDataProvider = require("./firefox-data-provider");
class FirefoxConnector {
constructor() {
// Internal properties
this.payloadQueue = [];
// Public methods
this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this);
this.willNavigate = this.willNavigate.bind(this);
this.displayCachedEvents = this.displayCachedEvents.bind(this);
this.onDocLoadingMarker = this.onDocLoadingMarker.bind(this);
this.addRequest = this.addRequest.bind(this);
this.updateRequest = this.updateRequest.bind(this);
this.fetchImage = this.fetchImage.bind(this);
this.fetchRequestHeaders = this.fetchRequestHeaders.bind(this);
this.fetchResponseHeaders = this.fetchResponseHeaders.bind(this);
this.fetchPostData = this.fetchPostData.bind(this);
this.fetchResponseCookies = this.fetchResponseCookies.bind(this);
this.fetchRequestCookies = this.fetchRequestCookies.bind(this);
this.getPayloadFromQueue = this.getPayloadFromQueue.bind(this);
this.isQueuePayloadReady = this.isQueuePayloadReady.bind(this);
this.pushPayloadToQueue = this.pushPayloadToQueue.bind(this);
this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
this.setPreferences = this.setPreferences.bind(this);
this.triggerActivity = this.triggerActivity.bind(this);
this.inspectRequest = this.inspectRequest.bind(this);
this.getLongString = this.getLongString.bind(this);
this.getNetworkRequest = this.getNetworkRequest.bind(this);
this.getTabTarget = this.getTabTarget.bind(this);
this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
// Event handlers
this.onNetworkEvent = this.onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
this.onRequestHeaders = this.onRequestHeaders.bind(this);
this.onRequestCookies = this.onRequestCookies.bind(this);
this.onRequestPostData = this.onRequestPostData.bind(this);
this.onSecurityInfo = this.onSecurityInfo.bind(this);
this.onResponseHeaders = this.onResponseHeaders.bind(this);
this.onResponseCookies = this.onResponseCookies.bind(this);
this.onResponseContent = this.onResponseContent.bind(this);
this.onEventTimings = this.onEventTimings.bind(this);
// Internals
this.getLongString = this.getLongString.bind(this);
this.getNetworkRequest = this.getNetworkRequest.bind(this);
}
async connect(connection, actions, getState) {
@ -63,10 +38,17 @@ class FirefoxConnector {
this.webConsoleClient = this.tabTarget.activeConsole;
this.dataProvider = new FirefoxDataProvider({
webConsoleClient: this.webConsoleClient,
actions: this.actions,
});
this.tabTarget.on("will-navigate", this.willNavigate);
this.tabTarget.on("close", this.disconnect);
this.webConsoleClient.on("networkEvent", this.onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate", this.onNetworkEventUpdate);
this.webConsoleClient.on("networkEvent",
this.dataProvider.onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate",
this.dataProvider.onNetworkEventUpdate);
// Don't start up waiting for timeline markers if the server isn't
// recent enough to emit the markers we're interested in.
@ -96,6 +78,7 @@ class FirefoxConnector {
this.webConsoleClient.off("networkEventUpdate");
this.webConsoleClient = null;
this.timelineFront = null;
this.dataProvider = null;
}
willNavigate() {
@ -114,10 +97,10 @@ class FirefoxConnector {
displayCachedEvents() {
for (let networkInfo of this.webConsoleClient.getNetworkEvents()) {
// First add the request to the timeline.
this.onNetworkEvent("networkEvent", networkInfo);
this.dataProvider.onNetworkEvent("networkEvent", networkInfo);
// Then replay any updates already received.
for (let updateType of networkInfo.updates) {
this.onNetworkEventUpdate("networkEventUpdate", {
this.dataProvider.onNetworkEventUpdate("networkEventUpdate", {
packet: { updateType },
networkInfo,
});
@ -135,222 +118,6 @@ class FirefoxConnector {
this.actions.addTimingMarker(marker);
}
/**
* Add a new network request to application state.
*
* @param {string} id request id
* @param {object} data data payload will be added to application state
*/
addRequest(id, data) {
let {
method,
url,
isXHR,
cause,
startedDateTime,
fromCache,
fromServiceWorker,
} = data;
this.actions.addRequest(
id,
{
// Convert the received date/time string to a unix timestamp.
startedMillis: Date.parse(startedDateTime),
method,
url,
isXHR,
cause,
fromCache,
fromServiceWorker,
},
true,
)
.then(() => window.emit(EVENTS.REQUEST_ADDED, id));
}
/**
* Update a network request if it already exists in application state.
*
* @param {string} id request id
* @param {object} data data payload will be updated to application state
*/
async updateRequest(id, data) {
let {
mimeType,
responseContent,
responseCookies,
responseHeaders,
requestCookies,
requestHeaders,
requestPostData,
} = data;
// fetch request detail contents in parallel
let [
imageObj,
requestHeadersObj,
responseHeadersObj,
postDataObj,
requestCookiesObj,
responseCookiesObj,
] = await Promise.all([
this.fetchImage(mimeType, responseContent),
this.fetchRequestHeaders(requestHeaders),
this.fetchResponseHeaders(responseHeaders),
this.fetchPostData(requestPostData),
this.fetchRequestCookies(requestCookies),
this.fetchResponseCookies(responseCookies),
]);
let payload = Object.assign({}, data,
imageObj, requestHeadersObj, responseHeadersObj,
postDataObj, requestCookiesObj, responseCookiesObj);
this.pushPayloadToQueue(id, payload);
if (this.isQueuePayloadReady(id)) {
await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
}
}
async fetchImage(mimeType, responseContent) {
let payload = {};
if (mimeType && responseContent && responseContent.content) {
let { encoding, text } = responseContent.content;
let response = await this.getLongString(text);
if (mimeType.includes("image/")) {
payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
}
responseContent.content.text = response;
payload.responseContent = responseContent;
}
return payload;
}
async fetchRequestHeaders(requestHeaders) {
let payload = {};
if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
let headers = await fetchHeaders(requestHeaders, this.getLongString);
if (headers) {
payload.requestHeaders = headers;
}
}
return payload;
}
async fetchResponseHeaders(responseHeaders) {
let payload = {};
if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
let headers = await fetchHeaders(responseHeaders, this.getLongString);
if (headers) {
payload.responseHeaders = headers;
}
}
return payload;
}
async fetchPostData(requestPostData) {
let payload = {};
if (requestPostData && requestPostData.postData) {
let { text } = requestPostData.postData;
let postData = await this.getLongString(text);
const headers = CurlUtils.getHeadersFromMultipartText(postData);
const headersSize = headers.reduce((acc, { name, value }) => {
return acc + name.length + value.length + 2;
}, 0);
requestPostData.postData.text = postData;
payload.requestPostData = Object.assign({}, requestPostData);
payload.requestHeadersFromUploadStream = { headers, headersSize };
}
return payload;
}
async fetchResponseCookies(responseCookies) {
let payload = {};
if (responseCookies) {
let resCookies = [];
// response store cookies in responseCookies or responseCookies.cookies
let cookies = responseCookies.cookies ?
responseCookies.cookies : responseCookies;
// make sure cookies is iterable
if (typeof cookies[Symbol.iterator] === "function") {
for (let cookie of cookies) {
resCookies.push(Object.assign({}, cookie, {
value: await this.getLongString(cookie.value),
}));
}
if (resCookies.length) {
payload.responseCookies = resCookies;
}
}
}
return payload;
}
async fetchRequestCookies(requestCookies) {
let payload = {};
if (requestCookies) {
let reqCookies = [];
// request store cookies in requestCookies or requestCookies.cookies
let cookies = requestCookies.cookies ?
requestCookies.cookies : requestCookies;
// make sure cookies is iterable
if (typeof cookies[Symbol.iterator] === "function") {
for (let cookie of cookies) {
reqCookies.push(Object.assign({}, cookie, {
value: await this.getLongString(cookie.value),
}));
}
if (reqCookies.length) {
payload.requestCookies = reqCookies;
}
}
}
return payload;
}
/**
* Access a payload item from payload queue.
*
* @param {string} id request id
* @return {boolean} return a queued payload item from queue.
*/
getPayloadFromQueue(id) {
return this.payloadQueue.find((item) => item.id === id);
}
/**
* Packet order of "networkUpdateEvent" is predictable, as a result we can wait for
* the last one "eventTimings" packet arrives to check payload is ready.
*
* @param {string} id request id
* @return {boolean} return whether a specific networkEvent has been updated completely.
*/
isQueuePayloadReady(id) {
let queuedPayload = this.getPayloadFromQueue(id);
return queuedPayload && queuedPayload.payload.eventTimings;
}
/**
* Push a request payload into a queue if request doesn't exist. Otherwise update the
* request itself.
*
* @param {string} id request id
* @param {object} payload request data payload
*/
pushPayloadToQueue(id, payload) {
let queuedPayload = this.getPayloadFromQueue(id);
if (!queuedPayload) {
this.payloadQueue.push({ id, payload });
} else {
// Merge upcoming networkEventUpdate payload into existing one
queuedPayload.payload = Object.assign({}, queuedPayload.payload, payload);
}
}
/**
* Send a HTTP request data payload
*
@ -490,7 +257,7 @@ class FirefoxConnector {
* @return {object} networkInfo data packet
*/
getNetworkRequest(id) {
return this.webConsoleClient.getNetworkRequest(id);
return this.dataProvider.getNetworkRequest(id);
}
/**
@ -505,7 +272,7 @@ class FirefoxConnector {
* are available, or rejected if something goes wrong.
*/
getLongString(stringGrip) {
return this.webConsoleClient.getString(stringGrip);
return this.dataProvider.getLongString(stringGrip);
}
/**
@ -526,213 +293,6 @@ class FirefoxConnector {
this.toolbox.viewSourceInDebugger(sourceURL, sourceLine);
}
}
/**
* The "networkEvent" message type handler.
*
* @param {string} type message type
* @param {object} networkInfo network request information
*/
onNetworkEvent(type, networkInfo) {
let {
actor,
cause,
fromCache,
fromServiceWorker,
isXHR,
request: {
method,
url,
},
startedDateTime,
} = networkInfo;
this.addRequest(actor, {
cause,
fromCache,
fromServiceWorker,
isXHR,
method,
startedDateTime,
url,
});
window.emit(EVENTS.NETWORK_EVENT, actor);
}
/**
* The "networkEventUpdate" message type handler.
*
* @param {string} type message type
* @param {object} packet the message received from the server.
* @param {object} networkInfo the network request information.
*/
onNetworkEventUpdate(type, { packet, networkInfo }) {
let { actor } = networkInfo;
switch (packet.updateType) {
case "requestHeaders":
this.webConsoleClient.getRequestHeaders(actor, this.onRequestHeaders);
window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
break;
case "requestCookies":
this.webConsoleClient.getRequestCookies(actor, this.onRequestCookies);
window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
break;
case "requestPostData":
this.webConsoleClient.getRequestPostData(actor, this.onRequestPostData);
window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
break;
case "securityInfo":
this.updateRequest(actor, {
securityState: networkInfo.securityInfo,
}).then(() => {
this.webConsoleClient.getSecurityInfo(actor, this.onSecurityInfo);
window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
});
break;
case "responseHeaders":
this.webConsoleClient.getResponseHeaders(actor, this.onResponseHeaders);
window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
break;
case "responseCookies":
this.webConsoleClient.getResponseCookies(actor, this.onResponseCookies);
window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
break;
case "responseStart":
this.updateRequest(actor, {
httpVersion: networkInfo.response.httpVersion,
remoteAddress: networkInfo.response.remoteAddress,
remotePort: networkInfo.response.remotePort,
status: networkInfo.response.status,
statusText: networkInfo.response.statusText,
headersSize: networkInfo.response.headersSize
}).then(() => {
window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
});
break;
case "responseContent":
this.webConsoleClient.getResponseContent(actor,
this.onResponseContent.bind(this, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType
}));
window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
break;
case "eventTimings":
this.updateRequest(actor, { totalTime: networkInfo.totalTime })
.then(() => {
this.webConsoleClient.getEventTimings(actor, this.onEventTimings);
window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
});
break;
}
}
/**
* Handles additional information received for a "requestHeaders" packet.
*
* @param {object} response the message received from the server.
*/
onRequestHeaders(response) {
this.updateRequest(response.from, {
requestHeaders: response
}).then(() => {
window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
});
}
/**
* Handles additional information received for a "requestCookies" packet.
*
* @param {object} response the message received from the server.
*/
onRequestCookies(response) {
this.updateRequest(response.from, {
requestCookies: response
}).then(() => {
window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
});
}
/**
* Handles additional information received for a "requestPostData" packet.
*
* @param {object} response the message received from the server.
*/
onRequestPostData(response) {
this.updateRequest(response.from, {
requestPostData: response
}).then(() => {
window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
});
}
/**
* Handles additional information received for a "securityInfo" packet.
*
* @param {object} response the message received from the server.
*/
onSecurityInfo(response) {
this.updateRequest(response.from, {
securityInfo: response.securityInfo
}).then(() => {
window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
});
}
/**
* Handles additional information received for a "responseHeaders" packet.
*
* @param {object} response the message received from the server.
*/
onResponseHeaders(response) {
this.updateRequest(response.from, {
responseHeaders: response
}).then(() => {
window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
});
}
/**
* Handles additional information received for a "responseCookies" packet.
*
* @param {object} response the message received from the server.
*/
onResponseCookies(response) {
this.updateRequest(response.from, {
responseCookies: response
}).then(() => {
window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
});
}
/**
* Handles additional information received for a "responseContent" packet.
*
* @param {object} data the message received from the server event.
* @param {object} response the message received from the server.
*/
onResponseContent(data, response) {
let payload = Object.assign({ responseContent: response }, data);
this.updateRequest(response.from, payload).then(() => {
window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
});
}
/**
* Handles additional information received for a "eventTimings" packet.
*
* @param {object} response the message received from the server.
*/
onEventTimings(response) {
this.updateRequest(response.from, {
eventTimings: response
}).then(() => {
window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
});
}
}
module.exports = new FirefoxConnector();

View File

@ -0,0 +1,525 @@
/* 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/. */
/* eslint-disable block-scoped-var */
"use strict";
const { EVENTS } = require("../constants");
const { CurlUtils } = require("devtools/client/shared/curl");
const { fetchHeaders, formDataURI } = require("../utils/request-utils");
/**
* This object is responsible for fetching additional HTTP
* data from the backend.
*/
class FirefoxDataProvider {
constructor({webConsoleClient, actions}) {
// Options
this.webConsoleClient = webConsoleClient;
this.actions = actions;
// Internal properties
this.payloadQueue = [];
// Public methods
this.addRequest = this.addRequest.bind(this);
this.updateRequest = this.updateRequest.bind(this);
// Internals
this.fetchImage = this.fetchImage.bind(this);
this.fetchRequestHeaders = this.fetchRequestHeaders.bind(this);
this.fetchResponseHeaders = this.fetchResponseHeaders.bind(this);
this.fetchPostData = this.fetchPostData.bind(this);
this.fetchResponseCookies = this.fetchResponseCookies.bind(this);
this.fetchRequestCookies = this.fetchRequestCookies.bind(this);
this.getPayloadFromQueue = this.getPayloadFromQueue.bind(this);
this.isQueuePayloadReady = this.isQueuePayloadReady.bind(this);
this.pushPayloadToQueue = this.pushPayloadToQueue.bind(this);
this.getLongString = this.getLongString.bind(this);
this.getNetworkRequest = this.getNetworkRequest.bind(this);
// Event handlers
this.onNetworkEvent = this.onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
this.onRequestHeaders = this.onRequestHeaders.bind(this);
this.onRequestCookies = this.onRequestCookies.bind(this);
this.onRequestPostData = this.onRequestPostData.bind(this);
this.onSecurityInfo = this.onSecurityInfo.bind(this);
this.onResponseHeaders = this.onResponseHeaders.bind(this);
this.onResponseCookies = this.onResponseCookies.bind(this);
this.onResponseContent = this.onResponseContent.bind(this);
this.onEventTimings = this.onEventTimings.bind(this);
}
/**
* Add a new network request to application state.
*
* @param {string} id request id
* @param {object} data data payload will be added to application state
*/
async addRequest(id, data) {
let {
method,
url,
isXHR,
cause,
startedDateTime,
fromCache,
fromServiceWorker,
} = data;
if (this.actions.addRequest) {
await this.actions.addRequest(id, {
// Convert the received date/time string to a unix timestamp.
startedMillis: Date.parse(startedDateTime),
method,
url,
isXHR,
cause,
fromCache,
fromServiceWorker},
true,
);
}
emit(EVENTS.REQUEST_ADDED, id);
}
/**
* Update a network request if it already exists in application state.
*
* @param {string} id request id
* @param {object} data data payload will be updated to application state
*/
async updateRequest(id, data) {
let {
mimeType,
responseContent,
responseCookies,
responseHeaders,
requestCookies,
requestHeaders,
requestPostData,
} = data;
// fetch request detail contents in parallel
let [
imageObj,
requestHeadersObj,
responseHeadersObj,
postDataObj,
requestCookiesObj,
responseCookiesObj,
] = await Promise.all([
this.fetchImage(mimeType, responseContent),
this.fetchRequestHeaders(requestHeaders),
this.fetchResponseHeaders(responseHeaders),
this.fetchPostData(requestPostData),
this.fetchRequestCookies(requestCookies),
this.fetchResponseCookies(responseCookies),
]);
let payload = Object.assign({},
data,
imageObj,
requestHeadersObj,
responseHeadersObj,
postDataObj,
requestCookiesObj,
responseCookiesObj
);
this.pushPayloadToQueue(id, payload);
if (this.actions.updateRequest && this.isQueuePayloadReady(id)) {
await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
}
}
async fetchImage(mimeType, responseContent) {
let payload = {};
if (mimeType && responseContent && responseContent.content) {
let { encoding, text } = responseContent.content;
let response = await this.getLongString(text);
if (mimeType.includes("image/")) {
payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
}
responseContent.content.text = response;
payload.responseContent = responseContent;
}
return payload;
}
async fetchRequestHeaders(requestHeaders) {
let payload = {};
if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
let headers = await fetchHeaders(requestHeaders, this.getLongString);
if (headers) {
payload.requestHeaders = headers;
}
}
return payload;
}
async fetchResponseHeaders(responseHeaders) {
let payload = {};
if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
let headers = await fetchHeaders(responseHeaders, this.getLongString);
if (headers) {
payload.responseHeaders = headers;
}
}
return payload;
}
async fetchPostData(requestPostData) {
let payload = {};
if (requestPostData && requestPostData.postData) {
let { text } = requestPostData.postData;
let postData = await this.getLongString(text);
const headers = CurlUtils.getHeadersFromMultipartText(postData);
// Calculate total header size and don't forget to include
// two new-line characters at the end.
const headersSize = headers.reduce((acc, { name, value }) => {
return acc + name.length + value.length + 2;
}, 0);
requestPostData.postData.text = postData;
payload.requestPostData = Object.assign({}, requestPostData);
payload.requestHeadersFromUploadStream = { headers, headersSize };
}
return payload;
}
async fetchResponseCookies(responseCookies) {
let payload = {};
if (responseCookies) {
let resCookies = [];
// response store cookies in responseCookies or responseCookies.cookies
let cookies = responseCookies.cookies ?
responseCookies.cookies : responseCookies;
// make sure cookies is iterable
if (typeof cookies[Symbol.iterator] === "function") {
for (let cookie of cookies) {
resCookies.push(Object.assign({}, cookie, {
value: await this.getLongString(cookie.value),
}));
}
if (resCookies.length) {
payload.responseCookies = resCookies;
}
}
}
return payload;
}
async fetchRequestCookies(requestCookies) {
let payload = {};
if (requestCookies) {
let reqCookies = [];
// request store cookies in requestCookies or requestCookies.cookies
let cookies = requestCookies.cookies ?
requestCookies.cookies : requestCookies;
// make sure cookies is iterable
if (typeof cookies[Symbol.iterator] === "function") {
for (let cookie of cookies) {
reqCookies.push(Object.assign({}, cookie, {
value: await this.getLongString(cookie.value),
}));
}
if (reqCookies.length) {
payload.requestCookies = reqCookies;
}
}
}
return payload;
}
/**
* Access a payload item from payload queue.
*
* @param {string} id request id
* @return {boolean} return a queued payload item from queue.
*/
getPayloadFromQueue(id) {
return this.payloadQueue.find((item) => item.id === id);
}
/**
* Return true if payload is ready (all data fetched from the backend)
*
* @param {string} id request id
* @return {boolean} return whether a specific networkEvent has been updated completely.
*/
isQueuePayloadReady(id) {
let queuedPayload = this.getPayloadFromQueue(id);
// TODO we should find a better solution since it might happen
// that eventTimings is not the last update.
return queuedPayload && queuedPayload.payload.eventTimings;
}
/**
* Push a request payload into a queue if request doesn't exist. Otherwise update the
* request itself.
*
* @param {string} id request id
* @param {object} payload request data payload
*/
pushPayloadToQueue(id, payload) {
let queuedPayload = this.getPayloadFromQueue(id);
if (!queuedPayload) {
this.payloadQueue.push({ id, payload });
} else {
// Merge upcoming networkEventUpdate payload into existing one
queuedPayload.payload = Object.assign({}, queuedPayload.payload, payload);
}
}
/**
* Fetches the network information packet from actor server
*
* @param {string} id request id
* @return {object} networkInfo data packet
*/
getNetworkRequest(id) {
return this.webConsoleClient.getNetworkRequest(id);
}
/**
* Fetches the full text of a LongString.
*
* @param {object|string} stringGrip
* The long string grip containing the corresponding actor.
* If you pass in a plain string (by accident or because you're lazy),
* then a promise of the same string is simply returned.
* @return {object}
* A promise that is resolved when the full string contents
* are available, or rejected if something goes wrong.
*/
getLongString(stringGrip) {
return this.webConsoleClient.getString(stringGrip);
}
/**
* The "networkEvent" message type handler.
*
* @param {string} type message type
* @param {object} networkInfo network request information
*/
onNetworkEvent(type, networkInfo) {
let {
actor,
cause,
fromCache,
fromServiceWorker,
isXHR,
request: {
method,
url,
},
startedDateTime,
} = networkInfo;
this.addRequest(actor, {
cause,
fromCache,
fromServiceWorker,
isXHR,
method,
startedDateTime,
url,
});
emit(EVENTS.NETWORK_EVENT, actor);
}
/**
* The "networkEventUpdate" message type handler.
*
* @param {string} type message type
* @param {object} packet the message received from the server.
* @param {object} networkInfo the network request information.
*/
onNetworkEventUpdate(type, { packet, networkInfo }) {
let { actor } = networkInfo;
switch (packet.updateType) {
case "requestHeaders":
this.webConsoleClient.getRequestHeaders(actor, this.onRequestHeaders);
emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
break;
case "requestCookies":
this.webConsoleClient.getRequestCookies(actor, this.onRequestCookies);
emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
break;
case "requestPostData":
this.webConsoleClient.getRequestPostData(actor, this.onRequestPostData);
emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
break;
case "securityInfo":
this.updateRequest(actor, {
securityState: networkInfo.securityInfo,
}).then(() => {
this.webConsoleClient.getSecurityInfo(actor, this.onSecurityInfo);
emit(EVENTS.UPDATING_SECURITY_INFO, actor);
});
break;
case "responseHeaders":
this.webConsoleClient.getResponseHeaders(actor, this.onResponseHeaders);
emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
break;
case "responseCookies":
this.webConsoleClient.getResponseCookies(actor, this.onResponseCookies);
emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
break;
case "responseStart":
this.updateRequest(actor, {
httpVersion: networkInfo.response.httpVersion,
remoteAddress: networkInfo.response.remoteAddress,
remotePort: networkInfo.response.remotePort,
status: networkInfo.response.status,
statusText: networkInfo.response.statusText,
headersSize: networkInfo.response.headersSize
}).then(() => {
emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
});
break;
case "responseContent":
this.webConsoleClient.getResponseContent(actor,
this.onResponseContent.bind(this, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType
}));
emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
break;
case "eventTimings":
this.updateRequest(actor, { totalTime: networkInfo.totalTime })
.then(() => {
this.webConsoleClient.getEventTimings(actor, this.onEventTimings);
emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
});
break;
}
}
/**
* Handles additional information received for a "requestHeaders" packet.
*
* @param {object} response the message received from the server.
*/
onRequestHeaders(response) {
this.updateRequest(response.from, {
requestHeaders: response
}).then(() => {
emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
});
}
/**
* Handles additional information received for a "requestCookies" packet.
*
* @param {object} response the message received from the server.
*/
onRequestCookies(response) {
this.updateRequest(response.from, {
requestCookies: response
}).then(() => {
emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
});
}
/**
* Handles additional information received for a "requestPostData" packet.
*
* @param {object} response the message received from the server.
*/
onRequestPostData(response) {
this.updateRequest(response.from, {
requestPostData: response
}).then(() => {
emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
});
}
/**
* Handles additional information received for a "securityInfo" packet.
*
* @param {object} response the message received from the server.
*/
onSecurityInfo(response) {
this.updateRequest(response.from, {
securityInfo: response.securityInfo
}).then(() => {
emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
});
}
/**
* Handles additional information received for a "responseHeaders" packet.
*
* @param {object} response the message received from the server.
*/
onResponseHeaders(response) {
this.updateRequest(response.from, {
responseHeaders: response
}).then(() => {
emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
});
}
/**
* Handles additional information received for a "responseCookies" packet.
*
* @param {object} response the message received from the server.
*/
onResponseCookies(response) {
this.updateRequest(response.from, {
responseCookies: response
}).then(() => {
emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
});
}
/**
* Handles additional information received for a "responseContent" packet.
*
* @param {object} data the message received from the server event.
* @param {object} response the message received from the server.
*/
onResponseContent(data, response) {
let payload = Object.assign({ responseContent: response }, data);
this.updateRequest(response.from, payload).then(() => {
emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
});
}
/**
* Handles additional information received for a "eventTimings" packet.
*
* @param {object} response the message received from the server.
*/
onEventTimings(response) {
this.updateRequest(response.from, {
eventTimings: response
}).then(() => {
emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
});
}
}
/**
* Guard 'emit' to avoid exception in non-window environment.
*/
function emit(type, data) {
if (typeof window != "undefined") {
window.emit(type, data);
}
}
module.exports = FirefoxDataProvider;

View File

@ -4,5 +4,6 @@
DevToolsModules(
'firefox-connector.js',
'firefox-data-provider.js',
'index.js',
)

View File

@ -94,6 +94,44 @@ const EVENTS = {
CONNECTED: "connected",
};
const UPDATE_PROPS = [
"method",
"url",
"remotePort",
"remoteAddress",
"status",
"statusText",
"httpVersion",
"securityState",
"securityInfo",
"mimeType",
"contentSize",
"transferredSize",
"totalTime",
"eventTimings",
"headersSize",
"customQueryValue",
"requestHeaders",
"requestHeadersFromUploadStream",
"requestCookies",
"requestPostData",
"responseHeaders",
"responseCookies",
"responseContent",
"responseContentDataUri",
"formDataSections",
];
const PANELS = {
COOKIES: "cookies",
HEADERS: "headers",
PARAMS: "params",
RESPONSE: "response",
SECURITY: "security",
STACK_TRACE: "stack-trace",
TIMINGS: "timings",
};
const RESPONSE_HEADERS = [
"Cache-Control",
"Connection",
@ -246,11 +284,13 @@ const general = {
ACTIVITY_TYPE,
EVENTS,
FILTER_SEARCH_DELAY: 200,
UPDATE_PROPS,
HEADERS,
RESPONSE_HEADERS,
FILTER_FLAGS,
SOURCE_EDITOR_SYNTAX_HIGHLIGHT_MAX_SIZE: 51200, // 50 KB in bytes
REQUESTS_WATERFALL,
PANELS,
};
// flatten constants

View File

@ -15,6 +15,7 @@ const {
SELECT_REQUEST,
SEND_CUSTOM_REQUEST,
UPDATE_REQUEST,
UPDATE_PROPS,
} = require("../constants");
const Request = I.Record({
@ -68,34 +69,6 @@ const Requests = I.Record({
lastEndedMillis: -Infinity,
});
const UPDATE_PROPS = [
"method",
"url",
"remotePort",
"remoteAddress",
"status",
"statusText",
"httpVersion",
"securityState",
"securityInfo",
"mimeType",
"contentSize",
"transferredSize",
"totalTime",
"eventTimings",
"headersSize",
"customQueryValue",
"requestHeaders",
"requestHeadersFromUploadStream",
"requestCookies",
"requestPostData",
"responseHeaders",
"responseCookies",
"responseContent",
"responseContentDataUri",
"formDataSections",
];
/**
* Remove the currently selected custom request.
*/

View File

@ -19,6 +19,7 @@ const {
SELECT_REQUEST,
TOGGLE_COLUMN,
WATERFALL_RESIZE,
PANELS,
} = require("../constants");
const cols = {
@ -51,7 +52,7 @@ const Columns = I.Record(
const UI = I.Record({
columns: new Columns(),
detailsPanelSelectedTab: "headers",
detailsPanelSelectedTab: PANELS.HEADERS,
networkDetailsOpen: false,
browserCacheDisabled: Services.prefs.getBoolPref("devtools.cache.disabled"),
statisticsOpen: false,

View File

@ -854,14 +854,43 @@ a.learn-more-link.webconsole-learn-more-link {
background-position: -36px -36px;
}
/* Network Messages */
.message.network .method {
margin-inline-end: 5px;
}
.network.message .network-info {
display: none;
margin-top: 8px;
border: solid 1px var(--theme-splitter-color);
}
.network.message.open .network-info {
display: block;
}
.network.message .network-info .panels {
max-height: 250px;
min-height: 100px;
}
/* Hide 'Edit And Resend' button since the feature isn't
supported in the Console panel. */
.network.message #headers-panel .edit-and-resend-button {
display: none;
}
.network.message #response-panel .treeTable {
overflow-y: hidden;
}
.network .message-flex-body > .message-body {
display: flex;
}
/* Output Wrapper */
.webconsole-output-wrapper .message .indent {
display: inline-block;
border-inline-end: solid 1px var(--theme-splitter-color);

View File

@ -15,6 +15,7 @@ const { batchActions } = require("devtools/client/shared/redux/middleware/deboun
const {
MESSAGE_ADD,
NETWORK_MESSAGE_UPDATE,
NETWORK_UPDATE_REQUEST,
MESSAGES_CLEAR,
MESSAGE_OPEN,
MESSAGE_CLOSE,
@ -94,7 +95,7 @@ function messageTableDataReceive(id, data) {
};
}
function networkMessageUpdate(packet, idGenerator = null) {
function networkMessageUpdate(packet, idGenerator = null, response) {
if (idGenerator == null) {
idGenerator = defaultIdGenerator;
}
@ -104,6 +105,15 @@ function networkMessageUpdate(packet, idGenerator = null) {
return {
type: NETWORK_MESSAGE_UPDATE,
message,
response,
};
}
function networkUpdateRequest(id, data) {
return {
type: NETWORK_UPDATE_REQUEST,
id,
data,
};
}
@ -179,6 +189,7 @@ module.exports = {
messageClose,
messageTableDataGet,
networkMessageUpdate,
networkUpdateRequest,
messageObjectPropertiesLoad,
messageObjectEntriesLoad,
// for test purpose only.

View File

@ -67,7 +67,7 @@ const ConsoleOutput = createClass({
const visibleMessagesDelta =
nextProps.visibleMessages.length - this.props.visibleMessages.length;
const messagesDelta =
nextProps.messages.length - this.props.messages.length;
nextProps.messages.size - this.props.messages.size;
// We need to scroll to the bottom if:
// - the number of messages displayed changed

View File

@ -14,6 +14,8 @@ const {
} = require("devtools/client/shared/vendor/react");
const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
const TabboxPanel = createFactory(require("devtools/client/netmonitor/src/components/tabbox-panel"));
const { PANELS } = require("devtools/client/netmonitor/src/constants");
NetworkEventMessage.displayName = "NetworkEventMessage";
@ -26,13 +28,29 @@ NetworkEventMessage.propTypes = {
networkMessageUpdate: PropTypes.object.isRequired,
};
/**
* This component is responsible for rendering network messages
* in the Console panel.
*
* Network logs are expandable and the user can inspect it inline
* within the Console panel (no need to switch to the Network panel).
*
* HTTP details are rendered using `TabboxPanel` component used to
* render contents of the side bar in the Network panel.
*
* All HTTP details data are fetched from the backend on-demand
* when the user is expanding network log for the first time.
*/
function NetworkEventMessage({
message = {},
serviceContainer,
timestampsVisible,
networkMessageUpdate = {},
dispatch,
open,
}) {
const {
id,
actor,
indent,
source,
@ -77,11 +95,28 @@ function NetworkEventMessage({
const messageBody = [method, xhr, url, statusBody];
// Only render the attachment if the network-event is
// actually opened (performance optimization).
const attachment = open && dom.div({className: "network-info devtools-monospace"},
TabboxPanel({
activeTabId: PANELS.HEADERS,
request: networkMessageUpdate,
sourceMapService: serviceContainer.sourceMapService,
cloneSelectedRequest: () => {},
selectTab: (tabId) => {},
})
);
return Message({
dispatch,
messageId: id,
source,
type,
level,
indent,
collapsible: true,
open,
attachment,
topLevelClasses,
timeStamp,
messageBody,

View File

@ -12,6 +12,7 @@ const actionTypes = {
MESSAGE_OPEN: "MESSAGE_OPEN",
MESSAGE_CLOSE: "MESSAGE_CLOSE",
NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
MESSAGE_OBJECT_PROPERTIES_RECEIVE: "MESSAGE_OBJECT_PROPERTIES_RECEIVE",
MESSAGE_OBJECT_ENTRIES_RECEIVE: "MESSAGE_OBJECT_ENTRIES_RECEIVE",

View File

@ -52,6 +52,11 @@ NewConsoleOutputWrapper.prototype = {
return;
}
// Do not focus if an input field was clicked
if (target.closest("input")) {
return;
}
// Do not focus if something other than the output region was clicked
if (!target.closest(".webconsole-output")) {
return;
@ -219,6 +224,10 @@ NewConsoleOutputWrapper.prototype = {
}
},
dispatchRequestUpdate: function (id, data) {
batchedMessageAdd(actions.networkUpdateRequest(id, data));
},
// Should be used for test purpose only.
getStore: function () {
return store;

View File

@ -22,6 +22,10 @@ const {
const { getGripPreviewItems } = require("devtools/client/shared/components/reps/reps");
const { getSourceNames } = require("devtools/client/shared/source-utils");
const {
UPDATE_PROPS
} = require("devtools/client/netmonitor/src/constants");
const MessageState = Immutable.Record({
// List of all the messages added to the console.
messagesById: Immutable.OrderedMap(),
@ -166,8 +170,10 @@ function messages(state = new MessageState(), action, filtersState, prefsState)
return state.withMutations(function (record) {
record.set("messagesUiById", messagesUiById.push(action.id));
let currMessage = messagesById.get(action.id);
// If the message is a group
if (isGroupType(messagesById.get(action.id).type)) {
if (isGroupType(currMessage.type)) {
// We want to make its children visible
const messagesToShow = [...messagesById].reduce((res, [id, message]) => {
if (
@ -195,6 +201,21 @@ function messages(state = new MessageState(), action, filtersState, prefsState)
...visibleMessages.slice(insertIndex),
]);
}
// If the current message is a network event, mark it as opened-once,
// so HTTP details are not fetched again the next time the user
// opens the log.
if (currMessage.source == "network") {
record.set("messagesById",
messagesById.set(
action.id, Object.assign({},
currMessage, {
openedOnce: true
}
)
)
);
}
});
case constants.MESSAGE_CLOSE:
@ -250,6 +271,44 @@ function messages(state = new MessageState(), action, filtersState, prefsState)
})
);
case constants.NETWORK_UPDATE_REQUEST: {
let request = networkMessagesUpdateById[action.id];
if (!request) {
return state;
}
let values = {};
for (let [key, value] of Object.entries(action.data)) {
if (UPDATE_PROPS.includes(key)) {
values[key] = value;
switch (key) {
case "securityInfo":
values.securityState = value.state;
break;
case "totalTime":
values.totalTime = request.totalTime;
break;
case "requestPostData":
values.requestHeadersFromUploadStream = {
headers: [],
headersSize: 0,
};
break;
}
}
}
let newState = state.set(
"networkMessagesUpdateById",
Object.assign({}, networkMessagesUpdateById, {
[action.id]: Object.assign({}, request, values)
})
);
return newState;
}
case constants.REMOVED_ACTORS_CLEAR:
return state.set("removedActors", []);

View File

@ -17,13 +17,24 @@ const {
} = require("devtools/client/shared/redux/middleware/debounce");
const {
MESSAGE_ADD,
MESSAGE_OPEN,
MESSAGES_CLEAR,
REMOVED_ACTORS_CLEAR,
NETWORK_MESSAGE_UPDATE,
PREFS,
} = require("devtools/client/webconsole/new-console-output/constants");
const { reducers } = require("./reducers/index");
const Services = require("Services");
const {
getMessage,
getAllMessagesUiById,
} = require("devtools/client/webconsole/new-console-output/selectors/messages");
const DataProvider = require("devtools/client/netmonitor/src/connector/firefox-data-provider");
/**
* Create and configure store for the Console panel. This is the place
* where various enhancers and middleware can be registered.
*/
function configureStore(hud, options = {}) {
const logLimit = options.logLimit
|| Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1);
@ -48,7 +59,12 @@ function configureStore(hud, options = {}) {
return createStore(
createRootReducer(),
initialState,
compose(applyMiddleware(thunk), enableActorReleaser(hud), enableBatching())
compose(
applyMiddleware(thunk),
enableActorReleaser(hud),
enableBatching(),
enableNetProvider(hud)
)
);
}
@ -125,6 +141,69 @@ function enableActorReleaser(hud) {
};
}
/**
* This enhancer is responsible for fetching HTTP details data
* collected by the backend. The fetch happens on-demand
* when the user expands network log in order to inspect it.
*
* This way we don't slow down the Console logging by fetching.
* unnecessary data over RDP.
*/
function enableNetProvider(hud) {
let dataProvider;
return next => (reducer, initialState, enhancer) => {
function netProviderEnhancer(state, action) {
let proxy = hud ? hud.proxy : null;
if (!proxy) {
return reducer(state, action);
}
let actions = {
updateRequest: (id, data, batch) => {
proxy.dispatchRequestUpdate(id, data);
}
};
// Data provider implements async logic for fetching
// data from the backend. It's created the first
// time it's needed.
if (!dataProvider) {
dataProvider = new DataProvider({
actions,
webConsoleClient: proxy.webConsoleClient
});
}
let type = action.type;
// If network message has been opened, fetch all
// HTTP details from the backend.
if (type == MESSAGE_OPEN) {
let message = getMessage(state, action.id);
if (!message.openedOnce && message.source == "network") {
message.updates.forEach(updateType => {
dataProvider.onNetworkEventUpdate(null, {
packet: { updateType: updateType },
networkInfo: message,
});
});
}
}
// Process all incoming HTTP details packets.
if (type == NETWORK_MESSAGE_UPDATE) {
let open = getAllMessagesUiById(state).includes(action.id);
if (open) {
dataProvider.onNetworkEventUpdate(null, action.response);
}
}
return reducer(state, action);
}
return next(netProviderEnhancer, initialState, enhancer);
};
}
/**
* Helper function for releasing backend actors.
*/

View File

@ -60,5 +60,10 @@ exports.NetworkEventMessage = function (props) {
timeStamp: null,
totalTime: null,
indent: 0,
updates: null,
openedOnce: false,
securityState: null,
securityInfo: null,
requestHeadersFromUploadStream: null,
}, props);
};

View File

@ -7,6 +7,7 @@
"use strict";
const l10n = require("devtools/client/webconsole/webconsole-l10n");
const { getUrlDetails } = require("devtools/client/netmonitor/src/utils/request-utils");
const {
MESSAGE_SOURCE,
@ -232,6 +233,11 @@ function transformNetworkEventPacket(packet) {
response: networkEvent.response,
timeStamp: networkEvent.timeStamp,
totalTime: networkEvent.totalTime,
url: networkEvent.request.url,
urlDetails: getUrlDetails(networkEvent.request.url),
method: networkEvent.request.method,
updates: networkEvent.updates,
cause: networkEvent.cause,
});
}

View File

@ -88,7 +88,7 @@ WebConsolePanel.prototype = {
let msg = "WebConsolePanel open failed. " +
reason.error + ": " + reason.message;
dump(msg + "\n");
console.error(msg);
console.error(msg, reason);
});
},

View File

@ -243,6 +243,10 @@ WebConsoleConnectionProxy.prototype = {
this.webConsoleFrame.newConsoleOutput.dispatchMessageUpdate(networkInfo, response);
},
dispatchRequestUpdate: function (id, data) {
this.webConsoleFrame.newConsoleOutput.dispatchRequestUpdate(id, data);
},
/**
* The "cachedMessages" response handler.
*

View File

@ -11,6 +11,10 @@
<link rel="stylesheet" href="chrome://devtools/skin/webconsole.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/components-frame.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/reps/reps.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabs.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabbar.css"/>
<link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/netmonitor.css"/>
<script src="chrome://devtools/content/shared/theme-switching.js"></script>
<script type="application/javascript"
src="resource://devtools/client/webconsole/new-console-output/main.js"></script>