change(ui): widget types

This commit is contained in:
sylenien 2023-01-05 11:20:35 +01:00 committed by Delirium
parent e6d46c3ea1
commit c2d9a9768a
34 changed files with 483 additions and 274 deletions

View file

@ -124,7 +124,7 @@ const reducer = (state = initialState, action: IAction) => {
return state.set('filteredEvents', filteredEvents).set('eventsQuery', query);
}
case FETCH.SUCCESS: {
// TODO: more common.. or TEMP
// TODO: more common.. or TEMP filters', 'appliedFilter
const events = action.filter.events;
const session = new Session(action.data);
@ -191,17 +191,10 @@ const reducer = (state = initialState, action: IAction) => {
};
return state.update('list', (list: Session[]) => list.sort(comparator)).update('favoriteList', (list: Session[]) => list.sort(comparator));
}
case REDEFINE_TARGET: {
// TODO: update for list
const { label, path } = action.target;
return state.updateIn(['current', 'events'], (list) =>
list.map((event) => (event.target && event.target.path === path ? event.setIn(['target', 'label'], label) : event))
);
}
case SET_ACTIVE_TAB:
const allList = action.tab.type === 'all' ? state.get('list') : state.get('list').filter((s) => s.issueTypes.includes(action.tab.type));
return state.set('activeTab', action.tab).set('sessionIds', allList.map(({ sessionId }) => sessionId).toJS());
return state.set('activeTab', action.tab).set('sessionIds', allList.map(({ sessionId }) => sessionId));
case SET_TIMEZONE:
return state.set('timezone', action.timezone);
case TOGGLE_CHAT_WINDOW:

View file

@ -8,6 +8,8 @@ import ErrorStack from 'Types/session/errorStack';
import { Location, InjectedEvent } from 'Types/session/event'
import { getDateRangeFromValue } from "App/dateRange";
import { getRE, setSessionFilter, getSessionFilter, compareJsonObjects, cleanSessionFilters } from 'App/utils';
import store from 'App/store'
import { Note } from "App/services/NotesService";
class UserFilter {
endDate: number = new Date().getTime();
@ -110,12 +112,13 @@ export default class SessionStore {
showChatWindow = false
liveSessions: Session[] = []
visitedEvents = []
insights = []
insights: any[] = []
insightFilters = defaultDateFilters
host = ''
funnelPage = {}
timelinePointer = null
sessionPath = {}
/** @Deprecated */
timelinePointer = {}
sessionPath = {}
lastPlayedSessionId: string
timeLineTooltip = { time: 0, offset: 0, isVisible: false, timeStr: '' }
createNoteTooltip = { time: 0, isVisible: false, isEdit: false, note: null }
@ -174,7 +177,43 @@ export default class SessionStore {
this.total = data.total;
this.sessionIds = data.sessions.map(s => s.sessionId);
this.favoriteList = list.filter(s => s.favorite);
} catch(e) {
} catch (e) {
console.error(e)
}
}
async fetchSessionInfo(sessionId: string, isLive = false) {
try {
const { events } = store.getState().getIn(['filters', 'appliedFilter']);
const data = await sessionService.getSessionInfo(sessionId, isLive)
const session = new Session(data)
const matching: number[] = [];
const visitedEvents: Location[] = [];
const tmpMap: Set<string> = new Set();
session.events.forEach((event) => {
if (event.type === 'LOCATION' && !tmpMap.has(event.url)) {
tmpMap.add(event.url);
visitedEvents.push(event);
}
});
(events as {}[]).forEach(({ key, operator, value }: any) => {
session.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);
}
}
});
});
} catch (e) {
console.error(e)
}
}
@ -194,7 +233,7 @@ export default class SessionStore {
const data = await sessionService.getAutoplayList(params);
const list = [...this.sessionIds, ...data.map(s => s.sessionId)]
this.sessionIds = list.filter((id, ind) => list.indexOf(id) === ind);
} catch(e) {
} catch (e) {
console.error(e)
}
}
@ -221,6 +260,147 @@ export default class SessionStore {
) : null;
this.filteredEvents = filteredEvents
this.eventsQuery = query
this.eventsQuery = query
}
async toggleFavorite(id: string) {
try {
const r = await sessionService.toggleFavorite(id)
if (r.success) {
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 && !wasInFavorite) {
session.favorite = true
this.list[sessionIdx] = session
}
if (current.sessionId === id) {
this.current.favorite = !wasInFavorite
}
if (session) {
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) {
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.sort(comparator)
this.favoriteList = this.favoriteList.sort(comparator)
return;
}
setActiveTab(tab: { type: string }) {
const list = tab.type === 'all'
? this.list : this.list.filter(s => s.issueTypes.includes(tab.type))
// @ts-ignore
this.activeTab = tab
this.sessionIds = list.map(s => s.sessionId)
}
setTimezone(tz: string) {
this.timezone = tz;
}
toggleChatWindow(isActive: boolean) {
this.showChatWindow = isActive
}
async fetchInsights(filters = {}) {
try {
const data = await sessionService.getClickMap(filters)
this.insights = data
} catch (e) {
console.error(e)
}
}
setFunnelPage(page = {}) {
this.funnelPage = page || false
}
/* @deprecated */
setTimelinePointer(pointer: {}) {
this.timelinePointer = pointer
}
setTimelineTooltip(tp: { time: number, offset: number, isVisible: boolean, timeStr: string }) {
this.timeLineTooltip = tp
}
setEditNoteTooltip(tp: { time: number, isVisible: boolean, isEdit: boolean, note: any }) {
this.createNoteTooltip = tp
}
filterOutNote(noteId: string) {
const current = this.current
current.notesWithEvents = current.notesWithEvents.filter(n => {
if ('noteId' in item) {
return item.noteId !== noteId
}
return true
})
this.current = current
}
addNote(note: Note) {
const current = this.current
current.notesWithEvents.push(note)
current.notesWithEvents.sort((a,b) => {
const aTs = a.time || a.timestamp
const bTs = b.time || b.timestamp
return aTs - bTs
})
this.current = current
}
updateNote(note: Note) {
const noteIndex = this.current.notesWithEvents.findIndex(item => {
if ('noteId' in item) {
return item.noteId === note.noteId
}
return false
})
this.current.notesWithEvents[noteIndex] = note
}
setSessionPath(path = {}) {
this.sessionPath = path
}
setLastPlayed(sessionId: string) {
const list = this.list
const sIndex = list.findIndex((s) => s.sessionId === sessionId)
if (sIndex !== -1) {
this.list[sIndex].viewed = true
}
}
}

View file

@ -57,11 +57,25 @@ export default class SettingsService {
.catch(e => Promise.reject(e))
}
getAutoplayList(params = {}): Promise<{ sessionId: string}[]> {
getAutoplayList(params = {}): Promise<{ sessionId: string }[]> {
return this.client
.post('/sessions/search/ids', cleanParams(params))
.then(r => r.json())
.then(j => j.data || [])
.catch(e => Promise.reject(e))
}
toggleFavorite(sessionId: string): Promise<any> {
return this.client
.get(`/sessions/${sessionId}/favorite`)
.catch(Promise.reject)
}
getClickMap(params = {}): Promise<any[]> {
return this.client
.post('/heatmaps/url', params)
.then(r => r.json())
.then(j => j.data || [])
.catch(Promise.reject)
}
}

View file

@ -1,13 +0,0 @@
import Record from 'Types/Record';
export default Record({
method: '',
urlHostpath: '',
allRequests: '',
'4xx': '',
'5xx': ''
}, {
// fromJS: pm => ({
// ...pm,
// }),
});

View file

@ -1,14 +0,0 @@
import { Record } from 'immutable';
const DomBuildingTime = Record({
avg: undefined,
chart: [],
});
function fromJS(data = {}) {
if (data instanceof DomBuildingTime) return data;
return new DomBuildingTime(data);
}
export default fromJS;

View file

@ -1,30 +1,9 @@
export const getDayStartAndEndTimestamps = (date) => {
const start = moment(date).startOf('day').valueOf();
const end = moment(date).endOf('day').valueOf();
return { start, end };
};
// const getPerformanceDensity = (period) => {
// switch (period) {
// case HALF_AN_HOUR:
// return 30;
// case WEEK:
// return 84;
// case MONTH:
// return 90;
// case DAY:
// return 48;
// default:
// return 48;
// }
// };
const DAY = 1000 * 60 * 60 * 24;
const WEEK = DAY * 8;
const startWithZero = num => (num < 10 ? `0${ num }` : `${ num }`);
const startWithZero = (num: number) => (num < 10 ? `0${ num }` : `${ num }`);
const weekdays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];
// const months = [ "January", "February" ];
export const getTimeString = (ts, period) => {
const date = new Date(ts);
const diff = period.endTimestamp - period.startTimestamp;
@ -41,11 +20,11 @@ export const getTimeString = (ts, period) => {
export const getChartFormatter = period => (data = []) =>
data.map(({ timestamp, ...rest }) => ({ time: getTimeString(timestamp, period), ...rest, timestamp }));
export const getStartAndEndTimestampsByDensity = (current, start, end, density) => {
export const getStartAndEndTimestampsByDensity = (current: number, start: number, end: number, density: number) => {
const diff = end - start;
const step = Math.floor(diff / density);
const currentIndex = Math.floor((current - start) / step);
const startTimestamp = parseInt(start + currentIndex * step);
const endTimestamp = parseInt(startTimestamp + step);
const startTimestamp = start + currentIndex * step;
const endTimestamp = startTimestamp + step;
return { startTimestamp, endTimestamp };
};

View file

@ -1,4 +1,4 @@
import { Map, List } from 'immutable';
import { List } from 'immutable';
import Session from 'Types/session';
import { camelCased } from 'App/utils';
@ -29,7 +29,6 @@ import SlowestDomains from './slowestDomains';
import ResourceLoadingTime from './resourceLoadingTime';
import Image from './image';
import Resource from './resource';
import Err from './err';
import MissingResource from './missingResource';
@ -58,7 +57,7 @@ export const WIDGET_LIST = [{
name: "User Activity",
description: 'The average user feedback score, average number of visited pages per session and average session duration.',
thumb: 'user_activity.png',
dataWrapper: UserActivity,
dataWrapper: data => new UserActivity(data),
}, {
key: "pageMetrics",
name: "Page Metrics",
@ -85,13 +84,6 @@ export const WIDGET_LIST = [{
return i2.avgDuration - i1.avgDuration;
}),
},
// {
// key: "errorsTrend",
// name: "Most Impactful Errors",
// description: 'List of errors and exceptions, sorted by the number of impacted sessions.',
// thumb: 'most_Impactful_errors.png',
// dataWrapper: list => List(list).map(Err),
// },
{
key: "sessionsFrustration",
name: "Recent Frustrations",
@ -114,19 +106,13 @@ export const WIDGET_LIST = [{
type: 'resources',
dataWrapper: list => List(list).map(MissingResource),
},
// {
// key: "sessionsPerformance",
// name: "Recent Performance Issues",
// description: "",
// dataWrapper: list => List(list).map(Session),
// }
{
key: "slowestResources",
name: "Slowest Resources",
description: 'List of resources that are slowing down your website, sorted by the number of impacted sessions.',
thumb: 'na.png',
type: 'resources',
dataWrapper: list => List(list).map(SlowestResources)
dataWrapper: list => list.map(res => new SlowestResources(res))
},
{
key: "overview",
@ -138,8 +124,6 @@ export const WIDGET_LIST = [{
.map(item => OverviewWidget({ key: camelCased(item.key), ...item.data}))
.map(widget => widget.update("chart", getChartFormatter(period)))
}
// dataWrapper: (p, period) => List(p)
// .update("chart", getChartFormatter(period))
},
{
key: "speedLocation",

View file

@ -1,16 +0,0 @@
import Record from 'Types/Record';
const getName = (url = '') => url.split('/').filter(part => !!part).pop();
export default Record({
avgDuration: undefined,
sessions: undefined,
chart: [],
url: '',
name: '',
}, {
fromJS: (resource) => ({
...resource,
name: getName(resource.url),
})
});

View file

@ -0,0 +1,24 @@
const getName = (url = '') => url.split('/').filter(part => !!part).pop();
interface IResource {
avgDuration: number;
sessions: any[];
chart: any[];
url: string;
name: string;
}
export default class Resource {
avgDuration: IResource["avgDuration"];
sessions: IResource["sessions"];
chart: IResource["chart"] = [];
url: IResource["url"] = '';
name: IResource["name"] = '';
constructor(data: IResource) {
Object.assign(this, {
...data,
name: getName(data.url),
})
}
}

View file

@ -1,13 +0,0 @@
import { Record } from 'immutable';
const ResourceLoadingTime = Record({
avg: undefined,
timestamp: undefined
});
function fromJS(resourceLoadingTime = {}) {
if (resourceLoadingTime instanceof ResourceLoadingTime) return resourceLoadingTime;
return new ResourceLoadingTime(resourceLoadingTime);
}
export default fromJS;

View file

@ -0,0 +1,21 @@
interface IResourceLoadingTime {
avg: number;
timestamp: number;
}
class ResourceLoadingTime {
avg: IResourceLoadingTime["avg"];
timestamp: IResourceLoadingTime["timestamp"];
constructor(data: IResourceLoadingTime) {
Object.assign(this, data)
}
}
function fromJS(resourceLoadingTime = {}) {
if (resourceLoadingTime instanceof ResourceLoadingTime) return resourceLoadingTime;
return new ResourceLoadingTime(resourceLoadingTime);
}
export default fromJS;

View file

@ -1,14 +0,0 @@
import { Record } from 'immutable';
const ResponseTime = Record({
avg: undefined,
chart: [],
});
function fromJS(data = {}) {
if (data instanceof ResponseTime) return data;
return new ResponseTime(data);
}
export default fromJS;

View file

@ -0,0 +1,20 @@
interface IResponseTime {
avg: number;
chart: any[];
}
class ResponseTime {
avg: IResponseTime["avg"]
chart: IResponseTime["chart"] = []
constructor(data: IResponseTime) {
Object.assign(this, data)
}
}
function fromJS(data = {}) {
if (data instanceof ResponseTime) return data;
return new ResponseTime(data);
}
export default fromJS;

View file

@ -1,17 +0,0 @@
import { Record } from 'immutable';
const ResponseTimeDistribution = Record({
chart: [],
avg: undefined,
percentiles: [],
extremeValues: [],
total: undefined
});
function fromJS(data = {}) {
if (data instanceof ResponseTimeDistribution) return data;
return new ResponseTimeDistribution(data);
}
export default fromJS;

View file

@ -0,0 +1,27 @@
interface IResponseTimeDistribution {
chart: any[],
avg: number,
percentiles: number[],
extremeValues: number[],
total: number
}
class ResponseTimeDistribution {
chart: IResponseTimeDistribution["chart"] = []
avg: IResponseTimeDistribution["avg"]
percentiles: IResponseTimeDistribution["percentiles"] = []
extremeValues: IResponseTimeDistribution["extremeValues"] = []
total: IResponseTimeDistribution["total"]
constructor(data: IResponseTimeDistribution) {
Object.assign(this, data)
}
}
function fromJS(data = {}) {
if (data instanceof ResponseTimeDistribution) return data;
return new ResponseTimeDistribution(data);
}
export default fromJS;

View file

@ -1,14 +0,0 @@
import { Record } from 'immutable';
const SessionsImpactedByJSErrors = Record({
errorsCount: undefined,
chart: []
});
function fromJS(data = {}) {
if (data instanceof SessionsImpactedByJSErrors) return data;
return new SessionsImpactedByJSErrors(data);
}
export default fromJS;

View file

@ -0,0 +1,20 @@
interface ISessionsImpactedByJSErrors {
errorsCount: number;
chart: any[];
}
class SessionsImpactedByJSErrors {
errorsCount: ISessionsImpactedByJSErrors["errorsCount"];
chart: ISessionsImpactedByJSErrors["chart"];
constructor(data: ISessionsImpactedByJSErrors) {
Object.assign(this, data)
}
}
function fromJS(data = {}) {
if (data instanceof SessionsImpactedByJSErrors) return data;
return new SessionsImpactedByJSErrors(data);
}
export default fromJS;

View file

@ -1,14 +0,0 @@
import { Record } from 'immutable';
const SessionsImpactedBySlowRequests = Record({
avg: undefined,
chart: [],
});
function fromJS(data = {}) {
if (data instanceof SessionsImpactedBySlowRequests) return data;
return new SessionsImpactedBySlowRequests(data);
}
export default fromJS;

View file

@ -0,0 +1,20 @@
interface ISessionsSlowRequests {
avg: number;
chart: any[];
}
class SessionsImpactedBySlowRequests {
avg: ISessionsSlowRequests["avg"];
chart: ISessionsSlowRequests["chart"] = [];
constructor(data: ISessionsSlowRequests) {
Object.assign(this, data)
}
}
function fromJS(data = {}) {
if (data instanceof SessionsImpactedBySlowRequests) return data;
return new SessionsImpactedBySlowRequests(data);
}
export default fromJS;

View file

@ -1,13 +0,0 @@
import { Record } from 'immutable';
const SessionsPerBrowser = Record({
count: undefined,
chart: [],
});
function fromJS(sessionsPerBrowser = {}) {
if (sessionsPerBrowser instanceof SessionsPerBrowser) return sessionsPerBrowser;
return new SessionsPerBrowser({...sessionsPerBrowser, avg: Math.round(sessionsPerBrowser.avg)});
}
export default fromJS;

View file

@ -0,0 +1,22 @@
interface ISessionsPerBrowser {
count?: number,
chart?: any[],
avg: number,
}
class SessionsPerBrowser {
count: ISessionsPerBrowser["count"]
chart: ISessionsPerBrowser["chart"] = []
avg: ISessionsPerBrowser["avg"]
constructor(data: ISessionsPerBrowser) {
Object.assign(this, data)
}
}
function fromJS(sessionsPerBrowser = {}) {
if (sessionsPerBrowser instanceof SessionsPerBrowser) return sessionsPerBrowser;
return new SessionsPerBrowser({...sessionsPerBrowser, avg: Math.round(sessionsPerBrowser.avg)});
}
export default fromJS;

View file

@ -1,9 +1,18 @@
import { Record } from 'immutable';
const SlowestDomains = Record({
partition: [],
avg: undefined,
});
interface ISlowestDomains {
partition?: string[];
avg: number;
}
class SlowestDomains {
partition: ISlowestDomains["partition"] = [];
avg: ISlowestDomains["avg"];
constructor(data: ISlowestDomains) {
Object.assign(this, data)
}
}
function fromJS(slowestDomains = {}) {
if (slowestDomains instanceof SlowestDomains) return slowestDomains;

View file

@ -1,21 +0,0 @@
import Record from 'Types/Record';
import { fileType, fileName } from 'App/utils';
const validTypes = ['jpg', 'jpeg', 'js', 'css', 'woff', 'css', 'png', 'gif', 'svg']
export default Record({
avg: 0,
url: '',
type: '',
name: '',
chart: [],
}, {
fromJS: pm => {
const type = fileType(pm.url).toLowerCase();
return {
...pm,
// type: validTypes.includes(type) ? type : 'n/a',
// fileName: fileName(pm.url)
}
},
});

View file

@ -0,0 +1,19 @@
interface ISlowestResources {
avg: number;
url: string;
type: string;
name: string;
chart: any[]
}
export default class SlowestResources {
avg: ISlowestResources["avg"];
url: ISlowestResources["url"];
type: ISlowestResources["type"];
name: ISlowestResources["name"];
chart: ISlowestResources["chart"];
constructor(data: ISlowestResources) {
Object.assign(this, data);
}
}

View file

@ -1,14 +0,0 @@
import { Record } from 'immutable';
const SpeedLocation = Record({
avg: undefined,
chart: [],
});
function fromJS(data = {}) {
if (data instanceof SpeedLocation) return data;
return new SpeedLocation(data);
}
export default fromJS;

View file

@ -0,0 +1,20 @@
interface ISpeedLocation {
avg?: number
chart?: any[]
}
class SpeedLocation {
avg?: ISpeedLocation["avg"]
chart?: ISpeedLocation["chart"]
constructor(data: ISpeedLocation) {
Object.assign(this, data)
}
}
function fromJS(data = {}) {
if (data instanceof SpeedLocation) return data;
return new SpeedLocation(data);
}
export default fromJS;

View file

@ -1,13 +0,0 @@
import { Record } from 'immutable';
const TimeToRender = Record({
avg: undefined,
chart: [],
});
function fromJS(data = {}) {
if (data instanceof TimeToRender) return data;
return new TimeToRender(data);
}
export default fromJS;

View file

@ -0,0 +1,20 @@
interface ITimeToRender {
avg?: number
chart?: any[]
}
class TimeToRender {
avg: ITimeToRender["avg"]
chart: ITimeToRender["chart"] = []
constructor(data: ITimeToRender) {
Object.assign(this, data)
}
}
function fromJS(data = {}) {
if (data instanceof TimeToRender) return data;
return new TimeToRender(data);
}
export default fromJS;

View file

@ -1,8 +1,16 @@
import { Record } from 'immutable';
const TopDomains = Record({
chart: []
});
interface ITopDomains {
chart?: any[]
}
class TopDomains {
chart: ITopDomains["chart"] = []
constructor(data: ITopDomains) {
this.chart = data.chart
}
}
function fromJS(data = {}) {
if (data instanceof TopDomains) return data;

View file

@ -1,19 +0,0 @@
import Record from 'Types/Record';
export default Record({
avgResponseTime: 0,
requestsCount: 0,
avgTimeTilFirstBite: 0,
avgDomCompleteTime: 0
}, {
// fromJS: aa => ({
// avgPageLoad: aa.avgDom,
// avgPageLoadProgress: aa.avgDomProgress,
// avgImgLoad: aa.avgLoad,
// avgImgLoadProgress: aa.avgLoadProgress,
// avgReqLoad: aa.avgFirstPixel,
// avgReqLoadProgress: aa.avgFirstPixelProgress,
// ...aa,
// }),
});

View file

@ -0,0 +1,17 @@
interface ITopMetrics {
avgResponseTime: number
requestsCount: number
avgTimeTilFirstBite: number
avgDomCompleteTime: number
}
export default class TopMetrics {
avgResponseTime: ITopMetrics["avgResponseTime"] = 0
requestsCount: ITopMetrics["requestsCount"] = 0
avgTimeTilFirstBite: ITopMetrics["avgTimeTilFirstBite"] = 0
avgDomCompleteTime: ITopMetrics["avgDomCompleteTime"] = 0
constructor(data: ITopMetrics) {
Object.assign(this, data)
}
}

View file

@ -1,10 +0,0 @@
import Record from 'Types/Record';
export default Record({
avgVisitedPages: undefined,
avgVisitedPagesProgress: undefined,
avgDuration: undefined,
avgDurationProgress: undefined,
avgEmotionalRating: undefined,
avgEmotionalRatingProgress: undefined,
});

View file

@ -0,0 +1,17 @@
interface IUserActivity {
avgVisitedPages: number;
avgVisitedPagesProgress: number;
avgDuration: number;
avgDurationProgress: number;
}
export default class UserActivity {
avgVisitedPages: IUserActivity["avgVisitedPages"]
avgVisitedPagesProgress: IUserActivity["avgDurationProgress"]
avgDuration: IUserActivity["avgDuration"]
avgDurationProgress: IUserActivity["avgDurationProgress"]
constructor(activity: IUserActivity) {
Object.assign(this, activity)
}
}

View file

@ -60,7 +60,7 @@ export interface ISession {
eventsCount: number,
pagesCount: number,
errorsCount: number,
issueTypes: [],
issueTypes: string[],
issues: [],
referrer: string | null,
userDeviceHeapSize: number,