From c4dc4ffded40e3cb8f4cf8df201a2e26d39746af Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 11:52:27 +0100 Subject: [PATCH] refactor(ui/player): fix errors after merge --- .../Session_/Exceptions/Exceptions.js | 2 - .../Session_/OverviewPanel/OverviewPanel.tsx | 6 +- .../app/components/Session_/Player/Player.js | 1 + frontend/app/player/_web/MessageManager.ts | 570 ++++++++++++++++++ 4 files changed, 573 insertions(+), 6 deletions(-) create mode 100644 frontend/app/player/_web/MessageManager.ts diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.js b/frontend/app/components/Session_/Exceptions/Exceptions.js index 0eabdf314..2b0c206b4 100644 --- a/frontend/app/components/Session_/Exceptions/Exceptions.js +++ b/frontend/app/components/Session_/Exceptions/Exceptions.js @@ -8,10 +8,8 @@ import { ErrorItem, SlideModal, ErrorDetails, - ErrorHeader, Link, QuestionMarkHint, - Tabs, } from 'UI'; import { fetchErrorStackList } from 'Duck/sessions'; import { connectPlayer, jump } from 'Player'; diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 0b542dce6..81ed0648d 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -14,10 +14,8 @@ import { NoContent, Icon } from 'UI'; import { observer } from 'mobx-react-lite'; import { PlayerContext } from 'App/components/Session/playerContext'; -function OverviewPanel({ issuesList }: { issuesList: any[] }) { - const { store } = React.useContext(PlayerContext) - function OverviewPanel() { + const { store } = React.useContext(PlayerContext) const [dataLoaded, setDataLoaded] = React.useState(false); const [selectedFeatures, setSelectedFeatures] = React.useState([ 'PERFORMANCE', @@ -140,4 +138,4 @@ export default connect( } )( observer(OverviewPanel) -); +) diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 9312589f8..b283b669a 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -47,6 +47,7 @@ function Player(props) { closedLive, bottomBlock, activeTab, + fullView, } = props; const playerContext = React.useContext(PlayerContext) const screenWrapper = React.useRef(); diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts new file mode 100644 index 000000000..49105200c --- /dev/null +++ b/frontend/app/player/_web/MessageManager.ts @@ -0,0 +1,570 @@ +// @ts-ignore +import { Decoder } from "syncod"; +import logger from 'App/logger'; + +import Resource, { TYPES } from 'Types/session/resource'; +import { TYPES as EVENT_TYPES } from 'Types/session/event'; +import Log from 'Types/session/log'; + +import { toast } from 'react-toastify'; + +import type { Store } from '../player/types'; +import ListWalker from '../_common/ListWalker'; + +import Screen from './Screen/Screen'; + +import PagesManager from './managers/PagesManager'; +import MouseMoveManager from './managers/MouseMoveManager'; + +import PerformanceTrackManager from './managers/PerformanceTrackManager'; +import WindowNodeCounter from './managers/WindowNodeCounter'; +import ActivityManager from './managers/ActivityManager'; + +import MFileReader from './messages/MFileReader'; +import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; +import { decryptSessionBytes } from './network/crypto'; + +import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; +import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager'; +import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; + +import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; +import type { SkipInterval } from './managers/ActivityManager'; + + +export interface State extends SuperState, AssistState { + performanceChartData: PerformanceChartPoint[], + skipIntervals: SkipInterval[], + connType?: string, + connBandwidth?: number, + location?: string, + performanceChartTime?: number, + + domContentLoadedTime?: any, + domBuildingTime?: any, + loadTime?: any, + error: boolean, + devtoolsLoading: boolean, + + liveTimeTravel: boolean, + messagesLoading: boolean, + cssLoading: boolean, + + ready: boolean, + lastMessageTime: number, +} + +export const INITIAL_STATE: State = { + ...SUPER_INITIAL_STATE, + ...LISTS_INITIAL_STATE, + ...ASSIST_INITIAL_STATE, + performanceChartData: [], + skipIntervals: [], + error: false, + devtoolsLoading: false, + + liveTimeTravel: false, + messagesLoading: false, + cssLoading: false, + get ready() { + return !this.messagesLoading && !this.cssLoading + }, + lastMessageTime: 0, +}; + + +import type { + Message, + SetPageLocation, + ConnectionInformation, + SetViewportSize, + SetViewportScroll, + MouseClick, +} from './messages'; + +const visualChanges = [ + "mouse_move", + "mouse_click", + "create_element_node", + "set_input_value", + "set_input_checked", + "set_viewport_size", + "set_viewport_scroll", +] + +export default class MessageManager extends Screen { + // TODO: consistent with the other data-lists + private locationEventManager: ListWalker/**/ = new ListWalker(); + private locationManager: ListWalker = new ListWalker(); + private loadedLocationManager: ListWalker = new ListWalker(); + private connectionInfoManger: ListWalker = new ListWalker(); + private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager(); + private windowNodeCounter: WindowNodeCounter = new WindowNodeCounter(); + private clickManager: ListWalker = new ListWalker(); + + private resizeManager: ListWalker = new ListWalker([]); + private pagesManager: PagesManager; + private mouseMoveManager: MouseMoveManager; + + private scrollManager: ListWalker = new ListWalker(); + + private readonly decoder = new Decoder(); + private readonly lists: Lists; + + private activityManager: ActivityManager | null = null; + + private sessionStart: number; + private navigationStartOffset: number = 0; + private lastMessageTime: number = 0; + private lastMessageInFileTime: number = 0; + + constructor( + private readonly session: any /*Session*/, + private readonly state: Store, + config: any, + live: boolean, + ) { + super(); + this.pagesManager = new PagesManager(this, this.session.isMobile) + this.mouseMoveManager = new MouseMoveManager(this); + + this.sessionStart = this.session.startedAt; + + if (live) { + this.lists = new Lists() + } else { + this.activityManager = new ActivityManager(this.session.duration.milliseconds); + /* == REFACTOR_ME == */ + const eventList = session.events.toJSON(); + // TODO: fix types for events, remove immutable js + eventList.forEach((e: Record) => { + if (e.type === EVENT_TYPES.LOCATION) { //TODO type system + this.locationEventManager.append(e); + } + }) + + this.lists = new Lists({ + event: eventList, + stack: session.stackEvents.toJSON(), + resource: session.resources.toJSON(), + exceptions: session.errors, + }) + + + /* === */ + this.loadMessages(); + } + } + + private parseAndDistributeMessages(fileReader: MFileReader, onMessage?: (msg: Message) => void) { + const msgs: Array = [] + let next: ReturnType + while (next = fileReader.next()) { + const [msg, index] = next + this.distributeMessage(msg, index) + msgs.push(msg) + onMessage?.(msg) + } + + logger.info("Messages count: ", msgs.length, msgs) + + + // @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first)) + const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id); + this.pagesManager.sortPages((m1, m2) => { + if (m1.time === m2.time) { + if (m1.tp === "remove_node" && m2.tp !== "remove_node") { + if (headChildrenIds.includes(m1.id)) { + return -1; + } + } else if (m2.tp === "remove_node" && m1.tp !== "remove_node") { + if (headChildrenIds.includes(m2.id)) { + return 1; + } + } else if (m2.tp === "remove_node" && m1.tp === "remove_node") { + const m1FromHead = headChildrenIds.includes(m1.id); + const m2FromHead = headChildrenIds.includes(m2.id); + if (m1FromHead && !m2FromHead) { + return -1; + } else if (m2FromHead && !m1FromHead) { + return 1; + } + } + } + return 0; + }) + } + + + private waitingForFiles: boolean = false + private onFileReadSuccess = () => { + const stateToUpdate = { + performanceChartData: this.performanceTrackManager.chartData, + performanceAvaliability: this.performanceTrackManager.avaliability, + ...this.lists.getFullListsState() + } + if (this.activityManager) { + this.activityManager.end() + stateToUpdate.skipIntervals = this.activityManager.list + } + + this.state.update(stateToUpdate) + } + private onFileReadFailed = (e: any) => { + logger.error(e) + this.state.update({ error: true }) + toast.error('Error requesting a session file') + } + private onFileReadFinally = () => { + this.incomingMessages + .filter(msg => msg.time >= this.lastMessageInFileTime) + .forEach(msg => this.distributeMessage(msg, 0)) + + this.waitingForFiles = false + this.setMessagesLoading(false) + } + + private loadMessages() { + // TODO: reuseable decryptor instance + const createNewParser = (shouldDecrypt=true) => { + const decrypt = shouldDecrypt && this.session.fileKey + ? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey) + : (b: Uint8Array) => Promise.resolve(b) + // Each time called - new fileReader created + const fileReader = new MFileReader(new Uint8Array(), this.sessionStart) + return (b: Uint8Array) => decrypt(b).then(b => { + fileReader.append(b) + this.parseAndDistributeMessages(fileReader) + this.setMessagesLoading(false) + }) + } + this.setMessagesLoading(true) + this.waitingForFiles = true + + loadFiles(this.session.domURL, createNewParser()) + .catch(() => // do if only the first file missing (404) (?) + requestEFSDom(this.session.sessionId) + .then(createNewParser(false)) + // Fallback to back Compatability with mobsUrl + .catch(e => + loadFiles(this.session.mobsUrl, createNewParser(false)) + ) + ) + .then(this.onFileReadSuccess) + .catch(this.onFileReadFailed) + .finally(this.onFileReadFinally) + + // load devtools + if (this.session.devtoolsURL.length) { + this.state.update({ devtoolsLoading: true }) + loadFiles(this.session.devtoolsURL, createNewParser()) + .catch(() => + requestEFSDevtools(this.session.sessionId) + .then(createNewParser(false)) + ) + //.catch() // not able to download the devtools file + .finally(() => this.state.update({ devtoolsLoading: false })) + } + } + + reloadWithUnprocessedFile() { + const onData = (byteArray: Uint8Array) => { + const onMessage = (msg: Message) => { this.lastMessageInFileTime = msg.time } + this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage) + } + const updateState = () => + this.state.update({ + liveTimeTravel: true, + }); + + // assist will pause and skip messages to prevent timestamp related errors + this.reloadMessageManagers() + this.windowNodeCounter.reset() + + this.setMessagesLoading(true) + this.waitingForFiles = true + + return requestEFSDom(this.session.sessionId) + .then(onData) + .then(updateState) + .then(this.onFileReadSuccess) + .catch(this.onFileReadFailed) + .finally(this.onFileReadFinally) + } + + private reloadMessageManagers() { + this.locationEventManager = new ListWalker(); + this.locationManager = new ListWalker(); + this.loadedLocationManager = new ListWalker(); + this.connectionInfoManger = new ListWalker(); + this.clickManager = new ListWalker(); + this.scrollManager = new ListWalker(); + this.resizeManager = new ListWalker([]); + + this.performanceTrackManager = new PerformanceTrackManager() + this.windowNodeCounter = new WindowNodeCounter(); + this.pagesManager = new PagesManager(this, this.session.isMobile) + this.mouseMoveManager = new MouseMoveManager(this); + this.activityManager = new ActivityManager(this.session.duration.milliseconds); + } + + move(t: number, index?: number): void { + const stateToUpdate: Partial = {}; + /* == REFACTOR_ME == */ + const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index); + if (!!lastLoadedLocationMsg) { + // TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time) + this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart; + } + const llEvent = this.locationEventManager.moveGetLast(t, index); + if (!!llEvent) { + if (llEvent.domContentLoadedTime != null) { + stateToUpdate.domContentLoadedTime = { + time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db) + value: llEvent.domContentLoadedTime, + } + } + if (llEvent.loadTime != null) { + stateToUpdate.loadTime = { + time: llEvent.loadTime + this.navigationStartOffset, + value: llEvent.loadTime, + } + } + if (llEvent.domBuildingTime != null) { + stateToUpdate.domBuildingTime = llEvent.domBuildingTime; + } + } + /* === */ + const lastLocationMsg = this.locationManager.moveGetLast(t, index); + if (!!lastLocationMsg) { + stateToUpdate.location = lastLocationMsg.url; + } + const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index); + if (!!lastConnectionInfoMsg) { + stateToUpdate.connType = lastConnectionInfoMsg.type; + stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink; + } + const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index); + if (!!lastPerformanceTrackMessage) { + stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time; + } + + this.lists.moveGetState(t) + Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); + + /* Sequence of the managers is important here */ + // Preparing the size of "screen" + const lastResize = this.resizeManager.moveGetLast(t, index); + if (!!lastResize) { + this.setSize(lastResize) + } + this.pagesManager.moveReady(t).then(() => { + + const lastScroll = this.scrollManager.moveGetLast(t, index); + if (!!lastScroll && this.window) { + this.window.scrollTo(lastScroll.x, lastScroll.y); + } + // Moving mouse and setting :hover classes on ready view + this.mouseMoveManager.move(t); + const lastClick = this.clickManager.moveGetLast(t); + if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms + this.cursor.click(); + } + // After all changes - redraw the marker + //this.marker.redraw(); + }) + + if (this.waitingForFiles && this.lastMessageTime <= t) { + this.setMessagesLoading(true) + } + } + + private decodeStateMessage(msg: any, keys: Array) { + const decoded = {}; + try { + keys.forEach(key => { + // @ts-ignore TODO: types for decoder + decoded[key] = this.decoder.decode(msg[key]); + }); + } catch (e) { + logger.error("Error on message decoding: ", e, msg); + return null; + } + return { ...msg, ...decoded }; + } + + private readonly incomingMessages: Message[] = [] + appendMessage(msg: Message, index: number) { + // @ts-ignore + // msg.time = this.md.getLastRecordedMessageTime() + msg.time\ + //TODO: put index in message type + this.incomingMessages.push(msg) + if (!this.waitingForFiles) { + this.distributeMessage(msg, index) + } + } + + private distributeMessage(msg: Message, index: number): void { + const lastMessageTime = Math.max(msg.time, this.lastMessageTime) + this.lastMessageTime = lastMessageTime + this.state.update({ lastMessageTime }) + if (visualChanges.includes(msg.tp)) { + this.activityManager?.updateAcctivity(msg.time); + } + let decoded; + const time = msg.time; + switch (msg.tp) { + /* Lists: */ + case "console_log": + if (msg.level === 'debug') break; + this.lists.lists.log.append(Log({ + level: msg.level, + value: msg.value, + time, + index, + })) + break; + case "fetch": + this.lists.lists.fetch.append(Resource({ + method: msg.method, + url: msg.url, + payload: msg.request, + response: msg.response, + status: msg.status, + duration: msg.duration, + type: TYPES.FETCH, + time: msg.timestamp - this.sessionStart, //~ + index, + })); + break; + /* */ + case "set_page_location": + this.locationManager.append(msg); + if (msg.navigationStart > 0) { + this.loadedLocationManager.append(msg); + } + break; + case "set_viewport_size": + this.resizeManager.append(msg); + break; + case "mouse_move": + this.mouseMoveManager.append(msg); + break; + case "mouse_click": + this.clickManager.append(msg); + break; + case "set_viewport_scroll": + this.scrollManager.append(msg); + break; + case "performance_track": + this.performanceTrackManager.append(msg); + break; + case "set_page_visibility": + this.performanceTrackManager.handleVisibility(msg) + break; + case "connection_information": + this.connectionInfoManger.append(msg); + break; + case "o_table": + this.decoder.set(msg.key, msg.value); + break; + case "redux": + decoded = this.decodeStateMessage(msg, ["state", "action"]); + logger.log('redux', decoded) + if (decoded != null) { + this.lists.lists.redux.append(decoded); + } + break; + case "ng_rx": + decoded = this.decodeStateMessage(msg, ["state", "action"]); + logger.log('ngrx', decoded) + if (decoded != null) { + this.lists.lists.ngrx.append(decoded); + } + break; + case "vuex": + decoded = this.decodeStateMessage(msg, ["state", "mutation"]); + logger.log('vuex', decoded) + if (decoded != null) { + this.lists.lists.vuex.append(decoded); + } + break; + case "zustand": + decoded = this.decodeStateMessage(msg, ["state", "mutation"]) + logger.log('zustand', decoded) + if (decoded != null) { + this.lists.lists.zustand.append(decoded) + } + case "mob_x": + decoded = this.decodeStateMessage(msg, ["payload"]); + logger.log('mobx', decoded) + + if (decoded != null) { + this.lists.lists.mobx.append(decoded); + } + break; + case "graph_ql": + this.lists.lists.graphql.append(msg); + break; + case "profiler": + this.lists.lists.profiles.append(msg); + break; + default: + switch (msg.tp) { + case "create_document": + this.windowNodeCounter.reset(); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + case "create_text_node": + case "create_element_node": + this.windowNodeCounter.addNode(msg.id, msg.parentID); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + case "move_node": + this.windowNodeCounter.moveNode(msg.id, msg.parentID); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + case "remove_node": + this.windowNodeCounter.removeNode(msg.id); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + } + this.performanceTrackManager.addNodeCountPointIfNeed(msg.time) + this.pagesManager.appendMessage(msg); + break; + } + } + + getLastMessageTime(): number { + return this.lastMessageTime; + } + + getFirstMessageTime(): number { + return this.pagesManager.minTime; + } + + + setMessagesLoading(messagesLoading: boolean) { + this.display(!messagesLoading); + this.state.update({ messagesLoading }); + } + + setCSSLoading(cssLoading: boolean) { + this.displayFrame(!cssLoading); + this.state.update({ cssLoading }); + } + + private setSize({ height, width }: { height: number, width: number }) { + this.scale({ height, width }); + this.state.update({ width, height }); + + //this.updateMarketTargets() + } + + // TODO: clean managers? + clean() { + this.state.update(INITIAL_STATE); + this.incomingMessages.length = 0 + } + +}