diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index cdb3e8a4a7f..7e3ba5d273d 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -371,6 +371,69 @@ class TimelinePanel extends React.Component { } } + /** + * 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 => { // If backwards, unpaginate from the back (i.e. the start of the timeline) const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; @@ -528,6 +591,9 @@ class TimelinePanel extends React.Component { case "ignore_state_changed": this.forceUpdate(); break; + case Action.DumpDebugLogs: + this.onDumpDebugLogs(); + break; } }; @@ -1464,7 +1530,7 @@ class TimelinePanel extends React.Component { const messagePanel = this.messagePanel.current; 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 const wrapperRect = messagePanelNode.getBoundingClientRect(); const myUserId = MatrixClientPeg.get().credentials.userId; @@ -1686,4 +1752,30 @@ class TimelinePanel extends React.Component { } } +/** + * 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; diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index eae26735214..b6ff9440471 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -30,6 +30,8 @@ import Field from '../elements/Field'; import Spinner from "../elements/Spinner"; import DialogButtons from "../elements/DialogButtons"; import { sendSentryReport } from "../../../sentry"; +import defaultDispatcher from '../../../dispatcher/dispatcher'; +import { Action } from '../../../dispatcher/actions'; interface IProps { onFinished: (success: boolean) => void; @@ -65,6 +67,16 @@ export default class BugReportDialog extends React.Component { downloadProgress: null, }; 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() { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 160bb9fff53..93c215221e2 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1292,6 +1292,7 @@ export class UnwrappedEventTile extends React.Component { "data-has-reply": !!replyChain, "data-layout": this.props.layout, "data-self": isOwnEvent, + "data-event-id": this.props.mxEvent.getId(), "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), }, [ @@ -1438,6 +1439,7 @@ export class UnwrappedEventTile extends React.Component { "data-scroll-tokens": scrollToken, "data-layout": this.props.layout, "data-self": isOwnEvent, + "data-event-id": this.props.mxEvent.getId(), "data-has-reply": !!replyChain, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index fd07b1641db..0331f7de945 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -306,4 +306,11 @@ export enum Action { * Opens a dialog to add an existing object to a space. Used with a OpenAddExistingToSpaceDialogPayload. */ 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", }