openreplay/frontend/app/mstore/sessionStore.ts
2024-11-19 17:27:51 +01:00

537 lines
14 KiB
TypeScript

import { action, makeAutoObservable, observable, runInAction } from 'mobx';
import { sessionService } from 'App/services';
import { Note } from 'App/services/NotesService';
import Session from 'Types/session';
import ErrorStack from 'Types/session/errorStack';
import { InjectedEvent, Location } from 'Types/session/event';
import {
cleanSessionFilters,
compareJsonObjects,
getRE,
getSessionFilter,
setSessionFilter,
} from 'App/utils';
import { loadFile } from 'App/player/web/network/loadFiles';
import { LAST_7_DAYS } from 'Types/app/period';
import { filterMap } from 'App/mstore/searchStore';
import { clean as cleanParams } from "../api_client";
import { searchStore, searchStoreLive } from "./index";
import { getDateRangeFromValue } from 'App/dateRange';
const range = getDateRangeFromValue(LAST_7_DAYS);
const defaultDateFilters = {
url: '',
rangeValue: LAST_7_DAYS,
startDate: range.start?.toMillis(),
endDate: range.end?.toMillis(),
};
class UserFilter {
endDate: number = new Date().getTime();
startDate: number = new Date().getTime() - 24 * 60 * 60 * 1000;
rangeName: string = LAST_7_DAYS;
filters: any = [];
page: number = 1;
limit: number = 10;
period: any = { rangeName: LAST_7_DAYS };
constructor() {
makeAutoObservable(this);
}
update(key: string, value: any) {
// @ts-ignore
this[key] = value;
if (key === 'period') {
this.startDate = this.period.start;
this.endDate = this.period.end;
}
}
setFilters(filters: any[]) {
this.filters = filters;
}
setPage(page: number) {
this.page = page;
}
toJson() {
return {
endDate: this.period.end,
startDate: this.period.start,
filters: this.filters.map(filterMap),
page: this.page,
limit: this.limit,
};
}
}
interface BaseDevState {
index: number;
filter: string;
activeTab: string;
isError: boolean;
}
class DevTools {
network: BaseDevState;
stackEvent: BaseDevState;
console: BaseDevState;
constructor() {
this.network = { index: 0, filter: '', activeTab: 'ALL', isError: false };
this.stackEvent = {
index: 0,
filter: '',
activeTab: 'ALL',
isError: false,
};
this.console = { index: 0, filter: '', activeTab: 'ALL', isError: false };
makeAutoObservable(this, {
update: action,
});
}
update(key: string, value: any) {
// @ts-ignore
this[key] = Object.assign(this[key], value);
}
}
export default class SessionStore {
userFilter: UserFilter = new UserFilter();
devTools: DevTools = new DevTools();
list: Session[] = [];
sessionIds: string[] = [];
current = new Session();
total = 0;
totalLiveSessions = 0;
favoriteList: Session[] = [];
activeTab = { name: 'All', type: 'all' }
timezone = 'local';
errorStack: ErrorStack[] = [];
eventsIndex: number[] = [];
sourcemapUploaded = true;
filteredEvents: InjectedEvent[] | null = null;
eventsQuery = '';
liveSessions: Session[] = [];
visitedEvents: Location[] = [];
insights: any[] = [];
insightsFilters = defaultDateFilters;
host = '';
sessionPath: Record<string, any> = {};
lastPlayedSessionId: string = '';
timeLineTooltip = {
time: 0,
offset: 0,
isVisible: false,
localTime: '',
userTime: '',
};
createNoteTooltip = { time: 0, isVisible: false, isEdit: false, note: null };
previousId = '';
nextId = '';
userTimezone = '';
prefetchedMobUrls: Record<string, { data: Uint8Array; entryNum: number }> = {};
prefetched: boolean = false;
fetchFailed: boolean = false;
loadingLiveSessions: boolean = false;
loadingSessions: boolean = false;
loadingSessionData: boolean = false;
constructor() {
makeAutoObservable(this);
}
get currentId() {
return this.current.sessionId;
}
setUserTimezone = (timezone: string) => {
this.userTimezone = timezone;
}
resetUserFilter = () => {
this.userFilter = new UserFilter();
}
getFirstMob = async (sessionId: string) => {
const { domURL } = await sessionService.getFirstMobUrl(sessionId);
await loadFile(domURL[0], (data) => this.setPrefetchedMobUrl(sessionId, data));
}
setPrefetchedMobUrl = (sessionId: string, fileData: Uint8Array) => {
const keys = Object.keys(this.prefetchedMobUrls);
const toLimit = 10 - keys.length;
if (toLimit < 0) {
const oldest = keys.sort(
(a, b) =>
this.prefetchedMobUrls[a].entryNum - this.prefetchedMobUrls[b].entryNum
)[0];
delete this.prefetchedMobUrls[oldest];
}
const nextEntryNum =
keys.length > 0
? Math.max(...keys.map((key) => this.prefetchedMobUrls[key].entryNum || 0)) + 1
: 0;
this.prefetchedMobUrls[sessionId] = {
data: fileData,
entryNum: nextEntryNum,
};
}
getSessions = (filter: any): Promise<any> => {
return new Promise((resolve, reject) => {
sessionService
.getSessions(filter.toJson?.() || filter)
.then((response: any) => {
resolve({
sessions: response.sessions.map((session: any) => new Session(session)),
total: response.total,
});
})
.catch((error: any) => {
reject(error);
});
});
}
fetchLiveSessions = async (params = {}) => {
runInAction(() => {
this.loadingLiveSessions = true;
})
try {
const data: any = await sessionService.getLiveSessions(params);
this.liveSessions = data.sessions.map((session: any) => new Session({ ...session, live: true }));
this.totalLiveSessions = data.total;
} catch (e) {
console.error(e);
} finally {
runInAction(() => {
this.loadingLiveSessions = false;
});
}
}
fetchSessions = async (params = {}, force = false) => {
runInAction(() => {
this.loadingSessions = true;
})
try {
if (!force) {
const oldFilters = getSessionFilter();
if (compareJsonObjects(oldFilters, cleanSessionFilters(params))) {
return;
}
}
setSessionFilter(cleanSessionFilters(params));
const data = await sessionService.getSessions(params);
const list = data.sessions.map((s) => new Session(s));
runInAction(() => {
this.list = list;
this.total = data.total;
this.sessionIds = data.sessions.map((s) => s.sessionId);
this.favoriteList = list.filter((s) => s.favorite);
})
} catch (e) {
console.error(e);
} finally {
runInAction(() => {
this.loadingSessions = false;
});
}
}
clearAll = () => {
this.list = [];
this.clearCurrentSession();
}
fetchSessionData = async (sessionId: string, isLive = false) => {
try {
const filter = isLive ? searchStoreLive.instance : searchStore.instance;
const data = await sessionService.getSessionInfo(sessionId, isLive);
const eventsData: Record<string, any[]> = {};
try {
const evData = await sessionService.getSessionEvents(sessionId);
Object.assign(eventsData, evData);
} catch (e) {
console.error('Failed to fetch events', e);
}
const {
errors = [],
events = [],
issues = [],
crashes = [],
resources = [],
stackEvents = [],
userEvents = [],
userTesting = [],
} = eventsData;
const filterEvents = filter.events as Record<string, any>[];
const matching: number[] = [];
const visitedEvents: Location[] = [];
const tmpMap = new Set();
events.forEach((event) => {
if (event.type === 'LOCATION' && !tmpMap.has(event.url)) {
tmpMap.add(event.url);
visitedEvents.push(event as Location);
}
});
filterEvents.forEach(({ key, operator, value }) => {
events.forEach((e, index) => {
if (key === e.type) {
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);
}
}
});
});
runInAction(() => {
const session = new Session(data);
session.addEvents(
events,
crashes,
errors,
issues,
resources,
userEvents,
stackEvents,
userTesting
);
this.current = session;
this.eventsIndex = matching;
this.visitedEvents = visitedEvents;
this.host = visitedEvents[0]?.host || '';
this.prefetched = false;
})
} catch (e) {
console.error(e);
this.fetchFailed = true;
}
}
fetchNotes = async (sessionId: string) => {
try {
const notes = await sessionService.getSessionNotes(sessionId);
if (notes.length > 0) {
this.current = this.current.addNotes(notes);
}
} catch (e) {
console.error(e);
}
}
fetchFavoriteList = async () => {
try {
const data = await sessionService.getFavoriteSessions();
this.favoriteList = data.map((s: any) => new Session(s));
} catch (e) {
console.error(e);
}
}
fetchSessionClickmap = async (sessionId: string, params: any) => {
try {
const data = await sessionService.getSessionClickMap(sessionId, params);
this.insights = data;
} catch (e) {
console.error(e);
}
}
setAutoplayValues = () => {
const currentId = this.current.sessionId;
const currentIndex = this.sessionIds.indexOf(currentId);
this.previousId = this.sessionIds[currentIndex - 1] || '';
this.nextId = this.sessionIds[currentIndex + 1] || '';
}
setEventQuery = (filter: { query: string }) => {
const events = this.current.events;
const query = filter.query;
const searchRe = getRE(query, 'i');
const filteredEvents = query
? events.filter(
(e) =>
searchRe.test(e.url) ||
searchRe.test(e.value) ||
searchRe.test(e.label) ||
searchRe.test(e.type) ||
(e.type === 'LOCATION' && searchRe.test('visited'))
)
: null;
this.filteredEvents = filteredEvents;
this.eventsQuery = query;
}
toggleFavorite = async (id: string) => {
try {
const r = await sessionService.toggleFavorite(id);
if (r.ok) {
const list = this.list;
const current = this.current;
const sessionIdx = list.findIndex(({ sessionId }) => sessionId === id);
const session = list[sessionIdx];
const wasInFavorite =
this.favoriteList.findIndex(({ sessionId }) => sessionId === id) > -1;
if (session) {
session.favorite = !wasInFavorite;
this.list[sessionIdx] = session;
}
if (current.sessionId === id) {
this.current.favorite = !wasInFavorite;
}
if (wasInFavorite) {
this.favoriteList = this.favoriteList.filter(
({ sessionId }) => sessionId !== id
);
} else {
this.favoriteList.push(session);
}
} else {
console.error(r);
}
} catch (e) {
console.error(e);
}
}
sortSessions = (sortKey: string, sign: number = 1) => {
const comparator = (s1: Session, s2: Session) => {
// @ts-ignore
let diff = s1[sortKey] - s2[sortKey];
diff = diff === 0 ? s1.startedAt - s2.startedAt : diff;
return sign * diff;
};
this.list = this.list.slice().sort(comparator);
this.favoriteList = this.favoriteList.slice().sort(comparator);
}
setActiveTab = (tab: { type: string, name: string }) => {
const list =
tab.type === 'all'
? this.list
: this.list.filter((s) => s.issueTypes.includes(tab.type));
this.activeTab = tab;
this.sessionIds = list.map((s) => s.sessionId);
}
setTimezone = (tz: string) => {
this.timezone = tz;
}
fetchInsights = async (params = {}) => {
try {
const data = await sessionService.getClickMap(params);
this.insights = data.sort((a: any, b: any) => b.count - a.count);
} catch (e) {
console.error(e);
}
}
setTimelineTooltip = (tp: {
time: number;
offset: number;
isVisible: boolean;
localTime: string;
userTime?: string;
}) => {
this.timeLineTooltip = tp;
}
setCreateNoteTooltip = (noteTooltip: any) => {
this.createNoteTooltip = noteTooltip;
}
setEditNoteTooltip = (noteTooltip: any) => {
this.createNoteTooltip = noteTooltip;
}
filterOutNote = (noteId: string) => {
this.current.notesWithEvents = this.current.notesWithEvents.filter((item) => {
if ('noteId' in item) {
return item.noteId !== noteId;
}
return true;
});
}
updateNote = (note: Note) => {
const noteIndex = this.current.notesWithEvents.findIndex((item) => {
if ('noteId' in item) {
return item.noteId === note.noteId;
}
return false;
});
if (noteIndex !== -1) {
this.current.notesWithEvents[noteIndex] = note;
}
}
setSessionPath = (path = {}) => {
this.sessionPath = path;
}
updateLastPlayedSession = (sessionId: string) => {
const sIndex = this.list.findIndex((s) => s.sessionId === sessionId);
if (sIndex !== -1) {
this.list[sIndex].viewed = true;
}
}
clearCurrentSession = () => {
this.current = new Session();
this.eventsIndex = [];
this.visitedEvents = [];
this.host = '';
}
prefetchSession = (sessionData: Session) => {
this.current = sessionData;
this.prefetched = true;
}
customSetSessions = (data: any) => {
this.liveSessions = data.sessions.map((s: any) => new Session(s));
this.totalLiveSessions = data.total
}
fetchAutoplayList = async (page: number) => {
try {
const filter = searchStore.instance.toSearch();
setSessionFilter(cleanSessionFilters(filter));
const data = await sessionService.getAutoplayList({ ...filter, page: page });
const ids = data.map((i: any) => i.sessionId + '').filter((i, index) => !this.sessionIds.includes(i));
this.sessionIds = this.sessionIds.concat(ids);
} catch (e) {
console.error(e);
}
};
clearList = () => {
this.list = [];
this.total = 0;
this.sessionIds = [];
}
setLastPlayedSessionId = (sessionId: string) => {
this.lastPlayedSessionId = sessionId;
}
}