Eric Eastwood 2022-05-09 21:32:25 -05:00 committed by GitHub
parent d5b363e971
commit 3a241e0dfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 1 deletions

View file

@ -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;

View file

@ -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() {

View file

@ -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 }),

View file

@ -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",
} }