Log TimelinePanel
debugging info when opening the bug report modal (#8502)
To debug all of the timeline problems: - https://github.com/vector-im/element-web/issues/21613 - https://github.com/vector-im/element-web/issues/21922 - https://github.com/vector-im/element-web/issues/21432 - https://github.com/vector-im/element-web/issues/21533
This commit is contained in:
parent
d5b363e971
commit
3a241e0dfb
4 changed files with 114 additions and 1 deletions
|
@ -371,6 +371,69 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs out debug info to describe the state of the TimelinePanel and the
|
||||||
|
* events in the room according to the matrix-js-sdk. This is useful when
|
||||||
|
* debugging problems like messages out of order, or messages that should
|
||||||
|
* not be showing up in a thread, etc.
|
||||||
|
*
|
||||||
|
* It's too expensive and cumbersome to do all of these calculations for
|
||||||
|
* every message change so instead we only log it out when asked.
|
||||||
|
*/
|
||||||
|
private onDumpDebugLogs = (): void => {
|
||||||
|
const roomId = this.props.timelineSet.room?.roomId;
|
||||||
|
// Get a list of the event IDs used in this TimelinePanel.
|
||||||
|
// This includes state and hidden events which we don't render
|
||||||
|
const eventIdList = this.state.events.map((ev) => ev.getId());
|
||||||
|
|
||||||
|
// Get the list of actually rendered events seen in the DOM.
|
||||||
|
// This is useful to know for sure what's being shown on screen.
|
||||||
|
// And we can suss out any corrupted React `key` problems.
|
||||||
|
let renderedEventIds: string[];
|
||||||
|
const messagePanel = this.messagePanel.current;
|
||||||
|
if (messagePanel) {
|
||||||
|
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
|
||||||
|
if (messagePanelNode) {
|
||||||
|
const actuallyRenderedEvents = messagePanelNode.querySelectorAll('[data-event-id]');
|
||||||
|
renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
|
||||||
|
return renderedEvent.getAttribute('data-event-id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list of events and threads for the room as seen by the
|
||||||
|
// matrix-js-sdk.
|
||||||
|
let serializedEventIdsFromTimelineSets: { [key: string]: string[] }[];
|
||||||
|
let serializedEventIdsFromThreadsTimelineSets: { [key: string]: string[] }[];
|
||||||
|
const serializedThreadsMap: { [key: string]: string[] } = {};
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const room = client?.getRoom(roomId);
|
||||||
|
if (room) {
|
||||||
|
const timelineSets = room.getTimelineSets();
|
||||||
|
const threadsTimelineSets = room.threadsTimelineSets;
|
||||||
|
|
||||||
|
// Serialize all of the timelineSets and timelines in each set to their event IDs
|
||||||
|
serializedEventIdsFromTimelineSets = serializeEventIdsFromTimelineSets(timelineSets);
|
||||||
|
serializedEventIdsFromThreadsTimelineSets = serializeEventIdsFromTimelineSets(threadsTimelineSets);
|
||||||
|
|
||||||
|
// Serialize all threads in the room from theadId -> event IDs in the thread
|
||||||
|
room.getThreads().forEach((thread) => {
|
||||||
|
serializedThreadsMap[thread.id] = thread.events.map(ev => ev.getId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`TimelinePanel(${this.context.timelineRenderingType}): Debugging info for ${roomId}\n` +
|
||||||
|
`\tevents(${eventIdList.length})=${JSON.stringify(eventIdList)}\n` +
|
||||||
|
`\trenderedEventIds(${renderedEventIds ? renderedEventIds.length : 0})=` +
|
||||||
|
`${JSON.stringify(renderedEventIds)}\n` +
|
||||||
|
`\tserializedEventIdsFromTimelineSets=${JSON.stringify(serializedEventIdsFromTimelineSets)}\n` +
|
||||||
|
`\tserializedEventIdsFromThreadsTimelineSets=` +
|
||||||
|
`${JSON.stringify(serializedEventIdsFromThreadsTimelineSets)}\n` +
|
||||||
|
`\tserializedThreadsMap=${JSON.stringify(serializedThreadsMap)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
|
private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
|
||||||
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
|
@ -528,6 +591,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
case "ignore_state_changed":
|
case "ignore_state_changed":
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
break;
|
break;
|
||||||
|
case Action.DumpDebugLogs:
|
||||||
|
this.onDumpDebugLogs();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1464,7 +1530,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
const messagePanel = this.messagePanel.current;
|
const messagePanel = this.messagePanel.current;
|
||||||
if (!messagePanel) return null;
|
if (!messagePanel) return null;
|
||||||
|
|
||||||
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as HTMLElement;
|
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
|
||||||
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
|
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
|
||||||
const wrapperRect = messagePanelNode.getBoundingClientRect();
|
const wrapperRect = messagePanelNode.getBoundingClientRect();
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
@ -1686,4 +1752,30 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate across all of the timelineSets and timelines inside to expose all of
|
||||||
|
* the event IDs contained inside.
|
||||||
|
*
|
||||||
|
* @return An event ID list for every timeline in every timelineSet
|
||||||
|
*/
|
||||||
|
function serializeEventIdsFromTimelineSets(timelineSets): { [key: string]: string[] }[] {
|
||||||
|
const serializedEventIdsInTimelineSet = timelineSets.map((timelineSet) => {
|
||||||
|
const timelineMap = {};
|
||||||
|
|
||||||
|
const timelines = timelineSet.getTimelines();
|
||||||
|
const liveTimeline = timelineSet.getLiveTimeline();
|
||||||
|
|
||||||
|
timelines.forEach((timeline, index) => {
|
||||||
|
// Add a special label when it is the live timeline so we can tell
|
||||||
|
// it apart from the others
|
||||||
|
const isLiveTimeline = timeline === liveTimeline;
|
||||||
|
timelineMap[isLiveTimeline ? 'liveTimeline' : `${index}`] = timeline.getEvents().map(ev => ev.getId());
|
||||||
|
});
|
||||||
|
|
||||||
|
return timelineMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
return serializedEventIdsInTimelineSet;
|
||||||
|
}
|
||||||
|
|
||||||
export default TimelinePanel;
|
export default TimelinePanel;
|
||||||
|
|
|
@ -30,6 +30,8 @@ import Field from '../elements/Field';
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import { sendSentryReport } from "../../../sentry";
|
import { sendSentryReport } from "../../../sentry";
|
||||||
|
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||||
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: (success: boolean) => void;
|
onFinished: (success: boolean) => void;
|
||||||
|
@ -65,6 +67,16 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||||
downloadProgress: null,
|
downloadProgress: null,
|
||||||
};
|
};
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
|
|
||||||
|
// Get all of the extra info dumped to the console when someone is about
|
||||||
|
// to send debug logs. Since this is a fire and forget action, we do
|
||||||
|
// this when the bug report dialog is opened instead of when we submit
|
||||||
|
// logs because we have no signal to know when all of the various
|
||||||
|
// components have finished logging. Someone could potentially send logs
|
||||||
|
// before we fully dump everything but it's probably unlikely.
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.DumpDebugLogs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
|
|
@ -1292,6 +1292,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
|
"data-event-id": this.props.mxEvent.getId(),
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
}, [
|
}, [
|
||||||
|
@ -1438,6 +1439,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
|
"data-event-id": this.props.mxEvent.getId(),
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
|
|
|
@ -306,4 +306,11 @@ export enum Action {
|
||||||
* Opens a dialog to add an existing object to a space. Used with a OpenAddExistingToSpaceDialogPayload.
|
* Opens a dialog to add an existing object to a space. Used with a OpenAddExistingToSpaceDialogPayload.
|
||||||
*/
|
*/
|
||||||
OpenAddToExistingSpaceDialog = "open_add_to_existing_space_dialog",
|
OpenAddToExistingSpaceDialog = "open_add_to_existing_space_dialog",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let components know that they should log any useful debugging information
|
||||||
|
* because we're probably about to send bug report which includes all of the
|
||||||
|
* logs. Fires with no payload.
|
||||||
|
*/
|
||||||
|
DumpDebugLogs = "dump_debug_logs",
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue