change(ui): split session info into separate calls for faster replay time

This commit is contained in:
nick-delirium 2023-03-14 15:00:55 +01:00 committed by Delirium
parent 89d45d2247
commit d7dc9b684f
8 changed files with 375 additions and 179 deletions

View file

@ -2,7 +2,7 @@ import React from 'react';
import { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import usePageTitle from 'App/hooks/usePageTitle';
import { fetch as fetchSession } from 'Duck/sessions';
import { fetchV2 } from "Duck/sessions";
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { Link, NoContent, Loader } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
@ -17,14 +17,14 @@ function Session({
sessionId,
loading,
hasErrors,
fetchSession,
fetchV2,
}) {
usePageTitle("OpenReplay Session Player");
const [ initializing, setInitializing ] = useState(true)
const { sessionStore } = useStore();
useEffect(() => {
if (sessionId != null) {
fetchSession(sessionId)
fetchV2(sessionId)
} else {
console.error("No sessionID in route.")
}
@ -63,6 +63,6 @@ export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state, pro
session: state.getIn([ 'sessions', 'current' ]),
};
}, {
fetchSession,
fetchSlackList,
fetchV2,
})(Session));

View file

@ -64,6 +64,12 @@ function WebPlayer(props: any) {
return () => WebPlayerInst.clean();
}, [session.sessionId]);
React.useEffect(() => {
if (session.events.length > 0 || session.errors.length > 0) {
contextValue.player.updateLists(session)
}
}, [session.events, session.errors])
const isPlayerReady = contextValue.store?.get().ready
React.useEffect(() => {

View file

@ -91,6 +91,7 @@ function Timeline(props: IProps) {
}
const time = getTime(e);
if (!time) return;
const tz = settingsStore.sessionSettings.timezone.value
const timeStr = DateTime.fromMillis(props.startedAt + time).setZone(tz).toFormat(`hh:mm:ss a`)
const timeLineTooltip = {

View file

@ -1,18 +1,25 @@
import { List, Map } from 'immutable';
import Session from 'Types/session';
import ErrorStack from 'Types/session/errorStack';
import { Location } from 'Types/session/event'
import { EventData, Location } from "Types/session/event";
import Watchdog from 'Types/watchdog';
import { clean as cleanParams } from 'App/api_client';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { getRE, setSessionFilter, getSessionFilter, compareJsonObjects, cleanSessionFilters } from 'App/utils';
import { LAST_7_DAYS } from 'Types/app/period';
import { getDateRangeFromValue } from 'App/dateRange';
import APIClient from 'App/api_client';
import { FETCH_ACCOUNT, UPDATE_JWT } from "Duck/user";
import logger from "App/logger";
const name = 'sessions';
const FETCH_LIST = new RequestTypes('sessions/FETCH_LIST');
const FETCH_AUTOPLAY_LIST = new RequestTypes('sessions/FETCH_AUTOPLAY_LIST');
const FETCH = new RequestTypes('sessions/FETCH');
const FETCH = new RequestTypes('sessions/FETCH')
const FETCHV2 = new RequestTypes('sessions/FETCHV2')
const FETCH_EVENTS = new RequestTypes('sessions/FETCH_EVENTS');
const FETCH_NOTES = new RequestTypes('sessions/FETCH_NOTES');
const FETCH_FAVORITE_LIST = new RequestTypes('sessions/FETCH_FAVORITE_LIST');
const FETCH_LIVE_LIST = new RequestTypes('sessions/FETCH_LIVE_LIST');
const TOGGLE_FAVORITE = new RequestTypes('sessions/TOGGLE_FAVORITE');
@ -160,11 +167,82 @@ const reducer = (state = initialState, action: IAction) => {
}
});
});
return state
.set('current', session)
.set('eventsIndex', matching)
.set('visitedEvents', visitedEvents)
.set('host', visitedEvents[0] && visitedEvents[0].host);
}
case FETCHV2.SUCCESS: {
const session = new Session(action.data);
return state
.set('current', session)
.set('eventsIndex', matching)
.set('visitedEvents', visitedEvents)
.set('host', visitedEvents[0] && visitedEvents[0].host);
}
case FETCH_EVENTS.SUCCESS: {
const {
errors,
events,
issues,
resources,
stackEvents,
userEvents
} = action.data as { errors: any[], events: any[], issues: any[], resources: any[], stackEvents: any[], userEvents: EventData[] };
const filterEvents = action.filter.events as Record<string, any>[];
const session = state.get('current') as Session;
const matching: number[] = [];
const visitedEvents: Location[] = [];
const tmpMap = new Set();
events.forEach((event) => {
// @ts-ignore assume that event is LocationEvent
if (event.type === 'LOCATION' && !tmpMap.has(event.url)) {
// @ts-ignore assume that event is LocationEvent
tmpMap.add(event.url);
// @ts-ignore assume that event is LocationEvent
visitedEvents.push(event);
}
});
filterEvents.forEach(({ key, operator, value }) => {
events.forEach((e, index) => {
if (key === e.type) {
// @ts-ignore assume that event is LocationEvent
const val = e.type === 'LOCATION' ? e.url : e.value;
if (operator === 'is' && value === val) {
matching.push(index);
}
if (operator === 'contains' && val.includes(value)) {
matching.push(index);
}
}
});
});
const newSession = session.addEvents(
events,
errors,
issues,
resources,
stackEvents,
userEvents
);
const forceUpdate = state.set('current', {})
return forceUpdate
.set('current', newSession)
.set('eventsIndex', matching)
.set('visitedEvents', visitedEvents)
.set('host', visitedEvents[0] && visitedEvents[0].host);
}
case FETCH_NOTES.SUCCESS: {
const notes = action.data;
if (notes.length > 0) {
const session = state.get('current') as Session;
const newSession = session.addNotes(notes);
return state.set('current', newSession);
}
return state
}
case FETCH_FAVORITE_LIST.SUCCESS:
return state.set('favoriteList', action.data.map(s => new Session(s)));
@ -321,6 +399,59 @@ export const fetch =
});
};
function parseError(e: any) {
try {
return [...JSON.parse(e).errors] || [];
} catch {
return Array.isArray(e) ? e : [e];
}
}
// implementing custom middleware-like request to keep the behavior
// TODO: move all to mobx
export const fetchV2 = (sessionId: string) =>
(dispatch, getState) => {
const apiClient = new APIClient()
const apiGet = (url: string, dispatch: any, FAILURE: string) => apiClient.get(url)
.then(async (response) => {
if (response.status === 403) {
dispatch({ type: FETCH_ACCOUNT.FAILURE });
}
if (!response.ok) {
const text = await response.text();
return Promise.reject(text);
}
return response.json();
})
.then((json) => json || {})
.catch(async (e) => {
const data = await e.response?.json();
logger.error('Error during API request. ', e);
return dispatch({ type: FAILURE, errors: data ? parseError(data.errors) : [] });
});
const filter = getState().getIn(['filters', 'appliedFilter'])
apiGet(`/sessions/${sessionId}/replay`, dispatch, FETCH.FAILURE)
.then(async ({ jwt, errors, data }) => {
if (errors) {
dispatch({ type: FETCH.FAILURE, errors, data });
} else {
dispatch({ type: FETCHV2.SUCCESS, data, ...filter });
let [events, notes] = await Promise.all([
apiGet(`/sessions/${sessionId}/events`, dispatch, FETCH_EVENTS.FAILURE),
apiGet(`/sessions/${sessionId}/notes`, dispatch, FETCH_NOTES.FAILURE),
]);
dispatch({ type: FETCH_EVENTS.SUCCESS, data: events.data, filter });
dispatch({ type: FETCH_NOTES.SUCCESS, data: notes.data });
}
if (jwt) {
dispatch({ type: UPDATE_JWT, data: jwt });
}
});
}
export function clearCurrentSession() {
return {
type: CLEAR_CURRENT_SESSION

View file

@ -108,7 +108,7 @@ export default class MessageManager {
private scrollManager: ListWalker<SetViewportScroll> = new ListWalker();
public readonly decoder = new Decoder();
private readonly lists: Lists;
private lists: Lists;
private activityManager: ActivityManager | null = null;
@ -137,6 +137,18 @@ export default class MessageManager {
this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live
}
public updateLists(lists: Partial<InitialLists>) {
this.lists = new Lists(lists)
lists?.event?.forEach((e: Record<string, string>) => {
if (e.type === EVENT_TYPES.LOCATION) {
this.locationEventManager.append(e);
}
})
this.state.update({ ...this.lists.getFullListsState() });
}
private setCSSLoading = (cssLoading: boolean) => {
this.screen.displayFrame(!cssLoading)
this.state.update({ cssLoading, ready: !this.state.get().messagesLoading && !cssLoading })

View file

@ -68,6 +68,21 @@ export default class WebPlayer extends Player {
}
updateLists = (session: any) => {
let lists = {
event: session.events || [],
stack: session.stackEvents || [],
exceptions: session.errors?.map(({ name, ...rest }: any) =>
Log({
level: LogLevel.ERROR,
value: name,
...rest,
})
) || [],
}
this.messageManager.updateLists(lists)
}
attach = (parent: HTMLElement, isClickmap?: boolean) => {
this.screen.attach(parent)
if (!isClickmap) {

View file

@ -32,7 +32,7 @@ interface InputEvent extends IEvent {
value: string;
}
interface LocationEvent extends IEvent {
export interface LocationEvent extends IEvent {
url: string;
host: string;
pageLoad: boolean;

View file

@ -3,7 +3,8 @@ import SessionEvent, { TYPES, EventData, InjectedEvent } from './event';
import StackEvent from './stackEvent';
import SessionError, { IError } from './error';
import Issue, { IIssue } from './issue';
import { Note } from 'App/services/NotesService'
import { Note } from 'App/services/NotesService';
import { toJS } from 'mobx';
const HASH_MOD = 1610612741;
const HASH_P = 53;
@ -19,70 +20,70 @@ function hashString(s: string): number {
}
export interface ISession {
sessionId: string,
pageTitle: string,
active: boolean,
siteId: string,
projectKey: string,
peerId: string,
live: boolean,
startedAt: number,
duration: number,
events: InjectedEvent[],
stackEvents: StackEvent[],
metadata: [],
favorite: boolean,
filterId?: string,
domURL: string[],
devtoolsURL: string[],
sessionId: string;
pageTitle: string;
active: boolean;
siteId: string;
projectKey: string;
peerId: string;
live: boolean;
startedAt: number;
duration: number;
events: InjectedEvent[];
stackEvents: StackEvent[];
metadata: [];
favorite: boolean;
filterId?: string;
domURL: string[];
devtoolsURL: string[];
/**
* @deprecated
*/
mobsUrl: string[],
userBrowser: string,
userBrowserVersion: string,
userCountry: string,
userDevice: string,
userDeviceType: string,
isMobile: boolean,
userOs: string,
userOsVersion: string,
userId: string,
userAnonymousId: string,
userUuid: string,
userDisplayName: string,
userNumericHash: number,
viewed: boolean,
consoleLogCount: number,
eventsCount: number,
pagesCount: number,
errorsCount: number,
issueTypes: string[],
issues: [],
referrer: string | null,
userDeviceHeapSize: number,
userDeviceMemorySize: number,
errors: SessionError[],
crashes?: [],
socket: string,
isIOS: boolean,
revId: string | null,
agentIds?: string[],
isCallActive?: boolean,
agentToken: string,
notes: Note[],
notesWithEvents: Array<Note | InjectedEvent>,
fileKey: string,
platform: string,
projectId: string,
startTs: number,
timestamp: number,
backendErrors: number,
consoleErrors: number,
sessionID?: string,
userID: string,
userUUID: string,
userEvents: any[],
mobsUrl: string[];
userBrowser: string;
userBrowserVersion: string;
userCountry: string;
userDevice: string;
userDeviceType: string;
isMobile: boolean;
userOs: string;
userOsVersion: string;
userId: string;
userAnonymousId: string;
userUuid: string;
userDisplayName: string;
userNumericHash: number;
viewed: boolean;
consoleLogCount: number;
eventsCount: number;
pagesCount: number;
errorsCount: number;
issueTypes: string[];
issues: IIssue[];
referrer: string | null;
userDeviceHeapSize: number;
userDeviceMemorySize: number;
errors: SessionError[];
crashes?: [];
socket: string;
isIOS: boolean;
revId: string | null;
agentIds?: string[];
isCallActive?: boolean;
agentToken: string;
notes: Note[];
notesWithEvents: Array<Note | InjectedEvent>;
fileKey: string;
platform: string;
projectId: string;
startTs: number;
timestamp: number;
backendErrors: number;
consoleErrors: number;
sessionID?: string;
userID: string;
userUUID: string;
userEvents: any[];
}
const emptyValues = {
@ -102,67 +103,67 @@ const emptyValues = {
notes: [],
metadata: {},
startedAt: 0,
}
};
export default class Session {
sessionId: ISession["sessionId"]
pageTitle: ISession["pageTitle"]
active: ISession["active"]
siteId: ISession["siteId"]
projectKey: ISession["projectKey"]
peerId: ISession["peerId"]
live: ISession["live"]
startedAt: ISession["startedAt"]
duration: ISession["duration"]
events: ISession["events"]
stackEvents: ISession["stackEvents"]
metadata: ISession["metadata"]
favorite: ISession["favorite"]
filterId?: ISession["filterId"]
domURL: ISession["domURL"]
devtoolsURL: ISession["devtoolsURL"]
sessionId: ISession['sessionId'];
pageTitle: ISession['pageTitle'];
active: ISession['active'];
siteId: ISession['siteId'];
projectKey: ISession['projectKey'];
peerId: ISession['peerId'];
live: ISession['live'];
startedAt: ISession['startedAt'];
duration: ISession['duration'];
events: ISession['events'];
stackEvents: ISession['stackEvents'];
metadata: ISession['metadata'];
favorite: ISession['favorite'];
filterId?: ISession['filterId'];
domURL: ISession['domURL'];
devtoolsURL: ISession['devtoolsURL'];
/**
* @deprecated
*/
mobsUrl: ISession["mobsUrl"]
userBrowser: ISession["userBrowser"]
userBrowserVersion: ISession["userBrowserVersion"]
userCountry: ISession["userCountry"]
userDevice: ISession["userDevice"]
userDeviceType: ISession["userDeviceType"]
isMobile: ISession["isMobile"]
userOs: ISession["userOs"]
userOsVersion: ISession["userOsVersion"]
userId: ISession["userId"]
userAnonymousId: ISession["userAnonymousId"]
userUuid: ISession["userUuid"]
userDisplayName: ISession["userDisplayName"]
userNumericHash: ISession["userNumericHash"]
viewed: ISession["viewed"]
consoleLogCount: ISession["consoleLogCount"]
eventsCount: ISession["eventsCount"]
pagesCount: ISession["pagesCount"]
errorsCount: ISession["errorsCount"]
issueTypes: ISession["issueTypes"]
issues: ISession["issues"]
referrer: ISession["referrer"]
userDeviceHeapSize: ISession["userDeviceHeapSize"]
userDeviceMemorySize: ISession["userDeviceMemorySize"]
errors: ISession["errors"]
crashes?: ISession["crashes"]
socket: ISession["socket"]
isIOS: ISession["isIOS"]
revId: ISession["revId"]
agentIds?: ISession["agentIds"]
isCallActive?: ISession["isCallActive"]
agentToken: ISession["agentToken"]
notes: ISession["notes"]
notesWithEvents: ISession["notesWithEvents"]
fileKey: ISession["fileKey"]
durationSeconds: number
mobsUrl: ISession['mobsUrl'];
userBrowser: ISession['userBrowser'];
userBrowserVersion: ISession['userBrowserVersion'];
userCountry: ISession['userCountry'];
userDevice: ISession['userDevice'];
userDeviceType: ISession['userDeviceType'];
isMobile: ISession['isMobile'];
userOs: ISession['userOs'];
userOsVersion: ISession['userOsVersion'];
userId: ISession['userId'];
userAnonymousId: ISession['userAnonymousId'];
userUuid: ISession['userUuid'];
userDisplayName: ISession['userDisplayName'];
userNumericHash: ISession['userNumericHash'];
viewed: ISession['viewed'];
consoleLogCount: ISession['consoleLogCount'];
eventsCount: ISession['eventsCount'];
pagesCount: ISession['pagesCount'];
errorsCount: ISession['errorsCount'];
issueTypes: ISession['issueTypes'];
issues: Issue[];
referrer: ISession['referrer'];
userDeviceHeapSize: ISession['userDeviceHeapSize'];
userDeviceMemorySize: ISession['userDeviceMemorySize'];
errors: ISession['errors'];
crashes?: ISession['crashes'];
socket: ISession['socket'];
isIOS: ISession['isIOS'];
revId: ISession['revId'];
agentIds?: ISession['agentIds'];
isCallActive?: ISession['isCallActive'];
agentToken: ISession['agentToken'];
notes: ISession['notes'];
notesWithEvents: ISession['notesWithEvents'];
fileKey: ISession['fileKey'];
durationSeconds: number;
constructor(plainSession?: ISession) {
const sessionData = plainSession || (emptyValues as unknown as ISession)
const sessionData = plainSession || (emptyValues as unknown as ISession);
const {
startTs = 0,
timestamp = 0,
@ -179,7 +180,7 @@ export default class Session {
mobsUrl = [],
notes = [],
...session
} = sessionData
} = sessionData;
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
const durationSeconds = duration.valueOf();
const startedAt = +startTs || +timestamp;
@ -188,44 +189,46 @@ export default class Session {
const userDeviceType = session.userDeviceType || 'other';
const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType);
const events: InjectedEvent[] = []
const rawEvents: (EventData & { key: number })[] = []
const events: InjectedEvent[] = [];
const rawEvents: (EventData & { key: number })[] = [];
if (session.events?.length) {
(session.events as EventData[]).forEach((event: EventData, k) => {
const time = event.timestamp - startedAt
const time = event.timestamp - startedAt;
if (event.type !== TYPES.CONSOLE && time <= durationSeconds) {
const EventClass = SessionEvent({ ...event, time, key: k })
const EventClass = SessionEvent({ ...event, time, key: k });
if (EventClass) {
events.push(EventClass);
}
rawEvents.push({ ...event, time, key: k });
}
})
});
}
const stackEventsList: StackEvent[] = []
const stackEventsList: StackEvent[] = [];
if (stackEvents?.length || session.userEvents?.length) {
const mergedArrays = [...stackEvents, ...session.userEvents]
.sort((a, b) => a.timestamp - b.timestamp)
.map((se) => new StackEvent({ ...se, time: se.timestamp - startedAt }))
.map((se) => new StackEvent({ ...se, time: se.timestamp - startedAt }));
stackEventsList.push(...mergedArrays);
}
const exceptions = (errors as IError[]).map(e => new SessionError(e)) || [];
const exceptions = (errors as IError[]).map((e) => new SessionError(e)) || [];
const issuesList = (issues as IIssue[]).map(
(i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })) || [];
const issuesList =
(issues as IIssue[]).map(
(i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })
) || [];
const rawNotes = notes;
const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => {
// @ts-ignore just in case
const aTs = a.timestamp || a.time;
// @ts-ignore
const bTs = b.timestamp || b.time;
const notesWithEvents =
[...rawEvents, ...notes].sort((a, b) => {
// @ts-ignore just in case
const aTs = a.timestamp || a.time;
// @ts-ignore
const bTs = b.timestamp || b.time;
return aTs - bTs;
}) || [];
return aTs - bTs;
}) || [];
Object.assign(this, {
...session,
@ -242,11 +245,11 @@ export default class Session {
durationSeconds,
userNumericHash: hashString(
session.userId ||
session.userAnonymousId ||
session.userUuid ||
session.userID ||
session.userUUID ||
''
session.userAnonymousId ||
session.userUuid ||
session.userID ||
session.userUUID ||
''
),
userDisplayName:
session.userId || session.userAnonymousId || session.userID || 'Anonymous User',
@ -258,47 +261,75 @@ export default class Session {
devtoolsURL,
notes,
notesWithEvents: notesWithEvents,
})
});
}
addIssues(issues: IIssue[]) {
const issuesList = issues.map(
(i, k) => new Issue({ ...i, time: i.timestamp - this.startedAt, key: k })) || [];
addEvents(
sessionEvents: EventData[],
errors: any[],
issues: any[],
resources: any[],
userEvents: any[],
stackEvents: any[]
) {
const exceptions = (errors as IError[]).map((e) => new SessionError(e)) || [];
const issuesList =
(issues as IIssue[]).map(
(i, k) => new Issue({ ...i, time: i.timestamp - this.startedAt, key: k })
) || [];
const stackEventsList: StackEvent[] = [];
if (stackEvents?.length || userEvents?.length) {
const mergedArrays = [...stackEvents, ...userEvents]
.sort((a, b) => a.timestamp - b.timestamp)
.map((se) => new StackEvent({ ...se, time: se.timestamp - this.startedAt }));
stackEventsList.push(...mergedArrays);
}
// @ts-ignore
this.issues = issuesList;
}
addEvents(sessionEvents: EventData[], sessionNotes: Note[]) {
const events: InjectedEvent[] = []
const rawEvents: (EventData & { key: number })[] = []
const events: InjectedEvent[] = [];
const rawEvents: (EventData & { key: number })[] = [];
if (sessionEvents.length) {
sessionEvents.forEach((event, k) => {
const time = event.timestamp - this.startedAt
const time = event.timestamp - this.startedAt;
if (event.type !== TYPES.CONSOLE && time <= this.durationSeconds) {
const EventClass = SessionEvent({ ...event, time, key: k })
const EventClass = SessionEvent({ ...event, time, key: k });
if (EventClass) {
events.push(EventClass);
}
rawEvents.push({ ...event, time, key: k });
}
})
});
}
const rawNotes = sessionNotes;
const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => {
// @ts-ignore just in case
const aTs = a.timestamp || a.time;
// @ts-ignore
const bTs = b.timestamp || b.time;
return aTs - bTs;
}) || [];
this.events = events;
// @ts-ignore
this.notesWithEvents = notesWithEvents;
this.notes = sessionNotes
this.events = events
this.notesWithEvents = rawEvents;
this.errors = exceptions;
this.issues = issuesList;
// @ts-ignore legacy code? no idea
this.resources = resources;
this.stackEvents = stackEventsList;
return this;
}
}
addNotes(sessionNotes: Note[]) {
// @ts-ignore
this.notesWithEvents =
[...this.notesWithEvents, ...sessionNotes].sort((a, b) => {
// @ts-ignore just in case
const aTs = a.timestamp || a.time;
// @ts-ignore
const bTs = b.timestamp || b.time;
return aTs - bTs;
}) || [];
this.notes = sessionNotes;
return this;
}
toJS() {
return { ...toJS(this) };
}
}