openreplay/frontend/app/duck/sessions.js

439 lines
15 KiB
JavaScript

import { List, Map } from 'immutable';
import Session from 'Types/session';
import ErrorStack from 'Types/session/errorStack';
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';
const name = 'sessions';
const INIT = 'sessions/INIT';
const FETCH_LIST = new RequestTypes('sessions/FETCH_LIST');
const FETCH = new RequestTypes('sessions/FETCH');
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');
const FETCH_ERROR_STACK = new RequestTypes('sessions/FETCH_ERROR_STACK');
const FETCH_INSIGHTS = new RequestTypes('sessions/FETCH_INSIGHTS');
const SORT = 'sessions/SORT';
const REDEFINE_TARGET = 'sessions/REDEFINE_TARGET';
const SET_TIMEZONE = 'sessions/SET_TIMEZONE';
const SET_EVENT_QUERY = 'sessions/SET_EVENT_QUERY';
const SET_AUTOPLAY_VALUES = 'sessions/SET_AUTOPLAY_VALUES';
const TOGGLE_CHAT_WINDOW = 'sessions/TOGGLE_CHAT_WINDOW';
const SET_FUNNEL_PAGE_FLAG = 'sessions/SET_FUNNEL_PAGE_FLAG';
const SET_TIMELINE_POINTER = 'sessions/SET_TIMELINE_POINTER';
const SET_TIMELINE_HOVER_POINTER = 'sessions/SET_TIMELINE_HOVER_POINTER';
const SET_CREATE_NOTE_TOOLTIP = 'sessions/SET_CREATE_NOTE_TOOLTIP'
const SET_EDIT_NOTE_TOOLTIP = 'sessions/SET_CREATE_NOTE_TOOLTIP'
const FILTER_OUT_NOTE = 'sessions/FILTER_OUT_NOTE'
const ADD_NOTE = 'sessions/ADD_NOTE'
const UPDATE_NOTE = 'sessions/UPDATE_NOTE'
const SET_SESSION_PATH = 'sessions/SET_SESSION_PATH';
const LAST_PLAYED_SESSION_ID = `${name}/LAST_PLAYED_SESSION_ID`;
const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
const range = getDateRangeFromValue(LAST_7_DAYS);
const defaultDateFilters = {
url: '',
rangeValue: LAST_7_DAYS,
startDate: range.start.unix() * 1000,
endDate: range.end.unix() * 1000,
};
const initialState = Map({
list: List(),
sessionIds: [],
current: Session(),
total: 0,
keyMap: Map(),
wdTypeCount: Map(),
favoriteList: List(),
activeTab: Watchdog({ name: 'All', type: 'all' }),
timezone: 'local',
errorStack: List(),
eventsIndex: [],
sourcemapUploaded: true,
filteredEvents: null,
showChatWindow: false,
liveSessions: List(),
visitedEvents: List(),
insights: List(),
insightFilters: defaultDateFilters,
host: '',
funnelPage: Map(),
timelinePointer: null,
sessionPath: {},
lastPlayedSessionId: null,
timeLineTooltip: { time: 0, offset: 0, isVisible: false },
createNoteTooltip: { time: 0, isVisible: false, isEdit: false, note: null },
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case INIT:
return state.set('current', Session(action.session));
// case FETCH_LIST.REQUEST:
// return action.clear ? state.set('list', List()) : state;
case FETCH_ERROR_STACK.SUCCESS:
return state.set('errorStack', List(action.data.trace).map(ErrorStack)).set('sourcemapUploaded', action.data.sourcemapUploaded);
case FETCH_LIVE_LIST.SUCCESS:
const liveList = List(action.data.sessions).map((s) => new Session({ ...s, live: true }));
return state.set('liveSessions', liveList);
case FETCH_LIST.SUCCESS:
const { sessions, total } = action.data;
const list = List(sessions).map(Session);
return state
.set('list', list)
.set('sessionIds', list.map(({ sessionId }) => sessionId).toJS())
.set(
'favoriteList',
list.filter(({ favorite }) => favorite)
)
.set('total', total);
case SET_AUTOPLAY_VALUES: {
const sessionIds = state.get('sessionIds');
const currentSessionId = state.get('current').sessionId;
const currentIndex = sessionIds.indexOf(currentSessionId);
return state.set('previousId', sessionIds[currentIndex - 1]).set('nextId', sessionIds[currentIndex + 1]);
}
case SET_EVENT_QUERY: {
const events = state.get('current').events;
const query = action.filter.query;
// const filter = action.filter.filter;
const searchRe = getRE(query, 'i');
let filteredEvents = query ? events.filter((e) => searchRe.test(e.url) || searchRe.test(e.value) || searchRe.test(e.label)) : null;
// if (filter) {
// filteredEvents = filteredEvents ? filteredEvents.filter(e => e.type === filter) : events.filter(e => e.type === filter);
// }
return state.set('filteredEvents', filteredEvents);
}
case FETCH.SUCCESS: {
// TODO: more common.. or TEMP
const events = action.filter.events;
// const filters = action.filter.filters;
const current = state.get('list').find(({ sessionId }) => sessionId === action.data.sessionId) || Session();
const session = Session(action.data);
const matching = [];
const visitedEvents = [];
const tmpMap = {};
session.events.forEach((event) => {
if (event.type === 'LOCATION' && !tmpMap.hasOwnProperty(event.url)) {
tmpMap[event.url] = event.url;
visitedEvents.push(event);
}
});
events.forEach(({ key, operator, value }) => {
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);
}
}
});
});
return state
.set('current', current.merge(session))
.set('eventsIndex', matching)
.set('visitedEvents', visitedEvents)
.set('host', visitedEvents[0] && visitedEvents[0].host);
}
case FETCH_FAVORITE_LIST.SUCCESS:
return state.set('favoriteList', List(action.data).map(Session));
case TOGGLE_FAVORITE.SUCCESS: {
const id = action.sessionId;
const session = state.get('list').find(({ sessionId }) => sessionId === id);
const wasInFavorite = state.get('favoriteList').findIndex(({ sessionId }) => sessionId === id) > -1;
return state
.update('list', (list) => list.map((session) => (session.sessionId === id ? session.set('favorite', !wasInFavorite) : session)))
.update('favoriteList', (list) =>
wasInFavorite ? list.filter(({ sessionId }) => sessionId !== id) : list.push(session.set('favorite', true))
)
.update('current', (session) => (session.sessionId === id ? session.set('favorite', !wasInFavorite) : session));
}
case SORT: {
const comparator = (s1, s2) => {
let diff = s1[action.sortKey] - s2[action.sortKey];
diff = diff === 0 ? s1.startedAt - s2.startedAt : diff;
return action.sign * diff;
};
return state.update('list', (list) => list.sort(comparator)).update('favoriteList', (list) => 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());
case SET_TIMEZONE:
return state.set('timezone', action.timezone);
case TOGGLE_CHAT_WINDOW:
return state.set('showChatWindow', action.state);
case FETCH_INSIGHTS.SUCCESS:
return state.set(
'insights',
List(action.data).sort((a, b) => b.count - a.count)
);
case SET_FUNNEL_PAGE_FLAG:
return state.set('funnelPage', action.funnelPage ? Map(action.funnelPage) : false);
case SET_TIMELINE_POINTER:
return state.set('timelinePointer', action.pointer);
case SET_TIMELINE_HOVER_POINTER:
return state.set('timeLineTooltip', action.timeLineTooltip);
case SET_CREATE_NOTE_TOOLTIP:
return state.set('createNoteTooltip', action.noteTooltip);
case SET_EDIT_NOTE_TOOLTIP:
return state.set('createNoteTooltip', action.noteTooltip);
case FILTER_OUT_NOTE:
return state.updateIn(['current', 'notesWithEvents'], (list) =>
list.filter(evt => !evt.noteId || evt.noteId !== action.noteId)
)
case ADD_NOTE:
return state.updateIn(['current', 'notesWithEvents'], (list) =>
list.push(action.note).sort((a, b) => {
const aTs = a.time || a.timestamp
const bTs = b.time || b.timestamp
return aTs - bTs
})
)
case UPDATE_NOTE:
const index = state.getIn(['current', 'notesWithEvents']).findIndex(item => item.noteId === action.note.noteId)
return state.setIn(['current', 'notesWithEvents', index], action.note)
case SET_SESSION_PATH:
return state.set('sessionPath', action.path);
case LAST_PLAYED_SESSION_ID:
return updateListItem(state, action.sessionId, { viewed: true }).set('lastPlayedSessionId', action.sessionId);
default:
return state;
}
};
function updateListItem(state, sourceSessionId, instance) {
const list = state.get('list');
const index = list.findIndex(({ sessionId }) => sessionId === sourceSessionId);
if (index === -1) return state;
return state.updateIn(['list', index], (session) => session.merge(instance));
}
export default withRequestState(
{
_: [FETCH, FETCH_LIST],
fetchLiveListRequest: FETCH_LIVE_LIST,
fetchFavoriteListRequest: FETCH_FAVORITE_LIST,
toggleFavoriteRequest: TOGGLE_FAVORITE,
fetchErrorStackList: FETCH_ERROR_STACK,
fetchInsightsRequest: FETCH_INSIGHTS,
},
reducer
);
function init(session) {
return {
type: INIT,
session,
};
}
export const fetchList =
(params = {}, force = false) =>
(dispatch, getState) => {
if (!force) { // compare with the last fetched filter
const oldFilters = getSessionFilter();
if (compareJsonObjects(oldFilters, cleanSessionFilters(params))) {
return;
}
}
setSessionFilter(cleanSessionFilters(params));
return dispatch({
types: FETCH_LIST.toArray(),
call: (client) => client.post('/sessions/search', params),
params: cleanParams(params),
});
};
export function fetchErrorStackList(sessionId, errorId) {
return {
types: FETCH_ERROR_STACK.toArray(),
call: (client) => client.get(`/sessions/${sessionId}/errors/${errorId}/sourcemaps`),
};
}
export const fetch =
(sessionId, isLive = false) =>
(dispatch, getState) => {
dispatch({
types: FETCH.toArray(),
call: (client) => client.get(isLive ? `/assist/sessions/${sessionId}` : `/sessions/${sessionId}`),
filter: getState().getIn(['filters', 'appliedFilter']),
});
};
export function toggleFavorite(sessionId) {
return {
types: TOGGLE_FAVORITE.toArray(),
call: (client) => client.get(`/sessions/${sessionId}/favorite`),
sessionId,
};
}
export function fetchFavoriteList() {
return {
types: FETCH_FAVORITE_LIST.toArray(),
call: (client) => client.get('/sessions/favorite'),
};
}
export function fetchInsights(params) {
return {
types: FETCH_INSIGHTS.toArray(),
call: (client) => client.post('/heatmaps/url', params),
};
}
export function fetchLiveList(params = {}) {
return {
types: FETCH_LIVE_LIST.toArray(),
call: (client) => client.get('/assist/sessions', params),
};
}
export function toggleChatWindow(state) {
return {
type: TOGGLE_CHAT_WINDOW,
state,
};
}
export function sort(sortKey, sign = 1, listName = 'list') {
return {
type: SORT,
sortKey,
sign,
listName,
};
}
export function redefineTarget(target) {
return {
type: REDEFINE_TARGET,
target,
};
}
export const setAutoplayValues = (sessionId) => {
return {
type: SET_AUTOPLAY_VALUES,
sessionId,
};
};
export const setActiveTab = (tab) => ({
type: SET_ACTIVE_TAB,
tab,
});
export function setTimezone(timezone) {
return {
type: SET_TIMEZONE,
timezone,
};
}
export function setEventFilter(filter) {
return {
type: SET_EVENT_QUERY,
filter,
};
}
export function setFunnelPage(funnelPage) {
return {
type: SET_FUNNEL_PAGE_FLAG,
funnelPage,
};
}
export function setTimelinePointer(pointer) {
return {
type: SET_TIMELINE_POINTER,
pointer,
};
}
export function setTimelineHoverTime(timeLineTooltip) {
return {
type: SET_TIMELINE_HOVER_POINTER,
timeLineTooltip
};
}
export function setCreateNoteTooltip(noteTooltip) {
return {
type: SET_CREATE_NOTE_TOOLTIP,
noteTooltip
}
}
export function setEditNoteTooltip(noteTooltip) {
return {
type: SET_EDIT_NOTE_TOOLTIP,
noteTooltip
}
}
export function filterOutNote(noteId) {
return {
type: FILTER_OUT_NOTE,
noteId
}
}
export function addNote(note) {
return {
type: ADD_NOTE,
note
}
}
export function updateNote(note) {
return {
type: UPDATE_NOTE,
note
}
}
export function setSessionPath(path) {
return {
type: SET_SESSION_PATH,
path,
};
}
export function updateLastPlayedSession(sessionId) {
return {
type: LAST_PLAYED_SESSION_ID,
sessionId,
};
}