Files
firefox/devtools/client/webconsole/components/Output/Message.js
T
Nicolas Chevobbe 468323a509 Bug 1888645 - [devtools] Fix SmartTrace crash with wasm frames. r=devtools-reviewers,bomsy.
In some cases, we were getting null sourceId, which was causing an exception
to be thrown in `getFormattedSourceId` since it expects a string.
We add a simple guard in this function so we don't throw anymore.

But we shouldn't get null sourceId in the first place. It looks like for
some Wasm sources, the SavedFrame sourceId can be 0, which wasn't playing
well with some loose check we were doing in the sources manager.

Finally, the "Copy metadata" button we display when a console message crash
wasn't working. The message now includes the targetFront, which can be serialized
in JSON, so we simply ignore it.

Differential Revision: https://phabricator.services.mozilla.com/D206105
2024-05-13 10:17:15 +00:00

487 lines
13 KiB
JavaScript

/* 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";
// React & Redux
const {
Component,
createFactory,
createElement,
} = require("resource://devtools/client/shared/vendor/react.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const {
l10n,
} = require("resource://devtools/client/webconsole/utils/messages.js");
const actions = require("resource://devtools/client/webconsole/actions/index.js");
const {
MESSAGE_LEVEL,
MESSAGE_SOURCE,
MESSAGE_TYPE,
} = require("resource://devtools/client/webconsole/constants.js");
const {
MessageIndent,
} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
const MessageIcon = require("resource://devtools/client/webconsole/components/Output/MessageIcon.js");
const FrameView = createFactory(
require("resource://devtools/client/shared/components/Frame.js")
);
loader.lazyRequireGetter(
this,
"CollapseButton",
"resource://devtools/client/webconsole/components/Output/CollapseButton.js"
);
loader.lazyRequireGetter(
this,
"MessageRepeat",
"resource://devtools/client/webconsole/components/Output/MessageRepeat.js"
);
loader.lazyRequireGetter(
this,
"PropTypes",
"resource://devtools/client/shared/vendor/react-prop-types.js"
);
loader.lazyRequireGetter(
this,
"SmartTrace",
"resource://devtools/client/shared/components/SmartTrace.js"
);
class Message extends Component {
static get propTypes() {
return {
open: PropTypes.bool,
collapsible: PropTypes.bool,
collapseTitle: PropTypes.string,
disabled: PropTypes.bool,
onToggle: PropTypes.func,
source: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
indent: PropTypes.number.isRequired,
inWarningGroup: PropTypes.bool,
isBlockedNetworkMessage: PropTypes.bool,
topLevelClasses: PropTypes.array.isRequired,
messageBody: PropTypes.any.isRequired,
repeat: PropTypes.any,
frame: PropTypes.any,
attachment: PropTypes.any,
stacktrace: PropTypes.any,
messageId: PropTypes.string,
scrollToMessage: PropTypes.bool,
exceptionDocURL: PropTypes.string,
request: PropTypes.object,
dispatch: PropTypes.func,
timeStamp: PropTypes.number,
timestampsVisible: PropTypes.bool.isRequired,
serviceContainer: PropTypes.shape({
emitForTests: PropTypes.func.isRequired,
onViewSource: PropTypes.func.isRequired,
onViewSourceInDebugger: PropTypes.func,
onViewSourceInStyleEditor: PropTypes.func,
openContextMenu: PropTypes.func.isRequired,
openLink: PropTypes.func.isRequired,
sourceMapURLService: PropTypes.any,
preventStacktraceInitialRenderDelay: PropTypes.bool,
}),
notes: PropTypes.arrayOf(
PropTypes.shape({
messageBody: PropTypes.string.isRequired,
frame: PropTypes.any,
})
),
maybeScrollToBottom: PropTypes.func,
message: PropTypes.object.isRequired,
};
}
static get defaultProps() {
return {
indent: 0,
};
}
constructor(props) {
super(props);
this.onLearnMoreClick = this.onLearnMoreClick.bind(this);
this.toggleMessage = this.toggleMessage.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this.renderIcon = this.renderIcon.bind(this);
}
componentDidMount() {
if (this.messageNode) {
if (this.props.scrollToMessage) {
this.messageNode.scrollIntoView();
}
this.emitNewMessage(this.messageNode);
}
}
componentDidCatch(e) {
this.setState({ error: e });
}
// Event used in tests. Some message types don't pass it in because existing tests
// did not emit for them.
emitNewMessage(node) {
const { serviceContainer, messageId, timeStamp } = this.props;
serviceContainer.emitForTests(
"new-messages",
new Set([{ node, messageId, timeStamp }])
);
}
onLearnMoreClick(e) {
const { exceptionDocURL } = this.props;
this.props.serviceContainer.openLink(exceptionDocURL, e);
e.preventDefault();
}
toggleMessage(e) {
// Don't bubble up to the main App component, which redirects focus to input,
// making difficult for screen reader users to review output
e.stopPropagation();
const { open, dispatch, messageId, onToggle, disabled } = this.props;
if (disabled) {
return;
}
// Early exit the function to avoid the message to collapse if the user is
// selecting a range in the toggle message.
const window = e.target.ownerDocument.defaultView;
if (window.getSelection && window.getSelection().type === "Range") {
return;
}
// If defined on props, we let the onToggle() method handle the toggling,
// otherwise we toggle the message open/closed ourselves.
if (onToggle) {
onToggle(messageId, e);
} else if (open) {
dispatch(actions.messageClose(messageId));
} else {
dispatch(actions.messageOpen(messageId));
}
}
onContextMenu(e) {
const { serviceContainer, source, request, messageId } = this.props;
const messageInfo = {
source,
request,
messageId,
};
serviceContainer.openContextMenu(e, messageInfo);
e.stopPropagation();
e.preventDefault();
}
renderIcon() {
const { level, inWarningGroup, isBlockedNetworkMessage, type, disabled } =
this.props;
if (inWarningGroup) {
return undefined;
}
if (disabled) {
return MessageIcon({
level: MESSAGE_LEVEL.INFO,
type,
title: l10n.getStr("webconsole.disableIcon.title"),
});
}
if (isBlockedNetworkMessage) {
return MessageIcon({
level: MESSAGE_LEVEL.ERROR,
type: "blockedReason",
});
}
return MessageIcon({
level,
type,
});
}
renderTimestamp() {
if (!this.props.timestampsVisible) {
return null;
}
return dom.span(
{
className: "timestamp devtools-monospace",
},
l10n.timestampString(this.props.timeStamp || Date.now())
);
}
renderErrorState() {
const newBugUrl =
"https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component=Console";
const timestampEl = this.renderTimestamp();
return dom.div(
{
className: "message error message-did-catch",
},
timestampEl,
MessageIcon({ level: "error" }),
dom.span(
{ className: "message-body-wrapper" },
dom.span(
{
className: "message-flex-body",
},
// Add whitespaces for formatting when copying to the clipboard.
timestampEl ? " " : null,
dom.span(
{ className: "message-body devtools-monospace" },
l10n.getFormatStr("webconsole.message.componentDidCatch.label", [
newBugUrl,
]),
dom.button(
{
className: "devtools-button",
onClick: () =>
navigator.clipboard.writeText(
JSON.stringify(
this.props.message,
function (key, value) {
if (key === "targetFront") {
return null;
}
// The message can hold one or multiple fronts that we need to serialize
if (value?.getGrip) {
return value.getGrip();
}
return value;
},
2
)
),
},
l10n.getStr(
"webconsole.message.componentDidCatch.copyButton.label"
)
)
)
)
),
dom.br()
);
}
// eslint-disable-next-line complexity
render() {
if (this.state && this.state.error) {
return this.renderErrorState();
}
const {
open,
collapsible,
collapseTitle,
disabled,
source,
type,
level,
indent,
inWarningGroup,
topLevelClasses,
messageBody,
frame,
stacktrace,
serviceContainer,
exceptionDocURL,
messageId,
notes,
} = this.props;
topLevelClasses.push("message", source, type, level);
if (open) {
topLevelClasses.push("open");
}
if (disabled) {
topLevelClasses.push("disabled");
}
const timestampEl = this.renderTimestamp();
const icon = this.renderIcon();
// Figure out if there is an expandable part to the message.
let attachment = null;
if (this.props.attachment) {
attachment = this.props.attachment;
} else if (stacktrace && open) {
const smartTraceAttributes = {
stacktrace,
onViewSourceInDebugger:
serviceContainer.onViewSourceInDebugger ||
serviceContainer.onViewSource,
onViewSource: serviceContainer.onViewSource,
onReady: this.props.maybeScrollToBottom,
sourceMapURLService: serviceContainer.sourceMapURLService,
};
if (serviceContainer.preventStacktraceInitialRenderDelay) {
smartTraceAttributes.initialRenderDelay = 0;
}
attachment = dom.div(
{
className: "stacktrace devtools-monospace",
},
createElement(SmartTrace, smartTraceAttributes)
);
}
// If there is an expandable part, make it collapsible.
let collapse = null;
if (collapsible && !disabled) {
collapse = createElement(CollapseButton, {
open,
title: collapseTitle,
onClick: this.toggleMessage,
});
}
let notesNodes;
if (notes) {
notesNodes = notes.map(note =>
dom.span(
{ className: "message-flex-body error-note" },
dom.span(
{ className: "message-body devtools-monospace" },
"note: " + note.messageBody
),
dom.span(
{ className: "message-location devtools-monospace" },
note.frame
? FrameView({
frame: note.frame,
onClick: serviceContainer
? serviceContainer.onViewSourceInDebugger ||
serviceContainer.onViewSource
: undefined,
showEmptyPathAsHost: true,
sourceMapURLService: serviceContainer
? serviceContainer.sourceMapURLService
: undefined,
})
: null
)
)
);
} else {
notesNodes = [];
}
const repeat =
this.props.repeat && this.props.repeat > 1
? createElement(MessageRepeat, { repeat: this.props.repeat })
: null;
let onFrameClick;
if (serviceContainer && frame) {
if (source === MESSAGE_SOURCE.CSS) {
onFrameClick =
serviceContainer.onViewSourceInStyleEditor ||
serviceContainer.onViewSource;
} else {
// Point everything else to debugger, if source not available,
// it will fall back to view-source.
onFrameClick =
serviceContainer.onViewSourceInDebugger ||
serviceContainer.onViewSource;
}
}
// Configure the location.
const location = frame
? FrameView({
className: "message-location devtools-monospace",
frame,
onClick: onFrameClick,
showEmptyPathAsHost: true,
sourceMapURLService: serviceContainer
? serviceContainer.sourceMapURLService
: undefined,
messageSource: source,
})
: null;
let learnMore;
if (exceptionDocURL) {
learnMore = dom.a(
{
className: "learn-more-link webconsole-learn-more-link",
href: exceptionDocURL,
title: exceptionDocURL.split("?")[0],
onClick: this.onLearnMoreClick,
},
`[${l10n.getStr("webConsoleMoreInfoLabel")}]`
);
}
const bodyElements = Array.isArray(messageBody)
? messageBody
: [messageBody];
return dom.div(
{
className: topLevelClasses.join(" "),
onContextMenu: this.onContextMenu,
ref: node => {
this.messageNode = node;
},
"data-message-id": messageId,
"data-indent": indent || 0,
"aria-live": type === MESSAGE_TYPE.COMMAND ? "off" : "polite",
},
timestampEl,
MessageIndent({
indent,
inWarningGroup,
}),
this.props.isBlockedNetworkMessage ? collapse : icon,
this.props.isBlockedNetworkMessage ? icon : collapse,
dom.span(
{ className: "message-body-wrapper" },
dom.span(
{
className: "message-flex-body",
onClick: collapsible ? this.toggleMessage : undefined,
},
// Add whitespaces for formatting when copying to the clipboard.
timestampEl ? " " : null,
dom.span(
{ className: "message-body devtools-monospace" },
...bodyElements,
learnMore
),
repeat ? " " : null,
repeat,
" ",
location
),
attachment,
...notesNodes
),
// If an attachment is displayed, the final newline is handled by the attachment.
attachment ? null : dom.br()
);
}
}
module.exports = Message;