From 80f0005362dafc90cef88522f8f9bbcd1bb2f1c3 Mon Sep 17 00:00:00 2001 From: Delirium Date: Fri, 22 Mar 2024 15:17:46 +0100 Subject: [PATCH] feat(ui): timeline zoom (#1982) * feat(ui): timeline zoom * stable draggable markers * integrate zoom into panels, ready up ai stuff for zoom * tabs for ai, slider styles * fixes for zoom tabs * code style --- frontend/.prettierrc | 5 +- .../ReplayPlayer/SummaryBlock/index.tsx | 115 +++++++++-- .../Session_/EventsBlock/EventsBlock.tsx | 37 +++- .../Session_/OverviewPanel/OverviewPanel.tsx | 130 ++++++++++--- .../Session_/Player/Controls/Controls.tsx | 11 +- .../Session_/Player/Controls/Timeline.tsx | 77 ++++---- .../Controls/components/PlayerControls.tsx | 6 +- .../components/TimelineZoomButton.tsx | 43 +++++ .../Controls/components/ZoomDragLayer.tsx | 161 ++++++++++++++++ .../DevTools/ConsolePanel/ConsolePanel.tsx | 167 +++++++++------- .../DevTools/NetworkPanel/NetworkPanel.tsx | 51 ++++- .../StackEventPanel/StackEventPanel.tsx | 118 +++++++++--- frontend/app/duck/components/player.ts | 162 ++++++++-------- frontend/app/mstore/aiSummaryStore.ts | 30 ++- frontend/app/services/AiService.ts | 28 ++- frontend/package.json | 2 + frontend/yarn.lock | 180 +++++++++++++++++- 17 files changed, 1035 insertions(+), 288 deletions(-) create mode 100644 frontend/app/components/Session_/Player/Controls/components/TimelineZoomButton.tsx create mode 100644 frontend/app/components/Session_/Player/Controls/components/ZoomDragLayer.tsx 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() ? (
    -
    ) : 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}
    -
    -
    +
    + -
    + ); +} + +export default connect( + (state: Record) => ({ + enabled: state.getIn(['components', 'player']).timelineZoom.enabled, + startTs: state.getIn(['components', 'player']).timelineZoom.startTs, + endTs: state.getIn(['components', 'player']).timelineZoom.endTs, + }), + { toggleZoom } +)(observer(TimelineZoomButton)); diff --git a/frontend/app/components/Session_/Player/Controls/components/ZoomDragLayer.tsx b/frontend/app/components/Session_/Player/Controls/components/ZoomDragLayer.tsx new file mode 100644 index 000000000..09a22f7d8 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/components/ZoomDragLayer.tsx @@ -0,0 +1,161 @@ +import React, { useCallback, useState } from 'react'; +import { connect } from 'react-redux'; + + + +import { getTimelinePosition } from 'Components/Session_/Player/Controls/getTimelinePosition'; +import { toggleZoom } from 'Duck/components/player'; + + +interface Props { + timelineZoomStartTs: number; + timelineZoomEndTs: number; + scale: number; + toggleZoom: typeof toggleZoom; +} + +const DraggableMarkers = ({ timelineZoomStartTs, timelineZoomEndTs, scale, toggleZoom }: Props) => { + const [startPos, setStartPos] = useState(getTimelinePosition(timelineZoomStartTs, scale)); + const [endPos, setEndPos] = useState(getTimelinePosition(timelineZoomEndTs, scale)); + const [dragging, setDragging] = useState(null); + + const convertToPercentage = useCallback((clientX: number, element: HTMLElement) => { + const rect = element.getBoundingClientRect(); + const x = clientX - rect.left; + return (x / rect.width) * 100; + }, []); + + const startDrag = useCallback( + (marker: 'start' | 'end' | 'body') => (event: React.MouseEvent) => { + event.stopPropagation(); + setDragging(marker); + }, + [convertToPercentage, startPos] + ); + + const minDistance = 1.5; + const onDrag = useCallback( + (event: any) => { + event.stopPropagation(); + if (dragging && event.clientX !== 0) { + const newPos = convertToPercentage(event.clientX, event.currentTarget); + if (dragging === 'start') { + setStartPos(newPos); + if (endPos - newPos <= minDistance) { + setEndPos(newPos + minDistance); + } + toggleZoom({ enabled: true, range: [newPos / scale, endPos / scale] }); + } else if (dragging === 'end') { + setEndPos(newPos); + if (newPos - startPos <= minDistance) { + setStartPos(newPos - minDistance); + } + toggleZoom({ enabled: true, range: [startPos / scale, newPos / scale] }); + } else if (dragging === 'body') { + const offset = (endPos - startPos) / 2; + let newStartPos = newPos - offset; + let newEndPos = newStartPos + (endPos - startPos); + if (newStartPos < 0) { + newStartPos = 0; + newEndPos = endPos - startPos; + } else if (newEndPos > 100) { + newEndPos = 100; + newStartPos = 100 - (endPos - startPos); + } + setStartPos(newStartPos); + setEndPos(newEndPos); + toggleZoom({ enabled: true, range: [newStartPos / scale, newEndPos / scale] }); + } + } + }, + [dragging, startPos, endPos, scale, toggleZoom] + ); + + const endDrag = useCallback(() => { + setDragging(null); + }, []); + + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default connect( + (state: Record) => ({ + timelineZoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs, + timelineZoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs, + }), + { toggleZoom } +)(DraggableMarkers); diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index ffaee7ce4..5ddd50fa3 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -11,8 +11,9 @@ import { useStore } from 'App/mstore'; import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal'; import { useModal } from 'App/components/Modal'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; -import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter' -import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache' +import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; +import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'; +import { connect } from 'react-redux'; const ALL = 'ALL'; const INFO = 'INFO'; @@ -25,13 +26,17 @@ const LEVEL_TAB = { [LogLevel.WARN]: WARNINGS, [LogLevel.ERROR]: ERRORS, [LogLevel.EXCEPTION]: ERRORS, -} as const +} as const; const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab })); function renderWithNL(s: string | null = '') { if (typeof s !== 'string') return ''; - return s.split('\n').map((line, i) =>
    {line}
    ); + return s.split('\n').map((line, i) => ( +
    + {line} +
    + )); } const getIconProps = (level: any) => { @@ -56,62 +61,80 @@ const getIconProps = (level: any) => { return null; }; - const INDEX_KEY = 'console'; -function ConsolePanel({ isLive }: { isLive?: boolean }) { +function ConsolePanel({ + isLive, + zoomEnabled, + zoomStartTs, + zoomEndTs, +}: { + isLive?: boolean; + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; +}) { const { sessionStore: { devTools }, - } = useStore() + } = useStore(); const filter = devTools[INDEX_KEY].filter; const activeTab = devTools[INDEX_KEY].activeTab; // Why do we need to keep index in the store? if we could get read of it it would simplify the code const activeIndex = devTools[INDEX_KEY].index; - const [ isDetailsModalActive, setIsDetailsModalActive ] = useState(false); + const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); const { showModal } = useModal(); - const { player, store } = React.useContext(PlayerContext) - const jump = (t: number) => player.jump(t) + const { player, store } = React.useContext(PlayerContext); + const jump = (t: number) => player.jump(t); - const { currentTab, tabStates } = store.get() - const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = tabStates[currentTab] + const { currentTab, tabStates } = store.get(); + const { + logList = [], + exceptionsList = [], + logListNow = [], + exceptionsListNow = [], + } = tabStates[currentTab]; - const list = isLive ? - useMemo(() => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time), - [logListNow.length, exceptionsListNow.length] - ) as ILog[] - : useMemo(() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time), - [ logList.length, exceptionsList.length ], - ) as ILog[] - let filteredList = useRegExListFilterMemo(list, l => l.value, filter) - filteredList = useTabListFilterMemo(filteredList, l => LEVEL_TAB[l.level], ALL, activeTab) + const list = isLive + ? (useMemo( + () => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time), + [logListNow.length, exceptionsListNow.length] + ) as ILog[]) + : (useMemo( + () => logList.concat(exceptionsList).sort((a, b) => a.time - b.time), + [logList.length, exceptionsList.length] + ).filter((l) => + zoomEnabled ? l.time >= zoomStartTs && l.time <= zoomEndTs : true + ) as ILog[]); + let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter); + filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab); React.useEffect(() => { setTimeout(() => { cache.clearAll(); _list.current?.recomputeRowHeights(); - }, 0) - }, [activeTab, filter]) - const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab }) - const onFilterChange = ({ target: { value } }: any) => devTools.update(INDEX_KEY, { filter: value }) + }, 0); + }, [activeTab, filter]); + const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab }); + const onFilterChange = ({ target: { value } }: any) => + devTools.update(INDEX_KEY, { filter: value }); - // AutoScroll - const [ - timeoutStartAutoscroll, - stopAutoscroll, - ] = useAutoscroll( + // AutoScroll + const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( filteredList, getLastItemTime(logListNow, exceptionsListNow), activeIndex, - index => devTools.update(INDEX_KEY, { index }) - ) - const onMouseEnter = stopAutoscroll + (index) => devTools.update(INDEX_KEY, { index }) + ); + const onMouseEnter = stopAutoscroll; const onMouseLeave = () => { - if (isDetailsModalActive) { return } - timeoutStartAutoscroll() - } - + if (isDetailsModalActive) { + return; + } + timeoutStartAutoscroll(); + }; + const _list = useRef(null); // TODO: fix react-virtualized types & encapsulate scrollToRow logic useEffect(() => { if (_list.current) { @@ -120,51 +143,45 @@ function ConsolePanel({ isLive }: { isLive?: boolean }) { } }, [activeIndex]); - const cache = useCellMeasurerCache() + const cache = useCellMeasurerCache(); const showDetails = (log: any) => { setIsDetailsModalActive(true); - showModal( - , - { - right: true, - width: 1200, - onClose: () => { - setIsDetailsModalActive(false) - timeoutStartAutoscroll() - } - }); + showModal(, { + right: true, + width: 1200, + onClose: () => { + setIsDetailsModalActive(false); + timeoutStartAutoscroll(); + }, + }); devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) }); - stopAutoscroll() - } + stopAutoscroll(); + }; const _rowRenderer = ({ index, key, parent, style }: any) => { const item = filteredList[index]; return ( - // @ts-ignore - - {({ measure, registerChild }) => ( -
    - showDetails(item)} - recalcHeight={measure} - /> -
    - )} -
    - ) - } + // @ts-ignore + + {({ measure, registerChild }) => ( +
    + showDetails(item)} + recalcHeight={measure} + /> +
    + )} +
    + ); + }; return ( - + {/* @ts-ignore */}
    @@ -220,4 +237,8 @@ function ConsolePanel({ isLive }: { isLive?: boolean }) { ); } -export default observer(ConsolePanel); +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, +}))(observer(ConsolePanel)); diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 79b964ab8..bca5f0148 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -18,7 +18,7 @@ import BottomBlock from '../BottomBlock'; import InfoLine from '../BottomBlock/InfoLine'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; -import WSModal from './WSModal' +import WSModal from './WSModal'; const INDEX_KEY = 'network'; @@ -150,7 +150,19 @@ function renderStatus({ status, cached }: { status: string; cached: boolean }) { ); } -function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panelHeight: number }) { +function NetworkPanelCont({ + startedAt, + panelHeight, + zoomEnabled, + zoomStartTs, + zoomEndTs, +}: { + startedAt: number; + panelHeight: number; + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; +}) { const { player, store } = React.useContext(PlayerContext); const { domContentLoadedTime, loadTime, domBuildingTime, tabStates, currentTab } = store.get(); @@ -184,9 +196,15 @@ function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panel function MobileNetworkPanelCont({ startedAt, panelHeight, + zoomEnabled, + zoomStartTs, + zoomEndTs, }: { startedAt: number; panelHeight: number; + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; }) { const { player, store } = React.useContext(MobilePlayerContext); @@ -219,6 +237,9 @@ function MobileNetworkPanelCont({ websocketList={websocketList} // @ts-ignore websocketListNow={websocketListNow} + zoomEnabled={zoomEnabled} + zoomStartTs={zoomStartTs} + zoomEndTs={zoomEndTs} /> ); } @@ -229,7 +250,7 @@ type WSMessage = Timed & { timestamp: number; dir: 'up' | 'down'; messageType: string; -} +}; interface Props { domContentLoadedTime?: { @@ -250,6 +271,9 @@ interface Props { player: WebPlayer | MobilePlayer; startedAt: number; isMobile?: boolean; + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; panelHeight: number; } @@ -267,6 +291,9 @@ const NetworkPanelComp = observer( isMobile, panelHeight, websocketList, + zoomEnabled, + zoomStartTs, + zoomEndTs, }: Props) => { const { showModal } = useModal(); const [sortBy, setSortBy] = useState('time'); @@ -333,6 +360,7 @@ const NetworkPanelComp = observer( transferredBodySize: 0, })) ) + .filter((req) => (zoomEnabled ? req.time >= zoomStartTs && req.time <= zoomEndTs : true)) .sort((a, b) => a.time - b.time), [resourceList.length, fetchList.length, socketList] ); @@ -407,13 +435,10 @@ const NetworkPanelComp = observer( if (item.type === 'websocket') { const socketMsgList = websocketList.filter((ws) => ws.channelName === item.channelName); - return showModal( - , { - right: true, width: 700, - } - ) + return showModal(, { + right: true, + width: 700, + }); } setIsDetailsModalActive(true); showModal( @@ -583,10 +608,16 @@ const NetworkPanelComp = observer( const WebNetworkPanel = connect((state: any) => ({ startedAt: state.getIn(['sessions', 'current']).startedAt, + zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled, + zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs, + zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs, }))(observer(NetworkPanelCont)); const MobileNetworkPanel = connect((state: any) => ({ startedAt: state.getIn(['sessions', 'current']).startedAt, + zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled, + zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs, + zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs, }))(observer(MobileNetworkPanelCont)); export { WebNetworkPanel, MobileNetworkPanel }; diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx index 8bf4df447..d027f4a35 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx +++ b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx @@ -14,45 +14,102 @@ import StackEventModal from '../StackEventModal'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'; +import { connect } from 'react-redux'; const mapNames = (type: string) => { if (type === 'openreplay') return 'OpenReplay'; - return type -} + return type; +}; const INDEX_KEY = 'stackEvent'; const ALL = 'ALL'; const TAB_KEYS = [ALL, ...typeList] as const; const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab })); -type EventsList = Array +type EventsList = Array; -export const WebStackEventPanel = observer(() => { - const { player, store } = React.useContext(PlayerContext); - const jump = (t: number) => player.jump(t); - const { currentTab, tabStates } = store.get(); +const WebStackEventPanelComp = observer( + ({ + zoomEnabled, + zoomStartTs, + zoomEndTs, + }: { + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; + }) => { + const { player, store } = React.useContext(PlayerContext); + const jump = (t: number) => player.jump(t); + const { currentTab, tabStates } = store.get(); - const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab]; + const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab]; - return ; -}); + return ( + + ); + } +); -export const MobileStackEventPanel = observer(() => { - const { player, store } = React.useContext(MobilePlayerContext); - const jump = (t: number) => player.jump(t); - const { eventList: list = [], eventListNow: listNow = [] } = store.get(); +export const WebStackEventPanel = connect((state: Record) => ({ + zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled, + zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs, + zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs, +}))(WebStackEventPanelComp); - return ; -}); +const MobileStackEventPanelComp = observer( + ({ + zoomEnabled, + zoomStartTs, + zoomEndTs, + }: { + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; + }) => { + const { player, store } = React.useContext(MobilePlayerContext); + const jump = (t: number) => player.jump(t); + const { eventList: list = [], eventListNow: listNow = [] } = store.get(); + + return ( + + ); + } +); + +export const MobileStackEventPanel = connect((state: Record) => ({ + zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled, + zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs, + zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs, +}))(MobileStackEventPanelComp); function EventsPanel({ list, listNow, jump, + zoomEnabled, + zoomStartTs, + zoomEndTs, }: { list: EventsList; listNow: EventsList; jump: (t: number) => void; + zoomEnabled: boolean; + zoomStartTs: number; + zoomEndTs: number; }) { const { sessionStore: { devTools }, @@ -63,7 +120,14 @@ function EventsPanel({ const activeTab = devTools[INDEX_KEY].activeTab; const activeIndex = devTools[INDEX_KEY].index; - let filteredList = useRegExListFilterMemo(list, (it) => it.name, filter); + const inZoomRangeList = list.filter(({ time }) => + zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true + ); + const inZoomRangeListNow = listNow.filter(({ time }) => + zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true + ); + + let filteredList = useRegExListFilterMemo(inZoomRangeList, (it) => it.name, filter); filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab); const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) => @@ -71,13 +135,13 @@ function EventsPanel({ const onFilterChange = ({ target: { value } }: React.ChangeEvent) => devTools.update(INDEX_KEY, { filter: value }); const tabs = useMemo( - () => TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)), - [list.length] + () => TABS.filter(({ key }) => key === ALL || inZoomRangeList.some(({ source }) => key === source)), + [inZoomRangeList.length] ); const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( filteredList, - getLastItemTime(listNow), + getLastItemTime(inZoomRangeListNow), activeIndex, (index) => devTools.update(INDEX_KEY, { index }) ); @@ -138,15 +202,17 @@ function EventsPanel({ }; return ( - +
    Stack Events - +
    ) => { + state.fullscreen = action.payload !== undefined ? action.payload : !state.fullscreen; + }, + toggleBottomBlock: (state, action: PayloadAction) => { + state.bottomBlock = state.bottomBlock !== action.payload && action.payload !== 0 ? action.payload : 0; + }, + closeBottomBlock: (state) => { + state.bottomBlock = 0; + }, + changeSkipInterval: (state, action: PayloadAction) => { + const skipInterval = action.payload; + localStorage.setItem('CHANGE_SKIP_INTERVAL', skipInterval.toString()); + state.skipInterval = skipInterval; + }, + hideHint: (state, action: PayloadAction<'storage' | 'stack'>) => { + const name = action.payload; + localStorage.setItem(`${name}HideHint`, 'true'); + state.hiddenHints[name] = 'true'; + state.bottomBlock = 0; + }, + toggleZoom: (state, action: PayloadAction) => { + const { enabled, range } = action.payload; + state.timelineZoom = { + enabled, + startTs: range?.[0] || 0, + endTs: range?.[1] || 0, + }; + }, + setZoomTab: (state, action: PayloadAction<'overview' | 'journey' | 'issues' | 'errors'>) => { + state.zoomTab = action.payload; + } + }, +}); + +interface ToggleZoomPayload { enabled: boolean, range?: [number, number]} + +export const { toggleFullscreen, toggleBottomBlock, changeSkipInterval, hideHint, toggleZoom, setZoomTab, closeBottomBlock } = playerSlice.actions; + +export default playerSlice.reducer; export const NONE = 0; export const CONSOLE = 1; @@ -26,7 +102,7 @@ export const blocks = { exceptions: EXCEPTIONS, inspector: INSPECTOR, overview: OVERVIEW, -} as const +} as const; export const blockValues = [ NONE, @@ -41,84 +117,4 @@ export const blockValues = [ EXCEPTIONS, INSPECTOR, OVERVIEW, -] as const - -const TOGGLE_FULLSCREEN = 'player/TOGGLE_FS'; -const TOGGLE_BOTTOM_BLOCK = 'player/SET_BOTTOM_BLOCK'; -const HIDE_HINT = 'player/HIDE_HINT'; -const CHANGE_INTERVAL = 'player/CHANGE_SKIP_INTERVAL' - -const initialState = Map({ - fullscreen: false, - bottomBlock: NONE, - hiddenHints: Map({ - storage: localStorage.getItem('storageHideHint'), - stack: localStorage.getItem('stackHideHint') - }), - skipInterval: localStorage.getItem(CHANGE_INTERVAL) || 10, -}); - -const reducer = (state = initialState, action: any = {}) => { - switch (action.type) { - case TOGGLE_FULLSCREEN: - const { flag } = action - return state.update('fullscreen', fs => typeof flag === 'boolean' ? flag : !fs); - case TOGGLE_BOTTOM_BLOCK: - const { bottomBlock } = action; - if (state.get('bottomBlock') !== bottomBlock && bottomBlock !== NONE) { - } - return state.update('bottomBlock', bb => bb === bottomBlock ? NONE : bottomBlock); - case CHANGE_INTERVAL: - const { skipInterval } = action; - localStorage.setItem(CHANGE_INTERVAL, skipInterval); - return state.update('skipInterval', () => skipInterval); - case HIDE_HINT: - const { name } = action; - localStorage.setItem(`${name}HideHint`, 'true'); - return state - .setIn([ "hiddenHints", name ], true) - .set('bottomBlock', NONE); - - } - return state; -}; - -export default reducer; - -export function toggleFullscreen(flag: any) { - return { - type: TOGGLE_FULLSCREEN, - flag, - }; -} -export function fullscreenOff() { - return toggleFullscreen(false); -} -export function fullscreenOn() { - return toggleFullscreen(true); -} - -export function toggleBottomBlock(bottomBlock = NONE) { - return { - bottomBlock, - type: TOGGLE_BOTTOM_BLOCK, - }; -} - -export function closeBottomBlock() { - return toggleBottomBlock(); -} - -export function changeSkipInterval(skipInterval: number) { - return { - skipInterval, - type: CHANGE_INTERVAL, - }; -} - -export function hideHint(name: string) { - return { - name, - type: HIDE_HINT, - } -} +] as const; diff --git a/frontend/app/mstore/aiSummaryStore.ts b/frontend/app/mstore/aiSummaryStore.ts index 42301af1d..662a16707 100644 --- a/frontend/app/mstore/aiSummaryStore.ts +++ b/frontend/app/mstore/aiSummaryStore.ts @@ -1,9 +1,11 @@ -import { aiService } from 'App/services'; import { makeAutoObservable } from 'mobx'; +import { aiService } from 'App/services'; + export default class AiSummaryStore { text = ''; toggleSummary = false; + isLoading = false; constructor() { makeAutoObservable(this); @@ -17,7 +19,14 @@ export default class AiSummaryStore { this.toggleSummary = toggleSummary; } + setLoading(loading: boolean) { + this.isLoading = loading; + } + getSummary = async (sessionId: string) => { + if (this.isLoading) return; + + this.setLoading(true); this.setText(''); try { const respText = await aiService.getSummary(sessionId); @@ -26,6 +35,25 @@ export default class AiSummaryStore { this.setText(respText); } catch (e) { console.error(e); + } finally { + this.setLoading(false); } }; + + getDetailedSummary = async (sessionId: string, networkEvents: any[], feat: 'errors' | 'issues' | 'journey', startTs: number, endTs: number) => { + if (this.isLoading) return; + + this.setLoading(true); + this.setText(''); + try { + const respText = await aiService.getDetailedSummary(sessionId, networkEvents,feat, startTs, endTs); + if (!respText) return; + + this.setText(respText); + } catch (e) { + console.error(e); + } finally { + this.setLoading(false); + } + } } diff --git a/frontend/app/services/AiService.ts b/frontend/app/services/AiService.ts index 0bda0081c..ad1cef859 100644 --- a/frontend/app/services/AiService.ts +++ b/frontend/app/services/AiService.ts @@ -4,19 +4,31 @@ export default class AiService extends BaseService { /** * @returns stream of text symbols * */ - async getSummary(sessionId: string): Promise { - const r = await this.client.post( - `/sessions/${sessionId}/intelligent/summary`, - ); + async getSummary(sessionId: string, start?: number, end?: number): Promise { + const r = await this.client.post(`/sessions/${sessionId}/intelligent/summary`, { + frameStartTimestamp: start, + frameEndTimestamp: end, + }); - return r.json() + return r.json(); + } + + async getDetailedSummary(sessionId: string, networkEvents: any[], feat: 'errors' | 'issues' | 'journey', start: number, end: number): Promise { + const r = await this.client.post(`/sessions/${sessionId}/intelligent/detailed-summary`, { + event: feat, + frameStartTimestamp: start, + frameEndTimestamp: end, + devtoolsEvents: networkEvents, + }); + + return r.json(); } async getSearchFilters(query: string): Promise> { const r = await this.client.post('/intelligent/search', { - question: query - }) + question: query, + }); const { data } = await r.json(); - return data + return data; } } diff --git a/frontend/package.json b/frontend/package.json index 0e9bba33e..f0a8ced3c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@babel/plugin-transform-private-methods": "^7.23.3", "@floating-ui/react-dom-interactions": "^0.10.3", "@medv/finder": "^3.1.0", + "@reduxjs/toolkit": "^2.2.2", "@sentry/browser": "^5.21.1", "@svg-maps/world": "^1.0.1", "@svgr/webpack": "^6.2.1", @@ -112,6 +113,7 @@ "@storybook/manager-webpack5": "^6.5.12", "@storybook/react": "^6.5.12", "@storybook/testing-library": "^0.0.13", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/luxon": "^3.0.0", "@types/react": "^18.0.9", "@types/react-dom": "^18.0.4", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4dddbae0b..aeb929a15 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -198,6 +198,17 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:7.17.7": + version: 7.17.7 + resolution: "@babel/generator@npm:7.17.7" + dependencies: + "@babel/types": ^7.17.0 + jsesc: ^2.5.1 + source-map: ^0.5.0 + checksum: 8088453c4418e0ee6528506fbd5847bbdfd56327a0025ca9496a259261e162c594ffd08be0d63e74c32feced795616772f38acc5f5e493a86a45fd439fd9feb0 + languageName: node + linkType: hard + "@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.4, @babel/generator@npm:^7.7.2": version: 7.23.4 resolution: "@babel/generator@npm:7.23.4" @@ -210,6 +221,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.23.0": + version: 7.24.1 + resolution: "@babel/generator@npm:7.24.1" + dependencies: + "@babel/types": ^7.24.0 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + jsesc: ^2.5.1 + checksum: f0eea7497657cdf68cfb4b7d181588e1498eefd1f303d73b0d8ca9b21a6db27136a6f5beb8f988b6bdcd4249870826080950450fd310951de42ecf36df274881 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.18.6, @babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -448,7 +471,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20": +"@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" checksum: dcad63db345fb110e032de46c3688384b0008a42a4845180ce7cd62b1a9c0507a1bed727c4d1060ed1a03ae57b4d918570259f81724aaac1a5b776056f37504e @@ -522,6 +545,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.23.0": + version: 7.24.1 + resolution: "@babel/parser@npm:7.24.1" + bin: + parser: ./bin/babel-parser.js + checksum: d2a8b99aa5f33182b69d5569367403a40e7c027ae3b03a1f81fd8ac9b06ceb85b31f6ee4267fb90726dc2ac99909c6bdaa9cf16c379efab73d8dfe85cee32c50 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -1850,6 +1882,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:7.23.2": + version: 7.23.2 + resolution: "@babel/traverse@npm:7.23.2" + dependencies: + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.23.0 + "@babel/types": ^7.23.0 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: d096c7c4bab9262a2f658298a3c630ae4a15a10755bb257ae91d5ab3e3b2877438934859c8d34018b7727379fe6b26c4fa2efc81cf4c462a7fe00caf79fa02ff + languageName: node + linkType: hard + "@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.23.3, @babel/traverse@npm:^7.23.4": version: 7.23.4 resolution: "@babel/traverse@npm:7.23.4" @@ -1868,6 +1918,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:7.17.0": + version: 7.17.0 + resolution: "@babel/types@npm:7.17.0" + dependencies: + "@babel/helper-validator-identifier": ^7.16.7 + to-fast-properties: ^2.0.0 + checksum: ad09224272b40fedb00b262677d12b6838f5b5df5c47d67059ba1181bd4805439993393a8de32459dae137b536d60ebfcaf39ae84d8b3873f1e81cc75f5aeae8 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.13.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.23.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.4 resolution: "@babel/types@npm:7.23.4" @@ -1879,6 +1939,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.17.0, @babel/types@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/types@npm:7.24.0" + dependencies: + "@babel/helper-string-parser": ^7.23.4 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 777a0bb5dbe038ca4c905fdafb1cdb6bdd10fe9d63ce13eca0bd91909363cbad554a53dc1f902004b78c1dcbc742056f877f2c99eeedff647333b1fadf51235d + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -2661,6 +2732,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": ^1.2.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: 1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -2675,6 +2757,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -2712,6 +2801,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -3087,6 +3186,26 @@ __metadata: languageName: node linkType: hard +"@reduxjs/toolkit@npm:^2.2.2": + version: 2.2.2 + resolution: "@reduxjs/toolkit@npm:2.2.2" + dependencies: + immer: ^10.0.3 + redux: ^5.0.1 + redux-thunk: ^3.1.0 + reselect: ^5.0.1 + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + checksum: d749181b1bc071698517cba7ce05c42ddfe99363019249722b4dfa3afc71b3a6e4cb9885af574cf81c5d6515f68201ebfedddb5c14b262c941a45112fdc66ce3 + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.2.0": version: 5.3.1 resolution: "@rollup/plugin-babel@npm:5.3.1" @@ -4733,6 +4852,26 @@ __metadata: languageName: node linkType: hard +"@trivago/prettier-plugin-sort-imports@npm:^4.3.0": + version: 4.3.0 + resolution: "@trivago/prettier-plugin-sort-imports@npm:4.3.0" + dependencies: + "@babel/generator": 7.17.7 + "@babel/parser": ^7.20.5 + "@babel/traverse": 7.23.2 + "@babel/types": 7.17.0 + javascript-natural-sort: 0.7.1 + lodash: ^4.17.21 + peerDependencies: + "@vue/compiler-sfc": 3.x + prettier: 2.x - 3.x + peerDependenciesMeta: + "@vue/compiler-sfc": + optional: true + checksum: 42270fb9c89e54a3f8b6ac8c43e6d0e03350e2857e902cdad4de22c78ef1864da600525595311bc7e94e51c16c7dd3882c2e048a162fdab59761ffa893756aa2 + languageName: node + linkType: hard + "@trysound/sax@npm:0.2.0": version: 0.2.0 resolution: "@trysound/sax@npm:0.2.0" @@ -13568,6 +13707,13 @@ __metadata: languageName: node linkType: hard +"immer@npm:^10.0.3": + version: 10.0.4 + resolution: "immer@npm:10.0.4" + checksum: da9da59d6e71cf3f2875024b5cfb58874baef3eefec6425483e53163e31ed0ab24bae85cd2829cb7812acb9723075eedb5946654f8bd47ecf38663388e04d3bd + languageName: node + linkType: hard + "immutable@npm:^3.7.2": version: 3.8.2 resolution: "immutable@npm:3.8.2" @@ -14657,6 +14803,13 @@ __metadata: languageName: node linkType: hard +"javascript-natural-sort@npm:0.7.1": + version: 0.7.1 + resolution: "javascript-natural-sort@npm:0.7.1" + checksum: 340f8ffc5d30fb516e06dc540e8fa9e0b93c865cf49d791fed3eac3bdc5fc71f0066fc81d44ec1433edc87caecaf9f13eec4a1fce8c5beafc709a71eaedae6fe + languageName: node + linkType: hard + "jest-changed-files@npm:^29.7.0": version: 29.7.0 resolution: "jest-changed-files@npm:29.7.0" @@ -18107,6 +18260,7 @@ __metadata: "@jest/globals": ^29.7.0 "@medv/finder": ^3.1.0 "@openreplay/sourcemap-uploader": ^3.0.8 + "@reduxjs/toolkit": ^2.2.2 "@sentry/browser": ^5.21.1 "@storybook/addon-actions": ^6.5.12 "@storybook/addon-docs": ^6.5.12 @@ -18119,6 +18273,7 @@ __metadata: "@storybook/testing-library": ^0.0.13 "@svg-maps/world": ^1.0.1 "@svgr/webpack": ^6.2.1 + "@trivago/prettier-plugin-sort-imports": ^4.3.0 "@types/luxon": ^3.0.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.4 @@ -21415,6 +21570,15 @@ __metadata: languageName: node linkType: hard +"redux-thunk@npm:^3.1.0": + version: 3.1.0 + resolution: "redux-thunk@npm:3.1.0" + peerDependencies: + redux: ^5.0.0 + checksum: 21557f6a30e1b2e3e470933247e51749be7f1d5a9620069a3125778675ce4d178d84bdee3e2a0903427a5c429e3aeec6d4df57897faf93eb83455bc1ef7b66fd + languageName: node + linkType: hard + "redux@npm:^4.0.0, redux@npm:^4.0.5, redux@npm:^4.1.2, redux@npm:^4.2.0": version: 4.2.1 resolution: "redux@npm:4.2.1" @@ -21424,6 +21588,13 @@ __metadata: languageName: node linkType: hard +"redux@npm:^5.0.1": + version: 5.0.1 + resolution: "redux@npm:5.0.1" + checksum: b10c28357194f38e7d53b760ed5e64faa317cc63de1fb95bc5d9e127fab956392344368c357b8e7a9bedb0c35b111e7efa522210cfdc3b3c75e5074718e9069c + languageName: node + linkType: hard + "reflect.getprototypeof@npm:^1.0.4": version: 1.0.4 resolution: "reflect.getprototypeof@npm:1.0.4" @@ -21757,6 +21928,13 @@ __metadata: languageName: node linkType: hard +"reselect@npm:^5.0.1": + version: 5.1.0 + resolution: "reselect@npm:5.1.0" + checksum: b0ed789f4f6f10dfbd23741823726793384932969aa7ce8f584c882ad87620a02b09b5d1146cd2ea6eaa0953b3fd9f7df22f113893af73f35f28432a8a4294de + languageName: node + linkType: hard + "resize-observer-polyfill@npm:^1.5.1": version: 1.5.1 resolution: "resize-observer-polyfill@npm:1.5.1"