fix(ui): refactor list types for player, add docs to legacy msg manager methods; refactor unpack methods
This commit is contained in:
parent
531cf7499e
commit
9e3d9ea437
12 changed files with 306 additions and 181 deletions
|
|
@ -76,7 +76,7 @@ function MobileOverviewPanelCont({ issuesList }: { issuesList: Record<string, a
|
|||
)
|
||||
}
|
||||
|
||||
function WebOverviewPanelCont({ issuesList }: { issuesList: Record<string, any>[] }) {
|
||||
function WebOverviewPanelCont() {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
const [selectedFeatures, setSelectedFeatures] = React.useState([
|
||||
'PERFORMANCE',
|
||||
|
|
@ -92,7 +92,6 @@ function WebOverviewPanelCont({ issuesList }: { issuesList: Record<string, any>[
|
|||
} = store.get();
|
||||
|
||||
const stackEventList = tabStates[currentTab]?.stackList || []
|
||||
// const eventsList = tabStates[currentTab]?.eventList || []
|
||||
const frustrationsList = tabStates[currentTab]?.frustrationsList || []
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || []
|
||||
const resourceListUnmap = tabStates[currentTab]?.resourceList || []
|
||||
|
|
|
|||
|
|
@ -234,8 +234,8 @@ const reducer = (state = initialState, action: IAction) => {
|
|||
errors,
|
||||
issues,
|
||||
resources,
|
||||
stackEvents,
|
||||
userEvents,
|
||||
stackEvents,
|
||||
userTesting
|
||||
);
|
||||
|
||||
|
|
|
|||
39
frontend/app/player/common/unpack.ts
Normal file
39
frontend/app/player/common/unpack.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import * as fzstd from 'fzstd';
|
||||
import { gunzipSync } from 'fflate'
|
||||
|
||||
const unpack = (b: Uint8Array): Uint8Array => {
|
||||
// zstd magical numbers 40 181 47 253
|
||||
const isZstd = b[0] === 0x28 && b[1] === 0xb5 && b[2] === 0x2f && b[3] === 0xfd
|
||||
const isGzip = b[0] === 0x1F && b[1] === 0x8B && b[2] === 0x08;
|
||||
if (isGzip) {
|
||||
const now = performance.now()
|
||||
const data = gunzipSync(b)
|
||||
console.debug(
|
||||
"Gunzip time",
|
||||
Math.floor(performance.now() - now) + 'ms',
|
||||
'size',
|
||||
Math.floor(b.byteLength / 1024),
|
||||
'->',
|
||||
Math.floor(data.byteLength / 1024),
|
||||
'kb'
|
||||
)
|
||||
return data
|
||||
}
|
||||
if (isZstd) {
|
||||
const now = performance.now()
|
||||
const data = fzstd.decompress(b)
|
||||
console.debug(
|
||||
"Zstd unpack time",
|
||||
Math.floor(performance.now() - now) + 'ms',
|
||||
'size',
|
||||
Math.floor(b.byteLength / 1024),
|
||||
'->',
|
||||
Math.floor(data.byteLength / 1024),
|
||||
'kb'
|
||||
)
|
||||
return data
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
export default unpack
|
||||
|
|
@ -128,10 +128,15 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
});
|
||||
}
|
||||
|
||||
_sortMessagesHack() {
|
||||
/** empty here. Kept for consistency with normal manager */
|
||||
sortDomRemoveMessages() {
|
||||
return;
|
||||
}
|
||||
|
||||
public getListsFullState = () => {
|
||||
return this.lists.getFullListsState();
|
||||
}
|
||||
|
||||
private waitingForFiles: boolean = false;
|
||||
public onFileReadSuccess = () => {
|
||||
let newState: Partial<State> = {
|
||||
|
|
@ -148,7 +153,7 @@ export default class IOSMessageManager implements IMessageManager {
|
|||
this.state.update(newState);
|
||||
};
|
||||
|
||||
public onFileReadFailed = (e: any) => {
|
||||
public onFileReadFailed = (...e: any[]) => {
|
||||
logger.error(e);
|
||||
this.state.update({error: true});
|
||||
this.uiErrorHandler?.error('Error requesting a session file');
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ export interface IMessageManager {
|
|||
onFileReadFinally(): void;
|
||||
startLoading(): void;
|
||||
resetMessageManagers(): void;
|
||||
getListsFullState(): Record<string, unknown>
|
||||
move(t: number): any;
|
||||
distributeMessage(msg: Message): void;
|
||||
setMessagesLoading(messagesLoading: boolean): void;
|
||||
clean(): void;
|
||||
_sortMessagesHack: (msgs: Message[]) => void;
|
||||
sortDomRemoveMessages: (msgs: Message[]) => void;
|
||||
}
|
||||
|
||||
export interface SetState {
|
||||
|
|
|
|||
|
|
@ -1,87 +1,142 @@
|
|||
import { InjectedEvent } from 'Types/session/event';
|
||||
import Issue from 'Types/session/issue';
|
||||
import ListWalker from '../common/ListWalker';
|
||||
import ListWalkerWithMarks from '../common/ListWalkerWithMarks';
|
||||
import type { Timed } from '../common/types';
|
||||
import type { IResourceRequest, IResourceTiming, Timed } from 'Player';
|
||||
import {
|
||||
Redux as reduxMsg,
|
||||
Vuex as vuexMsg,
|
||||
MobX as mobxMsg,
|
||||
Zustand as zustandMsg,
|
||||
NgRx as ngrxMsg,
|
||||
GraphQl as graphqlMsg,
|
||||
ConsoleLog as logMsg,
|
||||
WsChannel as websocketMsg,
|
||||
Profiler as profilerMsg,
|
||||
} from 'Player/web/messages';
|
||||
|
||||
type stackMsg = {
|
||||
name: string;
|
||||
Payload: string;
|
||||
tp: number;
|
||||
} & Timed;
|
||||
type exceptionsMsg = {
|
||||
tp: number;
|
||||
name: string;
|
||||
message: string;
|
||||
payload: string;
|
||||
metadata: string;
|
||||
} & Timed;
|
||||
|
||||
const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles", "frustrations"] as const
|
||||
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack", "websocket" ] as const
|
||||
//const entityNamesSimple = [ "event", "profile" ];
|
||||
type MsgTypeMap = {
|
||||
reduxList: reduxMsg;
|
||||
mobxList: mobxMsg;
|
||||
vuexList: vuexMsg;
|
||||
zustandList: zustandMsg;
|
||||
ngrxList: ngrxMsg;
|
||||
graphqlList: graphqlMsg;
|
||||
logList: logMsg;
|
||||
fetchList: IResourceRequest;
|
||||
resourceList: IResourceTiming;
|
||||
stackList: stackMsg;
|
||||
websocketList: websocketMsg;
|
||||
profilerList: profilerMsg;
|
||||
exceptionsList: exceptionsMsg;
|
||||
frustrationsList: Issue | InjectedEvent;
|
||||
};
|
||||
type ListMessageType<K> = K extends keyof MsgTypeMap ? Array<MsgTypeMap[K]> : Array<Timed>;
|
||||
|
||||
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ] as const
|
||||
const SIMPLE_LIST_NAMES = [
|
||||
'event',
|
||||
'redux',
|
||||
'mobx',
|
||||
'vuex',
|
||||
'zustand',
|
||||
'ngrx',
|
||||
'graphql',
|
||||
'exceptions',
|
||||
'profiles',
|
||||
'frustrations',
|
||||
] as const;
|
||||
const MARKED_LIST_NAMES = ['log', 'resource', 'fetch', 'stack', 'websocket'] as const;
|
||||
|
||||
type KeysList = `${typeof LIST_NAMES[number]}List`
|
||||
type KeysListNow = `${typeof LIST_NAMES[number]}ListNow`
|
||||
type KeysMarkedCountNow = `${typeof MARKED_LIST_NAMES[number]}MarkedCountNow`
|
||||
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES] as const;
|
||||
|
||||
type KeysList = `${(typeof LIST_NAMES)[number]}List`;
|
||||
type KeysMarkedCountNow = `${(typeof MARKED_LIST_NAMES)[number]}MarkedCountNow`;
|
||||
type StateList = {
|
||||
[key in KeysList]: Timed[]
|
||||
}
|
||||
[K in KeysList]: ListMessageType<K>;
|
||||
};
|
||||
type StateListNow = {
|
||||
[key in KeysListNow]: Timed[]
|
||||
}
|
||||
[K in KeysList as `${K}Now`]: ListMessageType<K>;
|
||||
};
|
||||
type StateMarkedCountNow = {
|
||||
[key in KeysMarkedCountNow]: number
|
||||
}
|
||||
type StateNow = StateListNow & StateMarkedCountNow
|
||||
export type State = StateList & StateNow
|
||||
[key in KeysMarkedCountNow]: number;
|
||||
};
|
||||
type StateNow = StateListNow & StateMarkedCountNow;
|
||||
export type State = StateList & StateNow;
|
||||
|
||||
// maybe use list object itself inside the store
|
||||
|
||||
export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}List`] = []
|
||||
state[`${name}ListNow`] = []
|
||||
return state
|
||||
}, MARKED_LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}MarkedCountNow`] = 0
|
||||
return state
|
||||
export const INITIAL_STATE = LIST_NAMES.reduce(
|
||||
(state, name) => {
|
||||
state[`${name}List`] = [];
|
||||
state[`${name}ListNow`] = [];
|
||||
return state;
|
||||
},
|
||||
MARKED_LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}MarkedCountNow`] = 0;
|
||||
return state;
|
||||
}, {} as Partial<StateMarkedCountNow>) as Partial<State>
|
||||
) as State
|
||||
|
||||
) as State;
|
||||
|
||||
type SimpleListsObject = {
|
||||
[key in typeof SIMPLE_LIST_NAMES[number]]: ListWalker<Timed>
|
||||
}
|
||||
[key in (typeof SIMPLE_LIST_NAMES)[number]]: ListWalker<Timed>;
|
||||
};
|
||||
type MarkedListsObject = {
|
||||
[key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks<Timed>
|
||||
}
|
||||
type ListsObject = SimpleListsObject & MarkedListsObject
|
||||
[key in (typeof MARKED_LIST_NAMES)[number]]: ListWalkerWithMarks<Timed>;
|
||||
};
|
||||
type ListsObject = SimpleListsObject & MarkedListsObject;
|
||||
|
||||
export type InitialLists = {
|
||||
[key in typeof LIST_NAMES[number]]: any[] // .isRed()?
|
||||
}
|
||||
export type InitialLists = {
|
||||
[key in (typeof LIST_NAMES)[number]]: any[]; // .isRed()?
|
||||
};
|
||||
|
||||
export default class Lists {
|
||||
lists: ListsObject
|
||||
lists: ListsObject;
|
||||
|
||||
constructor(initialLists: Partial<InitialLists> = {}) {
|
||||
const lists: Partial<ListsObject> = {}
|
||||
const lists: Partial<ListsObject> = {};
|
||||
for (const name of SIMPLE_LIST_NAMES) {
|
||||
lists[name] = new ListWalker(initialLists[name])
|
||||
lists[name] = new ListWalker(initialLists[name]);
|
||||
}
|
||||
for (const name of MARKED_LIST_NAMES) {
|
||||
// TODO: provide types
|
||||
lists[name] = new ListWalkerWithMarks((el) => el.isRed, initialLists[name])
|
||||
lists[name] = new ListWalkerWithMarks((el) => el.isRed, initialLists[name]);
|
||||
}
|
||||
this.lists = lists as ListsObject
|
||||
this.lists = lists as ListsObject;
|
||||
}
|
||||
|
||||
getFullListsState(): StateList {
|
||||
return LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}List`] = this.lists[name].list
|
||||
return state
|
||||
}, {} as Partial<StateList>) as StateList
|
||||
state[`${name}List`] = this.lists[name].list;
|
||||
return state;
|
||||
}, {} as Partial<StateList>) as StateList;
|
||||
}
|
||||
|
||||
moveGetState(t: number): StateNow {
|
||||
return LIST_NAMES.reduce((state, name) => {
|
||||
const lastMsg = this.lists[name].moveGetLast(t) // index: name === 'exceptions' ? undefined : index);
|
||||
if (lastMsg != null) {
|
||||
state[`${name}ListNow`] = this.lists[name].listNow
|
||||
}
|
||||
return state
|
||||
}, MARKED_LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}MarkedCountNow`] = this.lists[name].markedCountNow // Red --> Marked
|
||||
return state
|
||||
return LIST_NAMES.reduce(
|
||||
(state, name) => {
|
||||
const lastMsg = this.lists[name].moveGetLast(t); // index: name === 'exceptions' ? undefined : index);
|
||||
if (lastMsg != null) {
|
||||
state[`${name}ListNow`] = this.lists[name].listNow;
|
||||
}
|
||||
return state;
|
||||
},
|
||||
MARKED_LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}MarkedCountNow`] = this.lists[name].markedCountNow; // Red --> Marked
|
||||
return state;
|
||||
}, {} as Partial<StateMarkedCountNow>) as Partial<State>
|
||||
) as State
|
||||
) as State;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
import type { Store, SessionFilesInfo } from 'Player';
|
||||
import {IMessageManager} from "Player/player/Animator";
|
||||
import { decryptSessionBytes } from './network/crypto';
|
||||
import MFileReader from './messages/MFileReader';
|
||||
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
|
||||
import type {
|
||||
Message,
|
||||
} from './messages';
|
||||
import type { Message } from './messages';
|
||||
import logger from 'App/logger';
|
||||
import * as fzstd from 'fzstd';
|
||||
|
||||
import unpack from 'Player/common/unpack';
|
||||
import MessageManager from 'Player/web/MessageManager';
|
||||
import IOSMessageManager from 'Player/mobile/IOSMessageManager';
|
||||
|
||||
interface State {
|
||||
firstFileLoading: boolean,
|
||||
domLoading: boolean,
|
||||
devtoolsLoading: boolean,
|
||||
error: boolean,
|
||||
firstFileLoading: boolean;
|
||||
domLoading: boolean;
|
||||
devtoolsLoading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export default class MessageLoader {
|
||||
|
|
@ -23,131 +21,148 @@ export default class MessageLoader {
|
|||
domLoading: false,
|
||||
devtoolsLoading: false,
|
||||
error: false,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly session: SessionFilesInfo,
|
||||
private store: Store<State>,
|
||||
private messageManager: IMessageManager,
|
||||
private messageManager: MessageManager | IOSMessageManager,
|
||||
private isClickmap: boolean,
|
||||
private uiErrorHandler?: { error: (msg: string) => void }
|
||||
) {}
|
||||
|
||||
createNewParser(shouldDecrypt = true, file?: string, toggleStatus?: (isLoading: boolean) => void) {
|
||||
const decrypt = shouldDecrypt && this.session.fileKey
|
||||
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey!)
|
||||
: (b: Uint8Array) => Promise.resolve(b)
|
||||
createNewParser(
|
||||
shouldDecrypt = true,
|
||||
file?: string
|
||||
) {
|
||||
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 unarchived = (b: Uint8Array) => {
|
||||
// zstd magical numbers 40 181 47 253
|
||||
const isZstd = b[0] === 0x28 && b[1] === 0xb5 && b[2] === 0x2f && b[3] === 0xfd
|
||||
if (isZstd) {
|
||||
return fzstd.decompress(b)
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt)
|
||||
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt);
|
||||
return (b: Uint8Array) => {
|
||||
decrypt(b).then(b => {
|
||||
const data = unarchived(b)
|
||||
toggleStatus?.(true);
|
||||
fileReader.append(data)
|
||||
fileReader.checkForIndexes()
|
||||
const msgs: Array<Message & { _index?: number }> = []
|
||||
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
|
||||
msgs.push(msg)
|
||||
}
|
||||
const sorted = msgs.sort((m1, m2) => {
|
||||
return m1.time - m2.time
|
||||
})
|
||||
return decrypt(b)
|
||||
.then((b) => {
|
||||
const data = unpack(b);
|
||||
fileReader.append(data);
|
||||
fileReader.checkForIndexes();
|
||||
const msgs: Array<Message & { tabId: string }> = [];
|
||||
let finished = false;
|
||||
while (!finished) {
|
||||
const msg = fileReader.readNext();
|
||||
if (msg) {
|
||||
msgs.push(msg);
|
||||
} else {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sorted.forEach(msg => {
|
||||
this.messageManager.distributeMessage(msg)
|
||||
})
|
||||
logger.info("Messages count: ", msgs.length, sorted, file)
|
||||
const sortedMessages = msgs.sort((m1, m2) => {
|
||||
return m1.time - m2.time;
|
||||
});
|
||||
|
||||
this.messageManager._sortMessagesHack(sorted)
|
||||
toggleStatus?.(false);
|
||||
this.messageManager.setMessagesLoading(false)
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.uiErrorHandler?.error('Error parsing file: ' + e.message)
|
||||
})
|
||||
}
|
||||
sortedMessages.forEach((msg) => {
|
||||
this.messageManager.distributeMessage(msg);
|
||||
});
|
||||
logger.info('Messages count: ', msgs.length, sortedMessages, file);
|
||||
|
||||
this.messageManager.sortDomRemoveMessages(sortedMessages);
|
||||
this.messageManager.setMessagesLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.uiErrorHandler?.error('Error parsing file: ' + e.message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
|
||||
if (urls.length > 0) {
|
||||
this.store.update({ domLoading: true })
|
||||
return loadFiles(urls, parser, true).then(() => this.store.update({ domLoading: false }))
|
||||
this.store.update({ domLoading: true });
|
||||
return loadFiles(urls, parser, true).then(() => this.store.update({ domLoading: false }));
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
loadDevtools(parser: (b: Uint8Array) => Promise<void>) {
|
||||
if (!this.isClickmap) {
|
||||
this.store.update({ devtoolsLoading: true })
|
||||
return loadFiles(this.session.devtoolsURL, parser)
|
||||
// TODO: also in case of dynamic update through assist
|
||||
.then(() => {
|
||||
// @ts-ignore ?
|
||||
this.store.update({ ...this.messageManager.getListsFullState(), devtoolsLoading: false });
|
||||
})
|
||||
this.store.update({ devtoolsLoading: true });
|
||||
return (
|
||||
loadFiles(this.session.devtoolsURL, parser)
|
||||
// TODO: also in case of dynamic update through assist
|
||||
.then(() => {
|
||||
// @ts-ignore ?
|
||||
this.store.update({
|
||||
...this.messageManager.getListsFullState(),
|
||||
devtoolsLoading: false,
|
||||
});
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async loadFiles() {
|
||||
this.messageManager.startLoading()
|
||||
this.messageManager.startLoading();
|
||||
|
||||
const loadMethod = this.session.domURL && this.session.domURL.length > 0
|
||||
? { url: this.session.domURL, parser: () => this.createNewParser(true, 'dom') }
|
||||
: { url: this.session.mobsUrl, parser: () => this.createNewParser(false, 'dom') }
|
||||
const loadMethod =
|
||||
this.session.domURL && this.session.domURL.length > 0
|
||||
? { mobUrls: this.session.domURL, parser: () => this.createNewParser(true, 'dom') }
|
||||
: { mobUrls: this.session.mobsUrl, parser: () => this.createNewParser(false, 'dom') };
|
||||
|
||||
const parser = loadMethod.parser()
|
||||
const devtoolsParser = this.createNewParser(true, 'devtools')
|
||||
const parser = loadMethod.parser();
|
||||
const devtoolsParser = this.createNewParser(true, 'devtools');
|
||||
/**
|
||||
* We load first dom mob file before the rest
|
||||
* to speed up time to replay
|
||||
* but as a tradeoff we have to have some copy-paste
|
||||
* we load first dom mob file before the rest
|
||||
* (because parser can read them in parallel)
|
||||
* as a tradeoff we have some copy-paste code
|
||||
* for the devtools file
|
||||
* */
|
||||
try {
|
||||
await loadFiles([loadMethod.url[0]], parser)
|
||||
const restDomFilesPromise = this.loadDomFiles([...loadMethod.url.slice(1)], parser)
|
||||
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser)
|
||||
await loadFiles([loadMethod.mobUrls[0]], parser);
|
||||
const restDomFilesPromise = this.loadDomFiles([...loadMethod.mobUrls.slice(1)], parser);
|
||||
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser);
|
||||
|
||||
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise])
|
||||
this.messageManager.onFileReadSuccess()
|
||||
} catch (e) {
|
||||
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
|
||||
this.messageManager.onFileReadSuccess();
|
||||
} catch (sessionLoadError) {
|
||||
try {
|
||||
this.store.update({ domLoading: true, devtoolsLoading: true })
|
||||
const efsDomFilePromise = requestEFSDom(this.session.sessionId)
|
||||
const efsDevtoolsFilePromise = requestEFSDevtools(this.session.sessionId)
|
||||
this.store.update({ domLoading: true, devtoolsLoading: true });
|
||||
const efsDomFilePromise = requestEFSDom(this.session.sessionId);
|
||||
const efsDevtoolsFilePromise = requestEFSDevtools(this.session.sessionId);
|
||||
|
||||
const [domData, devtoolsData] = await Promise.allSettled([efsDomFilePromise, efsDevtoolsFilePromise])
|
||||
const domParser = this.createNewParser(false, 'domEFS')
|
||||
const devtoolsParser = this.createNewParser(false, 'devtoolsEFS')
|
||||
const parseDomPromise: Promise<any> = domData.status === 'fulfilled'
|
||||
? domParser(domData.value) : Promise.reject('No dom file in EFS')
|
||||
const parseDevtoolsPromise: Promise<any> = devtoolsData.status === 'fulfilled'
|
||||
? devtoolsParser(devtoolsData.value) : Promise.reject('No devtools file in EFS')
|
||||
const [domData, devtoolsData] = await Promise.allSettled([
|
||||
efsDomFilePromise,
|
||||
efsDevtoolsFilePromise,
|
||||
]);
|
||||
const domParser = this.createNewParser(false, 'domEFS');
|
||||
const devtoolsParser = this.createNewParser(false, 'devtoolsEFS');
|
||||
const parseDomPromise: Promise<any> =
|
||||
domData.status === 'fulfilled'
|
||||
? domParser(domData.value)
|
||||
: Promise.reject('No dom file in EFS');
|
||||
const parseDevtoolsPromise: Promise<any> =
|
||||
devtoolsData.status === 'fulfilled'
|
||||
? devtoolsParser(devtoolsData.value)
|
||||
: Promise.reject('No devtools file in EFS');
|
||||
|
||||
await Promise.all([parseDomPromise, parseDevtoolsPromise])
|
||||
this.messageManager.onFileReadSuccess()
|
||||
} catch (e2) {
|
||||
this.messageManager.onFileReadFailed(e)
|
||||
await Promise.all([parseDomPromise, parseDevtoolsPromise]);
|
||||
this.messageManager.onFileReadSuccess();
|
||||
} catch (unprocessedLoadError) {
|
||||
this.messageManager.onFileReadFailed(sessionLoadError, unprocessedLoadError);
|
||||
}
|
||||
} finally {
|
||||
this.messageManager.onFileReadFinally()
|
||||
this.store.update({ domLoading: false, devtoolsLoading: false })
|
||||
this.messageManager.onFileReadFinally();
|
||||
this.store.update({ domLoading: false, devtoolsLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.store.update(MessageLoader.INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,8 +146,12 @@ export default class MessageManager {
|
|||
});
|
||||
}
|
||||
|
||||
public _sortMessagesHack = (msgs: Message[]) => {
|
||||
Object.values(this.tabs).forEach((tab) => tab._sortMessagesHack(msgs));
|
||||
/**
|
||||
* Legacy code. Iterates over all tab managers and sorts messages for their pagesManager.
|
||||
* Ensures that RemoveNode messages with parent being <HEAD> are sorted before other RemoveNode messages.
|
||||
* */
|
||||
public sortDomRemoveMessages = (msgs: Message[]) => {
|
||||
Object.values(this.tabs).forEach((tab) => tab.sortDomRemoveMessages(msgs));
|
||||
};
|
||||
|
||||
private waitingForFiles: boolean = false;
|
||||
|
|
@ -159,7 +163,7 @@ export default class MessageManager {
|
|||
Object.values(this.tabs).forEach((tab) => tab.onFileReadSuccess?.());
|
||||
};
|
||||
|
||||
public onFileReadFailed = (e: any) => {
|
||||
public onFileReadFailed = (...e: any[]) => {
|
||||
logger.error(e);
|
||||
this.state.update({ error: true });
|
||||
this.uiErrorHandler?.error('Error requesting a session file');
|
||||
|
|
|
|||
|
|
@ -351,22 +351,25 @@ export default class TabSessionManager {
|
|||
return this.decoder.decode(msg);
|
||||
}
|
||||
|
||||
public _sortMessagesHack = (msgs: Message[]) => {
|
||||
/**
|
||||
* Legacy code. Ensures that RemoveNode messages with parent being <HEAD> are sorted before other RemoveNode messages.
|
||||
* */
|
||||
public sortDomRemoveMessages = (msgs: Message[]) => {
|
||||
// @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);
|
||||
const headChildrenMsgIds = msgs.filter((m) => m.parentID === 1).map((m) => m.id);
|
||||
this.pagesManager.sortPages((m1, m2) => {
|
||||
if (m1.time === m2.time) {
|
||||
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
|
||||
if (headChildrenIds.includes(m1.id)) {
|
||||
if (headChildrenMsgIds.includes(m1.id)) {
|
||||
return -1;
|
||||
}
|
||||
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
|
||||
if (headChildrenIds.includes(m2.id)) {
|
||||
if (headChildrenMsgIds.includes(m2.id)) {
|
||||
return 1;
|
||||
}
|
||||
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
|
||||
const m1FromHead = headChildrenIds.includes(m1.id);
|
||||
const m2FromHead = headChildrenIds.includes(m2.id);
|
||||
const m1FromHead = headChildrenMsgIds.includes(m1.id);
|
||||
const m2FromHead = headChildrenMsgIds.includes(m2.id);
|
||||
if (m1FromHead && !m2FromHead) {
|
||||
return -1;
|
||||
} else if (m2FromHead && !m1FromHead) {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
}
|
||||
|
||||
currentTab = 'back-compatability'
|
||||
readNext(): Message & { _index?: number } | null {
|
||||
readNext(): Message & { tabId: string; _index?: number } | null {
|
||||
if (this.error || !this.hasNextByte()) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
this.currentTime = rMsg.timestamp - this.startTime
|
||||
return {
|
||||
tp: 9999,
|
||||
tabId: '',
|
||||
time: this.currentTime,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { gunzipSync } from 'fflate'
|
||||
|
||||
const u8aFromHex = (hexString:string) =>
|
||||
Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
|
||||
|
||||
|
|
@ -20,24 +18,6 @@ export function decryptSessionBytes(cypher: Uint8Array, keyString: string): Prom
|
|||
return crypto.subtle.importKey("raw", byteKey, { name: "AES-CBC" }, false, ["decrypt"])
|
||||
.then(key => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv}, key, cypher))
|
||||
.then((bArray: ArrayBuffer) => new Uint8Array(bArray))
|
||||
.then(async (u8Array: Uint8Array) => {
|
||||
const isGzip = u8Array[0] === 0x1F && u8Array[1] === 0x8B && u8Array[2] === 0x08;
|
||||
if (isGzip) {
|
||||
const now = performance.now()
|
||||
const data = gunzipSync(u8Array)
|
||||
console.debug(
|
||||
"Decompression time",
|
||||
Math.floor(performance.now() - now) + 'ms',
|
||||
'size',
|
||||
Math.floor(u8Array.byteLength/1024),
|
||||
'->',
|
||||
Math.floor(data.byteLength/1024),
|
||||
'kb'
|
||||
)
|
||||
return data
|
||||
} else return u8Array
|
||||
})
|
||||
//?? TS doesn not catch the `decrypt`` returning type
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,29 @@ interface IResource {
|
|||
responseBodySize?: number,
|
||||
}
|
||||
|
||||
export interface IResourceTiming extends IResource {
|
||||
name: string,
|
||||
isRed: boolean,
|
||||
isYellow: boolean,
|
||||
type: ResourceType,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "..",
|
||||
success: boolean,
|
||||
status: "2xx-3xx" | "4xx-5xx",
|
||||
time: number,
|
||||
}
|
||||
|
||||
export interface IResourceRequest extends IResource {
|
||||
name: string,
|
||||
isRed: boolean,
|
||||
isYellow: boolean,
|
||||
type: ResourceType.XHR | ResourceType.FETCH | ResourceType.IOS,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "..",
|
||||
success: boolean,
|
||||
status: number,
|
||||
time: number,
|
||||
decodedBodySize?: number,
|
||||
}
|
||||
|
||||
|
||||
export const Resource = (resource: IResource) => ({
|
||||
...resource,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue