--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -448,16 +448,20 @@ netmonitor.tab.params=Params
# LOCALIZATION NOTE (netmonitor.tab.response): This is the label displayed
# in the network details pane identifying the response tab.
netmonitor.tab.response=Response
# LOCALIZATION NOTE (netmonitor.tab.timings): This is the label displayed
# in the network details pane identifying the timings tab.
netmonitor.tab.timings=Timings
+# LOCALIZATION NOTE (netmonitor.tab.stackTrace): This is the label displayed
+# in the network details pane identifying the stack-trace tab.
+netmonitor.tab.stackTrace=Stack Trace
+
# LOCALIZATION NOTE (netmonitor.tab.preview): This is the label displayed
# in the network details pane identifying the preview tab.
netmonitor.tab.preview=Preview
# LOCALIZATION NOTE (netmonitor.tab.security): This is the label displayed
# in the network details pane identifying the security tab.
netmonitor.tab.security=Security
--- a/devtools/client/netmonitor/src/assets/styles/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -1,16 +1,17 @@
/* 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/. */
@import "resource://devtools/client/shared/components/splitter/split-box.css";
@import "resource://devtools/client/shared/components/tree/tree-view.css";
@import "resource://devtools/client/shared/components/tabs/tabs.css";
@import "resource://devtools/client/shared/components/tabs/tabbar.css";
+@import "chrome://devtools/skin/components-frame.css";
* {
box-sizing: border-box;
}
.toolbar-labels {
overflow: hidden;
display: flex;
@@ -741,16 +742,49 @@
min-width: 1px;
transition: width 0.2s ease-out;
}
.theme-firebug .requests-list-timings-total {
color: var(--theme-body-color);
}
+/* Stack trace panel */
+
+.stack-trace {
+ font-family: var(--monospace-font-family);
+ /* The markup contains extra whitespace to improve formatting of clipboard text.
+ Make sure this whitespace doesn't affect the HTML rendering */
+ white-space: normal;
+ padding: 5px;
+}
+
+.stack-trace .frame-link-source,
+.message-location .frame-link-source {
+ /* Makes the file name truncated (and ellipsis shown) on the left side */
+ direction: rtl;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.stack-trace .frame-link-source-inner,
+.message-location .frame-link-source-inner {
+ /* Enforce LTR direction for the file name - fixes bug 1290056 */
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+.stack-trace .frame-link-function-display-name {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-inline-end: 1ch;
+}
+
/* Security tabpanel */
/* Overwrite tree-view cell colon `:` for security panel and tree section */
.security-panel .treeTable .treeLabelCell::after,
.treeTable .tree-section .treeLabelCell::after {
content: "";
}
--- a/devtools/client/netmonitor/src/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -16,13 +16,14 @@ DevToolsModules(
'properties-view.js',
'request-list-content.js',
'request-list-empty-notice.js',
'request-list-header.js',
'request-list-item.js',
'request-list.js',
'response-panel.js',
'security-panel.js',
+ 'stack-trace-panel.js',
'statistics-panel.js',
'tabbox-panel.js',
'timings-panel.js',
'toolbar.js',
)
--- a/devtools/client/netmonitor/src/components/request-list-content.js
+++ b/devtools/client/netmonitor/src/components/request-list-content.js
@@ -8,20 +8,17 @@ const {
createClass,
createFactory,
DOM,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
const Actions = require("../actions/index");
-const {
- setTooltipImageContent,
- setTooltipStackTraceContent,
-} = require("../request-list-tooltip");
+const { setTooltipImageContent } = require("../request-list-tooltip");
const {
getDisplayedRequests,
getWaterfallScale,
} = require("../selectors/index");
// Components
const RequestListItem = createFactory(require("./request-list-item"));
const RequestListContextMenu = require("../request-list-context-menu");
@@ -37,16 +34,17 @@ const REQUESTS_TOOLTIP_TOGGLE_DELAY = 50
const RequestListContent = createClass({
displayName: "RequestListContent",
propTypes: {
dispatch: PropTypes.func.isRequired,
displayedRequests: PropTypes.object.isRequired,
firstRequestStartedMillis: PropTypes.number.isRequired,
fromCache: PropTypes.bool.isRequired,
+ onCauseBadgeClick: PropTypes.func.isRequired,
onItemMouseDown: PropTypes.func.isRequired,
onSecurityIconClick: PropTypes.func.isRequired,
onSelectDelta: PropTypes.func.isRequired,
scale: PropTypes.number,
selectedRequestId: PropTypes.string,
},
componentWillMount() {
@@ -152,18 +150,16 @@ const RequestListContent = createClass({
}
let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
if (!requestItem) {
return false;
}
if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
return setTooltipImageContent(tooltip, itemEl, requestItem);
- } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
- return setTooltipStackTraceContent(tooltip, requestItem);
}
return false;
},
/**
* Scroll listener for the requests menu view.
*/
@@ -222,16 +218,17 @@ const RequestListContent = createClass({
this.shouldScrollBottom = false;
},
render() {
const {
displayedRequests,
firstRequestStartedMillis,
selectedRequestId,
+ onCauseBadgeClick,
onItemMouseDown,
onSecurityIconClick,
} = this.props;
return (
div({
ref: "contentEl",
className: "requests-list-contents",
@@ -243,16 +240,17 @@ const RequestListContent = createClass({
fromCache: item.status === "304" || item.fromCache,
item,
index,
isSelected: item.id === selectedRequestId,
key: item.id,
onContextMenu: this.onContextMenu,
onFocusedNodeChange: this.onFocusedNodeChange,
onMouseDown: () => onItemMouseDown(item.id),
+ onCauseBadgeClick: () => onCauseBadgeClick(item.cause),
onSecurityIconClick: () => onSecurityIconClick(item.securityState),
}))
)
);
},
});
module.exports = connect(
@@ -269,11 +267,19 @@ module.exports = connect(
* A handler that opens the security tab in the details view if secure or
* broken security indicator is clicked.
*/
onSecurityIconClick: (securityState) => {
if (securityState && securityState !== "insecure") {
dispatch(Actions.selectDetailsPanelTab("security"));
}
},
+ /**
+ * A handler that opens the stack trace tab when a stack trace is available
+ */
+ onCauseBadgeClick: (cause) => {
+ if (cause.stacktrace && cause.stacktrace.length > 0) {
+ dispatch(Actions.selectDetailsPanelTab("stack-trace"));
+ }
+ },
onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
}),
)(RequestListContent);
--- a/devtools/client/netmonitor/src/components/request-list-item.js
+++ b/devtools/client/netmonitor/src/components/request-list-item.js
@@ -61,16 +61,17 @@ const RequestListItem = createClass({
displayName: "RequestListItem",
propTypes: {
item: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
isSelected: PropTypes.bool.isRequired,
firstRequestStartedMillis: PropTypes.number.isRequired,
fromCache: PropTypes.bool.isRequired,
+ onCauseBadgeClick: PropTypes.func.isRequired,
onContextMenu: PropTypes.func.isRequired,
onFocusedNodeChange: PropTypes.func,
onMouseDown: PropTypes.func.isRequired,
onSecurityIconClick: PropTypes.func.isRequired,
},
componentDidMount() {
if (this.props.isSelected) {
@@ -96,16 +97,17 @@ const RequestListItem = createClass({
const {
item,
index,
isSelected,
firstRequestStartedMillis,
fromCache,
onContextMenu,
onMouseDown,
+ onCauseBadgeClick,
onSecurityIconClick
} = this.props;
let classList = ["request-list-item"];
if (isSelected) {
classList.push("selected");
}
@@ -123,17 +125,17 @@ const RequestListItem = createClass({
tabIndex: 0,
onContextMenu,
onMouseDown,
},
StatusColumn({ item }),
MethodColumn({ item }),
FileColumn({ item }),
DomainColumn({ item, onSecurityIconClick }),
- CauseColumn({ item }),
+ CauseColumn({ item, onCauseBadgeClick }),
TypeColumn({ item }),
TransferredSizeColumn({ item }),
ContentSizeColumn({ item }),
WaterfallColumn({ item, firstRequestStartedMillis }),
)
);
}
});
@@ -296,24 +298,30 @@ const DomainColumn = createFactory(creat
}
}));
const CauseColumn = createFactory(createClass({
displayName: "CauseColumn",
propTypes: {
item: PropTypes.object.isRequired,
+ onCauseBadgeClick: PropTypes.func.isRequired,
},
shouldComponentUpdate(nextProps) {
return this.props.item.cause !== nextProps.item.cause;
},
render() {
- const { cause } = this.props.item;
+ const {
+ item,
+ onCauseBadgeClick,
+ } = this.props;
+
+ const { cause } = item;
let causeType = "";
let causeUri = undefined;
let causeHasStack = false;
if (cause) {
// Legacy server might send a numeric value. Display it as "unknown"
causeType = typeof cause.type === "string" ? cause.type : "unknown";
@@ -324,16 +332,17 @@ const CauseColumn = createFactory(create
return (
div({
className: "requests-list-subitem requests-list-cause",
title: causeUri,
},
span({
className: "requests-list-cause-stack",
hidden: !causeHasStack,
+ onClick: onCauseBadgeClick,
}, "JS"),
span({ className: "subitem-label" }, causeType),
)
);
}
}));
const CONTENT_MIME_TYPE_ABBREVIATIONS = {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/stack-trace-panel.js
@@ -0,0 +1,39 @@
+/* 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 {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { div } = DOM;
+
+// Components
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+function StackTracePanel({ request }) {
+ let { stacktrace } = request.cause;
+
+ return (
+ div({ className: "panel-container" },
+ StackTrace({
+ stacktrace,
+ onViewSourceInDebugger: (name, line) => {
+ window.NetMonitorController.viewSourceInDebugger(name, line);
+ },
+ }),
+ )
+ );
+}
+
+StackTracePanel.displayName = "StackTracePanel";
+
+StackTracePanel.propTypes = {
+ request: PropTypes.object.isRequired,
+};
+
+module.exports = StackTracePanel;
--- a/devtools/client/netmonitor/src/components/tabbox-panel.js
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.js
@@ -18,25 +18,27 @@ const { getSelectedRequest } = require("
const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
const TabPanel = createFactory(require("devtools/client/shared/components/tabs/tabs").TabPanel);
const CookiesPanel = createFactory(require("./cookies-panel"));
const HeadersPanel = createFactory(require("./headers-panel"));
const ParamsPanel = createFactory(require("./params-panel"));
const PreviewPanel = createFactory(require("./preview-panel"));
const ResponsePanel = createFactory(require("./response-panel"));
const SecurityPanel = createFactory(require("./security-panel"));
+const StackTracePanel = createFactory(require("./stack-trace-panel"));
const TimingsPanel = createFactory(require("./timings-panel"));
+const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
-const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
+const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
+const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
+const STACK_TRACE_TITLE = L10N.getStr("netmonitor.tab.stackTrace");
const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
-const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
-const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
/*
* Tabbox panel component
* Display the network request details
*/
function TabboxPanel({
activeTabId,
cloneSelectedRequest,
@@ -79,16 +81,23 @@ function TabboxPanel({
ResponsePanel({ request }),
),
TabPanel({
id: "timings",
title: TIMINGS_TITLE,
},
TimingsPanel({ request }),
),
+ request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
+ TabPanel({
+ id: "stack-trace",
+ title: STACK_TRACE_TITLE,
+ },
+ StackTracePanel({ request }),
+ ),
request.securityState && request.securityState !== "insecure" &&
TabPanel({
id: "security",
title: SECURITY_TITLE,
},
SecurityPanel({ request }),
),
Filters.html(request) &&
--- a/devtools/client/netmonitor/src/request-list-tooltip.js
+++ b/devtools/client/netmonitor/src/request-list-tooltip.js
@@ -4,23 +4,19 @@
"use strict";
const {
setImageTooltip,
getImageDimensions,
} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
const { getLongString } = require("./utils/client");
-const { WEBCONSOLE_L10N } = require("./utils/l10n");
const { formDataURI } = require("./utils/request-utils");
const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
-const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600; // px
-
-const HTML_NS = "http://www.w3.org/1999/xhtml";
async function setTooltipImageContent(tooltip, itemEl, requestItem) {
let { mimeType, text, encoding } = requestItem.responseContent.content;
if (!mimeType || !mimeType.includes("image/")) {
return false;
}
@@ -29,77 +25,11 @@ async function setTooltipImageContent(to
let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
let options = { maxDim, naturalWidth, naturalHeight };
setImageTooltip(tooltip, tooltip.doc, src, options);
return itemEl.querySelector(".requests-list-icon");
}
-async function setTooltipStackTraceContent(tooltip, requestItem) {
- let {stacktrace} = requestItem.cause;
-
- if (!stacktrace || stacktrace.length == 0) {
- return false;
- }
-
- let doc = tooltip.doc;
- let el = doc.createElementNS(HTML_NS, "div");
- el.className = "stack-trace-tooltip devtools-monospace";
-
- for (let f of stacktrace) {
- let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
-
- if (asyncCause) {
- // if there is asyncCause, append a "divider" row into the trace
- let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
- asyncFrameEl.className = "stack-frame stack-frame-async";
- asyncFrameEl.textContent =
- WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
- el.appendChild(asyncFrameEl);
- }
-
- // Parse a source name in format "url -> url"
- let sourceUrl = filename.split(" -> ").pop();
-
- let frameEl = doc.createElementNS(HTML_NS, "div");
- frameEl.className = "stack-frame stack-frame-call";
-
- let funcEl = doc.createElementNS(HTML_NS, "span");
- funcEl.className = "stack-frame-function-name";
- funcEl.textContent =
- functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
- frameEl.appendChild(funcEl);
-
- let sourceEl = doc.createElementNS(HTML_NS, "span");
- sourceEl.className = "stack-frame-source-name";
- frameEl.appendChild(sourceEl);
-
- let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
- sourceInnerEl.className = "stack-frame-source-name-inner";
- sourceEl.appendChild(sourceInnerEl);
-
- sourceInnerEl.textContent = sourceUrl;
- sourceInnerEl.title = sourceUrl;
-
- let lineEl = doc.createElementNS(HTML_NS, "span");
- lineEl.className = "stack-frame-line";
- lineEl.textContent = `:${lineNumber}:${columnNumber}`;
- sourceInnerEl.appendChild(lineEl);
-
- frameEl.addEventListener("click", () => {
- // hide the tooltip immediately, not after delay
- tooltip.hide();
- window.NetMonitorController.viewSourceInDebugger(filename, lineNumber);
- });
-
- el.appendChild(frameEl);
- }
-
- tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
-
- return true;
-}
-
module.exports = {
setTooltipImageContent,
- setTooltipStackTraceContent,
};