diff --git a/frontend/.prettierrc b/frontend/.prettierrc
index 4c38cc4c4..322be28b9 100644
--- a/frontend/.prettierrc
+++ b/frontend/.prettierrc
@@ -2,5 +2,8 @@
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
- "singleQuote": true
+ "singleQuote": true,
+ "importOrderSeparation": true,
+ "importOrderSortSpecifiers": true,
+ "importOrder": ["^Components|^App|^UI|^Duck", "^Shared", "^[./]"]
}
diff --git a/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx b/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx
index a2ad224bc..628dc5f48 100644
--- a/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx
+++ b/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx
@@ -1,36 +1,107 @@
-import React from 'react';
-import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
+import React from 'react';
+import { connect } from 'react-redux';
+
+import { useStore } from 'App/mstore';
+import { debounce } from 'App/utils';
+import { IResourceRequest, IResourceTiming } from "../../../../../player";
+import { WsChannel } from "../../../../../player/web/messages";
+import { PlayerContext } from "../../../playerContext";
+
+let debounceUpdate: any = () => {};
const userBehaviorRegex = /User\s+(\w+\s+)?Behavior/i;
const issuesErrorsRegex = /Issues\s+(and\s+|,?\s+)?(\w+\s+)?Errors/i;
-function testLine(line: string): boolean {
+function isTitleLine(line: string): boolean {
return userBehaviorRegex.test(line) || issuesErrorsRegex.test(line);
}
-function SummaryBlock({ sessionId }: { sessionId: string }) {
+function SummaryBlock({
+ sessionId,
+ zoomEnabled,
+ zoomStartTs,
+ zoomEndTs,
+ zoomTab,
+ duration,
+}: {
+ sessionId: string;
+ zoomEnabled: boolean;
+ zoomStartTs: number;
+ zoomEndTs: number;
+ zoomTab: 'overview' | 'journey' | 'issues' | 'errors';
+ duration: any;
+}) {
+ const { store } = React.useContext(PlayerContext)
+ const { tabStates } = store.get();
const { aiSummaryStore } = useStore();
React.useEffect(() => {
- void aiSummaryStore.getSummary(sessionId);
+ debounceUpdate = debounce(
+ (
+ sessionId: string,
+ events: any[],
+ feat: 'journey' | 'issues' | 'errors',
+ startTs: number,
+ endTs: number
+ ) => aiSummaryStore.getDetailedSummary(sessionId, events, feat, startTs, endTs),
+ 500
+ );
}, []);
+ React.useEffect(() => {
+ if (zoomTab === 'overview') {
+ void aiSummaryStore.getSummary(sessionId);
+ } else {
+ const totalFetchList: IResourceRequest[] = [];
+ const totalResourceList: IResourceTiming[] = [];
+ const totalWebsocketList: WsChannel[] = [];
+ Object.values(tabStates).forEach(({
+ fetchList,
+ resourceList,
+ websocketList,
+ }) => {
+ totalFetchList.push(...fetchList);
+ totalResourceList.push(...resourceList);
+ totalWebsocketList.push(...websocketList);
+ })
+ const resultingEvents = [
+ ...totalFetchList,
+ ...totalResourceList,
+ ...totalWebsocketList,
+ ]
+ const range = !zoomEnabled ? [0, duration] : [zoomStartTs, zoomEndTs];
+ void debounceUpdate(sessionId, resultingEvents, zoomTab, range[0], range[1]);
+ }
+ }, [zoomTab]);
+
const formattedText = aiSummaryStore.text.split('\n').map((line) => {
- if (testLine(line)) {
+ if (isTitleLine(line)) {
return
{line}
;
}
if (line.startsWith('*')) {
- return {line.replace('* ', '')};
+ return (
+
+
+
+ );
}
- return {line}
;
+ return (
+
+
+
+ );
});
return (
-
- User Behavior Analysis
-
+ {/*
*/}
+ {/* User Behavior Analysis*/}
+ {/*
*/}
{aiSummaryStore.text ? (
@@ -66,6 +137,20 @@ function TextPlaceholder() {
);
}
+const CodeStringFormatter = ({ text }: { text: string }) => {
+ const parts = text.split(/(`[^`]*`)/).map((part, index) =>
+ part.startsWith('`') && part.endsWith('`') ? (
+
+ {part.substring(1, part.length - 1)}
+
+ ) : (
+
{part}
+ )
+ );
+
+ return <>{parts}>;
+};
+
const summaryBlockStyle: React.CSSProperties = {
background: 'linear-gradient(180deg, #E8EBFF -24.14%, rgba(236, 254, 255, 0.00) 100%)',
width: '100%',
@@ -77,4 +162,10 @@ const summaryBlockStyle: React.CSSProperties = {
padding: '1rem',
};
-export default observer(SummaryBlock);
+export default connect((state: Record
) => ({
+ zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
+ zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
+ zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
+ zoomTab: state.getIn(['components', 'player']).zoomTab,
+ duration: state.getIn(['sessions', 'current']).durationSeconds,
+}))(observer(SummaryBlock));
diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
index 550d9ecd0..7564914d0 100644
--- a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
+++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
@@ -26,6 +26,9 @@ interface IProps {
filterOutNote: (id: string) => void;
eventsIndex: number[];
uxtVideo: string;
+ zoomEnabled: boolean;
+ zoomStartTs: number;
+ zoomEndTs: number;
}
function EventsBlock(props: IProps) {
@@ -82,8 +85,21 @@ function EventsBlock(props: IProps) {
return mergeEventLists(
filteredLength > 0 ? filteredEvents : eventsWithMobxNotes,
tabChangeEvents
+ ).filter((e) =>
+ props.zoomEnabled
+ ? 'time' in e
+ ? e.time >= props.zoomStartTs && e.time <= props.zoomEndTs
+ : false
+ : true
);
- }, [filteredLength, notesWithEvtsLength, notesLength]);
+ }, [
+ filteredLength,
+ notesWithEvtsLength,
+ notesLength,
+ props.zoomEnabled,
+ props.zoomStartTs,
+ props.zoomEndTs,
+ ]);
const write = ({ target: { value } }: React.ChangeEvent) => {
props.setEventFilter({ query: value });
@@ -180,8 +196,20 @@ function EventsBlock(props: IProps) {
{uxtestingStore.isUxt() ? (
-
-
No video
+
+
+ No video
+
) : null}
@@ -233,6 +261,9 @@ export default connect(
filteredEvents: state.getIn(['sessions', 'filteredEvents']),
query: state.getIn(['sessions', 'eventsQuery']),
eventsIndex: state.getIn(['sessions', 'eventsIndex']),
+ zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
+ zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
+ zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
}),
{
setEventFilter,
diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
index 394425af1..e9f7bd2ea 100644
--- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
+++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
@@ -1,27 +1,40 @@
-import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
-import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
-import React, { useEffect } from 'react';
-import { toggleBottomBlock } from 'Duck/components/player';
-import BottomBlock from '../BottomBlock';
-import EventRow from './components/EventRow';
-import { connect } from 'react-redux';
-import TimelineScale from './components/TimelineScale';
-import FeatureSelection, { HELP_MESSAGE } from './components/FeatureSelection/FeatureSelection';
-import TimelinePointer from './components/TimelinePointer';
-import VerticalPointerLine from './components/VerticalPointerLine';
import cn from 'classnames';
-import OverviewPanelContainer from './components/OverviewPanelContainer';
-import { NoContent, Icon } from 'UI';
import { observer } from 'mobx-react-lite';
+import React, { useEffect } from 'react';
+import { connect } from 'react-redux';
+import { Segmented } from 'antd'
+
import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext';
import { useStore } from 'App/mstore';
+import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
+import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
+import { toggleBottomBlock, setZoomTab } from 'Duck/components/player';
+import { Icon, NoContent } from 'UI';
+
+import BottomBlock from '../BottomBlock';
+import EventRow from './components/EventRow';
+import FeatureSelection, { HELP_MESSAGE } from './components/FeatureSelection/FeatureSelection';
+import OverviewPanelContainer from './components/OverviewPanelContainer';
+import TimelinePointer from './components/TimelinePointer';
+import TimelineScale from './components/TimelineScale';
+import VerticalPointerLine from './components/VerticalPointerLine';
function MobileOverviewPanelCont({
issuesList,
sessionId,
+ zoomEnabled,
+ zoomStartTs,
+ zoomEndTs,
+ setZoomTab,
+ zoomTab
}: {
issuesList: Record
[];
sessionId: string;
+ zoomEnabled: boolean;
+ zoomStartTs: number;
+ zoomEndTs: number;
+ setZoomTab: (tab: string) => void;
+ zoomTab: 'overview' | 'journey' | 'issues' | 'errors'
}) {
const { aiSummaryStore } = useStore();
const { store, player } = React.useContext(MobilePlayerContext);
@@ -45,12 +58,18 @@ function MobileOverviewPanelCont({
const fetchPresented = fetchList.length > 0;
+ const checkInZoomRange = (list: any[]) => {
+ return list.filter((i) => (zoomEnabled ? i.time >= zoomStartTs && i.time <= zoomEndTs : true));
+ };
+
const resources = {
- NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow),
- ERRORS: exceptionsList,
- EVENTS: eventsList,
- PERFORMANCE: performanceChartData,
- FRUSTRATIONS: frustrationsList,
+ NETWORK: checkInZoomRange(
+ fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow)
+ ),
+ ERRORS: checkInZoomRange(exceptionsList),
+ EVENTS: checkInZoomRange(eventsList),
+ PERFORMANCE: checkInZoomRange(performanceChartData),
+ FRUSTRATIONS: checkInZoomRange(frustrationsList),
};
useEffect(() => {
@@ -88,11 +107,27 @@ function MobileOverviewPanelCont({
showSummary={isSaas}
toggleSummary={() => aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)}
summaryChecked={aiSummaryStore.toggleSummary}
+ setZoomTab={setZoomTab}
+ zoomTab={zoomTab}
/>
);
}
-function WebOverviewPanelCont({ sessionId }: { sessionId: string }) {
+function WebOverviewPanelCont({
+ sessionId,
+ zoomEnabled,
+ zoomStartTs,
+ zoomEndTs,
+ setZoomTab,
+ zoomTab,
+}: {
+ sessionId: string;
+ zoomEnabled: boolean;
+ zoomStartTs: number;
+ zoomEndTs: number;
+ setZoomTab: (tab: string) => void;
+ zoomTab: 'overview' | 'journey' | 'issues' | 'errors'
+}) {
const { aiSummaryStore } = useStore();
const { store } = React.useContext(PlayerContext);
const [selectedFeatures, setSelectedFeatures] = React.useState([
@@ -121,15 +156,19 @@ function WebOverviewPanelCont({ sessionId }: { sessionId: string }) {
.concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400))
.filter((i: any) => i.type === 'fetch');
+ const checkInZoomRange = (list: any[]) => {
+ return list.filter((i) => (zoomEnabled ? i.time >= zoomStartTs && i.time <= zoomEndTs : true));
+ };
+
const resources: any = React.useMemo(() => {
return {
- NETWORK: resourceList,
- ERRORS: exceptionsList,
- EVENTS: stackEventList,
- PERFORMANCE: performanceChartData,
- FRUSTRATIONS: frustrationsList,
+ NETWORK: checkInZoomRange(resourceList),
+ ERRORS: checkInZoomRange(exceptionsList),
+ EVENTS: checkInZoomRange(stackEventList),
+ PERFORMANCE: checkInZoomRange(performanceChartData),
+ FRUSTRATIONS: checkInZoomRange(frustrationsList),
};
- }, [tabStates, currentTab]);
+ }, [tabStates, currentTab, zoomEnabled, zoomStartTs, zoomEndTs]);
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
@@ -144,6 +183,8 @@ function WebOverviewPanelCont({ sessionId }: { sessionId: string }) {
toggleSummary={() => aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)}
summaryChecked={aiSummaryStore.toggleSummary}
sessionId={sessionId}
+ setZoomTab={setZoomTab}
+ zoomTab={zoomTab}
/>
);
}
@@ -160,6 +201,8 @@ function PanelComponent({
toggleSummary,
summaryChecked,
sessionId,
+ zoomTab,
+ setZoomTab,
}: any) {
return (
@@ -168,7 +211,31 @@ function PanelComponent({
X-Ray
{showSummary ? (
-
+ <>
+
+ setZoomTab(val)}
+ options={[
+ {
+ label: 'Overview',
+ value: 'overview',
+ },
+ {
+ label: 'User Journey',
+ value: 'journey',
+ },
+ {
+ label: 'Issues',
+ value: 'issues',
+ },
+ {
+ label: 'Suggestions',
+ value: 'errors',
+ }
+ ]}
+ />
+ >
) : null}
@@ -247,9 +314,12 @@ export const OverviewPanel = connect(
(state: Record
) => ({
issuesList: state.getIn(['sessions', 'current']).issues,
sessionId: state.getIn(['sessions', 'current']).sessionId,
+ zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
+ zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
+ zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
}),
{
- toggleBottomBlock,
+ toggleBottomBlock, setZoomTab
}
)(observer(WebOverviewPanelCont));
@@ -257,8 +327,12 @@ export const MobileOverviewPanel = connect(
(state: Record) => ({
issuesList: state.getIn(['sessions', 'current']).issues,
sessionId: state.getIn(['sessions', 'current']).sessionId,
+ zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
+ zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
+ zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
+ zoomTab: state.getIn(['components', 'player']).zoomTab
}),
{
- toggleBottomBlock,
+ toggleBottomBlock, setZoomTab,
}
)(observer(MobileOverviewPanelCont));
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx
index 659659671..ab5b0d172 100644
--- a/frontend/app/components/Session_/Player/Controls/Controls.tsx
+++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx
@@ -75,8 +75,15 @@ function Controls(props: any) {
const { player, store } = React.useContext(PlayerContext);
const { uxtestingStore } = useStore();
- const { playing, completed, skip, speed, messagesLoading, markedTargets, inspectorMode } =
- store.get();
+ const {
+ playing,
+ completed,
+ skip,
+ speed,
+ messagesLoading,
+ markedTargets,
+ inspectorMode,
+ } = store.get();
const {
bottomBlock,
diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx
index 41374daca..5992e19f5 100644
--- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx
+++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx
@@ -1,9 +1,8 @@
+import DraggableMarkers from 'Components/Session_/Player/Controls/components/ZoomDragLayer';
import React, { useEffect, useMemo, useContext, useState, useRef } from 'react';
import { connect } from 'react-redux';
-import TimeTracker from './TimeTracker';
import stl from './timeline.module.css';
import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions';
-import DraggableCircle from './components/DraggableCircle';
import CustomDragLayer, { OnDragCallback } from './components/CustomDragLayer';
import { debounce } from 'App/utils';
import TooltipContainer from './components/TooltipContainer';
@@ -11,37 +10,33 @@ import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { DateTime, Duration } from 'luxon';
-import Issue from "Types/session/issue";
+import Issue from 'Types/session/issue';
import { WebEventsList, MobEventsList } from './EventsList';
import NotesList from './NotesList';
-import SkipIntervalsList from './SkipIntervalsList'
-import TimelineTracker from "Components/Session_/Player/Controls/TimelineTracker";
+import SkipIntervalsList from './SkipIntervalsList';
+import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
interface IProps {
- issues: Issue[]
- setTimelineHoverTime: (t: number) => void
- startedAt: number
- tooltipVisible: boolean
- timezone?: string
- isMobile?: boolean
+ issues: Issue[];
+ setTimelineHoverTime: (t: number) => void;
+ startedAt: number;
+ tooltipVisible: boolean;
+ timezone?: string;
+ isMobile?: boolean;
+ timelineZoomEnabled: boolean;
+ timelineZoomStartTs: number;
+ timelineZoomEndTs: number;
}
function Timeline(props: IProps) {
- const { player, store } = useContext(PlayerContext)
- const [wasPlaying, setWasPlaying] = useState(false)
+ const { player, store } = useContext(PlayerContext);
+ const [wasPlaying, setWasPlaying] = useState(false);
const { settingsStore } = useStore();
- const {
- playing,
- skipToIssue,
- ready,
- endTime,
- devtoolsLoading,
- domLoading,
- } = store.get()
- const { issues, timezone } = props;
+ const { playing, skipToIssue, ready, endTime, devtoolsLoading, domLoading } = store.get();
+ const { issues, timezone, timelineZoomEnabled } = props;
- const progressRef = useRef(null)
- const timelineRef = useRef(null)
+ const progressRef = useRef(null);
+ const timelineRef = useRef(null);
const scale = 100 / endTime;
@@ -51,10 +46,10 @@ function Timeline(props: IProps) {
if (firstIssue && skipToIssue) {
player.jump(firstIssue.time);
}
- }, [])
+ }, []);
- const debouncedJump = useMemo(() => debounce(player.jump, 500), [])
- const debouncedTooltipChange = useMemo(() => debounce(props.setTimelineHoverTime, 50), [])
+ const debouncedJump = useMemo(() => debounce(player.jump, 500), []);
+ const debouncedTooltipChange = useMemo(() => debounce(props.setTimelineHoverTime, 50), []);
const onDragEnd = () => {
if (wasPlaying) {
@@ -64,31 +59,37 @@ function Timeline(props: IProps) {
const onDrag: OnDragCallback = (offset) => {
// @ts-ignore react mismatch
- const p = (offset.x) / progressRef.current.offsetWidth;
+ const p = offset.x / progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
debouncedJump(time);
hideTimeTooltip();
if (playing) {
- setWasPlaying(true)
+ setWasPlaying(true);
player.pause();
}
};
const showTimeTooltip = (e: React.MouseEvent) => {
if (
- e.target !== progressRef.current
- && e.target !== timelineRef.current
+ e.target !== progressRef.current &&
+ e.target !== timelineRef.current &&
// @ts-ignore black magic
- && !progressRef.current.contains(e.target)
+ !progressRef.current.contains(e.target)
) {
return props.tooltipVisible && hideTimeTooltip();
}
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 userTimeStr = timezone ? DateTime.fromMillis(props.startedAt + time).setZone(timezone).toFormat(`hh:mm:ss a`) : undefined
+ const tz = settingsStore.sessionSettings.timezone.value;
+ const timeStr = DateTime.fromMillis(props.startedAt + time)
+ .setZone(tz)
+ .toFormat(`hh:mm:ss a`);
+ const userTimeStr = timezone
+ ? DateTime.fromMillis(props.startedAt + time)
+ .setZone(timezone)
+ .toFormat(`hh:mm:ss a`)
+ : undefined;
const timeLineTooltip = {
time: Duration.fromMillis(time).toFormat(`mm:ss`),
@@ -99,7 +100,7 @@ function Timeline(props: IProps) {
};
debouncedTooltipChange(timeLineTooltip);
- }
+ };
const hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false };
@@ -134,6 +135,7 @@ function Timeline(props: IProps) {
left: '0.5rem',
}}
>
+ {timelineZoomEnabled ? : null}
-
-
+
+
-