diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
index 8228c0495..d7312fdc0 100644
--- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
+++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx
@@ -319,10 +319,11 @@ function PanelComponent({
isGraph={feature === 'PERFORMANCE'}
title={feature}
list={resources[feature]}
- renderElement={(pointer: any) => (
+ renderElement={(pointer: any[], isGrouped: boolean) => (
)}
diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx
index e063daddb..784d53d3f 100644
--- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx
+++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx
@@ -9,7 +9,7 @@ interface Props {
message?: string;
className?: string;
endTime?: number;
- renderElement?: (item: any) => React.ReactNode;
+ renderElement?: (item: any, isGrouped: boolean) => React.ReactNode;
isGraph?: boolean;
zIndex?: number;
noMargin?: boolean;
@@ -18,15 +18,70 @@ const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
const scale = 100 / endTime;
const _list =
- !isGraph &&
+ isGraph ? [] :
React.useMemo(() => {
- return list.map((item: any, _index: number) => {
- const spread = item.toJS ? { ...item.toJS() } : { ...item };
- return {
- ...spread,
- left: getTimelinePosition(item.time, scale),
- };
- });
+ const tolerance = 2; // within what %s to group items
+ const groupedItems = [];
+ let currentGroup = [];
+ let currentLeft = 0;
+
+ for (let i = 0; i < list.length; i++) {
+ const item = list[i];
+ const spread = item.toJS ? { ...item.toJS() } : item;
+ const left: number = getTimelinePosition(item.time, scale);
+ const itemWithLeft = { ...spread, left };
+
+ if (currentGroup.length === 0) {
+ currentGroup.push(itemWithLeft);
+ currentLeft = left;
+ } else {
+ if (Math.abs(left - currentLeft) <= tolerance) {
+ currentGroup.push(itemWithLeft);
+ } else {
+ if (currentGroup.length > 1) {
+ const leftValues = currentGroup.map(item => item.left);
+ const minLeft = Math.min(...leftValues);
+ const maxLeft = Math.max(...leftValues);
+ const middleLeft = (minLeft + maxLeft) / 2;
+
+ groupedItems.push({
+ isGrouped: true,
+ items: currentGroup,
+ left: middleLeft,
+ });
+ } else {
+ groupedItems.push({
+ isGrouped: false,
+ items: [currentGroup[0]],
+ left: currentGroup[0].left,
+ });
+ }
+ currentGroup = [itemWithLeft];
+ currentLeft = left;
+ }
+ }
+ }
+
+ if (currentGroup.length > 1) {
+ const leftValues = currentGroup.map(item => item.left);
+ const minLeft = Math.min(...leftValues);
+ const maxLeft = Math.max(...leftValues);
+ const middleLeft = (minLeft + maxLeft) / 2;
+
+ groupedItems.push({
+ isGrouped: true,
+ items: currentGroup,
+ left: middleLeft,
+ });
+ } else if (currentGroup.length === 1) {
+ groupedItems.push({
+ isGrouped: false,
+ items: [currentGroup[0]],
+ left: currentGroup[0].left,
+ });
+ }
+
+ return groupedItems;
}, [list]);
return (
@@ -52,17 +107,18 @@ const EventRow = React.memo((props: Props) => {
{isGraph ? (
) : _list.length > 0 ? (
- _list.map((item: any, index: number) => {
+ _list.map((item: { items: any[], left: number, isGrouped: boolean }, index: number) => {
+ const left = item.left
return (
- {props.renderElement ? props.renderElement(item) : null}
+ {props.renderElement ? props.renderElement(item.items, item.isGrouped) : null}
);
})
diff --git a/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx
index 8b802953d..2a02bcfa5 100644
--- a/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx
+++ b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx
@@ -15,6 +15,9 @@ const OverviewPanelContainer = React.memo((props: Props) => {
const [mouseX, setMouseX] = React.useState(0);
const [mouseIn, setMouseIn] = React.useState(false);
const onClickTrack = (e: any) => {
+ if (e.target.className.includes('ant-popover')) {
+ return;
+ }
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
if (time) {
diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/Dots.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/Dots.tsx
new file mode 100644
index 000000000..51db59065
--- /dev/null
+++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/Dots.tsx
@@ -0,0 +1,148 @@
+import React from "react";
+import { EXCEPTIONS, NETWORK } from "App/mstore/uiPlayerStore";
+import { TYPES } from "App/types/session/event";
+import { types as issueTypes } from "App/types/session/issue";
+import { Icon } from "UI";
+import { Tooltip } from "antd";
+
+interface CommonProps {
+ item: any;
+ createEventClickHandler: any;
+}
+
+export function shortenResourceName(name: string) {
+ return name.length > 100
+ ? name.slice(0, 100) + ' ... ' + name.slice(-50)
+ : name
+}
+export function NetworkElement({ item, createEventClickHandler }: CommonProps) {
+ const name = item.name || '';
+ return (
+
+ {item.success ? 'Slow resource: ' : '4xx/5xx Error:'}
+
+ {shortenResourceName(name)}
+
+ }
+ >
+
+
+ );
+}
+
+export function getFrustration(item: any) {
+ const elData = { name: '', icon: '' };
+ if (item.type === TYPES.CLICK)
+ Object.assign(elData, {
+ name: `User hesitated to click for ${Math.round(
+ item.hesitation / 1000
+ )}s`,
+ icon: 'click-hesitation',
+ });
+ if (item.type === TYPES.INPUT)
+ Object.assign(elData, {
+ name: `User hesitated to enter a value for ${Math.round(
+ item.hesitation / 1000
+ )}s`,
+ icon: 'input-hesitation',
+ });
+ if (item.type === TYPES.CLICKRAGE || item.type === TYPES.TAPRAGE)
+ Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' });
+ if (item.type === TYPES.DEAD_LICK)
+ Object.assign(elData, { name: 'Dead Click', icon: 'emoji-dizzy' });
+ if (item.type === issueTypes.MOUSE_THRASHING)
+ Object.assign(elData, { name: 'Mouse Thrashing', icon: 'cursor-trash' });
+ if (item.type === 'ios_perf_event')
+ Object.assign(elData, { name: item.name, icon: item.icon });
+
+ return elData;
+}
+export function FrustrationElement({ item, createEventClickHandler }: CommonProps) {
+ const elData = getFrustration(item);
+ return (
+
+ {elData.name}
+
+ }
+ >
+
+
+
+
+ );
+}
+
+export function StackEventElement({ item, createEventClickHandler }: CommonProps) {
+ return (
+
+ {item.name || 'Stack Event'}
+
+ }
+ >
+
+ {/* */}
+
+
+ );
+}
+
+export function PerformanceElement({ item, createEventClickHandler }: CommonProps) {
+ return (
+
+ {item.type}
+
+ }
+ >
+
+ {/* */}
+
+
+ );
+}
+
+export function ExceptionElement({ item, createEventClickHandler }: CommonProps) {
+ return (
+
+ {'Exception'}
+
+ {item.message}
+
+ }
+ >
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx
index 028f54922..cc7f13f63 100644
--- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx
+++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx
@@ -1,24 +1,40 @@
import React from 'react';
-import { NETWORK, EXCEPTIONS } from 'App/mstore/uiPlayerStore';
import { useModal } from 'App/components/Modal';
import { Icon } from 'UI';
+import { shortDurationFromMs } from "App/date";
import StackEventModal from '../StackEventModal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import FetchDetails from 'Shared/FetchDetailsModal';
import GraphQLDetailsModal from 'Shared/GraphQLDetailsModal';
import { PlayerContext } from 'App/components/Session/playerContext';
-import { TYPES } from 'App/types/session/event'
-import { types as issueTypes } from 'App/types/session/issue'
-import { Tooltip } from 'antd';
+import { Popover } from 'antd';
+import {
+ shortenResourceName,
+ NetworkElement,
+ getFrustration,
+ FrustrationElement,
+ StackEventElement,
+ PerformanceElement,
+ ExceptionElement,
+} from './Dots'
interface Props {
pointer: any;
- type: 'ERRORS' | 'EVENT' | 'NETWORK' | 'FRUSTRATIONS' | 'EVENTS' | 'PERFORMANCE';
+ type:
+ | 'ERRORS'
+ | 'EVENT'
+ | 'NETWORK'
+ | 'FRUSTRATIONS'
+ | 'EVENTS'
+ | 'PERFORMANCE'
noClick?: boolean;
fetchPresented?: boolean;
+ isGrouped?: boolean;
}
const TimelinePointer = React.memo((props: Props) => {
- const { player } = React.useContext(PlayerContext)
+ const { pointer, type, isGrouped } = props;
+ const { player } = React.useContext(PlayerContext);
+ const item = isGrouped ? pointer : pointer[0]
const { showModal } = useModal();
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
@@ -30,150 +46,162 @@ const TimelinePointer = React.memo((props: Props) => {
}
if (type === 'ERRORS') {
- showModal(, { right: true, width: 1200 });
+ showModal(, {
+ right: true,
+ width: 1200,
+ });
}
if (type === 'EVENT') {
- showModal(, { right: true, width: 450 });
+ showModal(, {
+ right: true,
+ width: 450,
+ });
}
- if (type === NETWORK) {
+ if (type === 'NETWORK') {
if (pointer.tp === 'graph_ql') {
- showModal(, { right: true, width: 500 });
+ showModal(, {
+ right: true,
+ width: 500,
+ });
} else {
- showModal(, { right: true, width: 500 });
+ showModal(
+ ,
+ { right: true, width: 500 }
+ );
}
}
- // props.toggleBottomBlock(type);
};
- const renderNetworkElement = (item: any) => {
- const name = item.name || '';
+ if (isGrouped) {
+ const onClick = createEventClickHandler(item[0], type);
+ return ;
+ }
+
+ if (type === 'NETWORK') {
return (
-
- {item.success ? 'Slow resource: ' : '4xx/5xx Error:'}
-
- {name.length > 200
- ? name.slice(0, 100) + ' ... ' + name.slice(-50)
- : name.length > 200
- ? item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)
- : item.name}
-
- }
- >
-
-
+
);
- };
-
- const renderFrustrationElement = (item: any) => {
- const elData = { name: '', icon: ''}
- if (item.type === TYPES.CLICK) Object.assign(elData, { name: `User hesitated to click for ${Math.round(item.hesitation/1000)}s`, icon: 'click-hesitation' })
- if (item.type === TYPES.INPUT) Object.assign(elData, { name: `User hesitated to enter a value for ${Math.round(item.hesitation/1000)}s`, icon: 'input-hesitation' })
- if (item.type === TYPES.CLICKRAGE || item.type === TYPES.TAPRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' })
- if (item.type === TYPES.DEAD_LICK) Object.assign(elData, { name: 'Dead Click', icon: 'emoji-dizzy' })
- if (item.type === issueTypes.MOUSE_THRASHING) Object.assign(elData, { name: 'Mouse Thrashing', icon: 'cursor-trash' })
- if (item.type === 'ios_perf_event') Object.assign(elData, { name: item.name, icon: item.icon })
-
+ }
+ if (type === 'FRUSTRATIONS') {
return (
-
- {elData.name}
-
- }
- >
-
-
-
-
+
);
- };
-
- const renderStackEventElement = (item: any) => {
+ }
+ if (type === 'ERRORS') {
return (
-
- {item.name || 'Stack Event'}
-
- }
- >
-
- {/* */}
-
-
+
);
- };
-
- const renderPerformanceElement = (item: any) => {
+ }
+ if (type === 'EVENTS') {
return (
-
- {item.type}
-
- }
- >
-
- {/* */}
-
-
+
);
- };
+ }
- const renderExceptionElement = (item: any) => {
+ if (type === 'PERFORMANCE') {
return (
-
- {'Exception'}
-
- {item.message}
-
- }
- >
-
-
+
);
- };
+ }
- const render = () => {
- const { pointer, type } = props;
- if (type === 'NETWORK') {
- return renderNetworkElement(pointer);
- }
- if (type === 'FRUSTRATIONS') {
- return renderFrustrationElement(pointer);
- }
- if (type === 'ERRORS') {
- return renderExceptionElement(pointer);
- }
- if (type === 'EVENTS') {
- return renderStackEventElement(pointer);
- }
-
- if (type === 'PERFORMANCE') {
- return renderPerformanceElement(pointer);
- }
- };
- return {render()}
;
+ return unknown type
;
});
+function GroupedIssue({
+ type,
+ items,
+ onClick,
+ createEventClickHandler,
+}: {
+ type: string;
+ items: Record[];
+ onClick: () => void;
+ createEventClickHandler: any;
+}) {
+ const subStr = {
+ NETWORK: 'Network Issues',
+ ERRORS: 'Errors',
+ EVENTS: 'Events',
+ FRUSTRATIONS: 'Frustrations',
+ };
+ const title = `${items.length} ${subStr[type]} Observed`;
+
+ return (
+
+ {items.map((pointer) => (
+
+
@{shortDurationFromMs(pointer.time)}
+
+
+ ))}
+
+ }
+ >
+
+ {items.length}
+
+
+ );
+}
+
+function RenderLineData({ item, type }: any) {
+ if (type === 'FRUSTRATIONS') {
+ const elData = getFrustration(item);
+ return <>
+
+ {elData.name}
+ >
+ }
+ if (type === 'NETWORK') {
+ const name = item.success ? 'Slow resource' : '4xx/5xx Error';
+ return <>
+ {name}
+ {shortenResourceName(item.name)}
+ >
+ }
+ if (type === 'EVENTS') {
+ return {item.name || 'Stack Event'}
+ }
+ if (type === 'PERFORMANCE') {
+ return {item.type}
+ }
+ if (type === 'ERRORS') {
+ return {item.message}
+ }
+ return {JSON.stringify(item)}
+}
+
export default TimelinePointer;
diff --git a/frontend/app/components/Session_/OverviewPanel/fakeData.ts b/frontend/app/components/Session_/OverviewPanel/fakeData.ts
new file mode 100644
index 000000000..0f3e42c3c
--- /dev/null
+++ b/frontend/app/components/Session_/OverviewPanel/fakeData.ts
@@ -0,0 +1,80 @@
+const zoomStartTime = 100
+// Generate fake fetchList data for NETWORK
+const fetchList: any[] = [];
+for (let i = 0; i < 100; i++) {
+ const statusOptions = [200, 200, 200, 404, 500]; // Higher chance of 200
+ const status = statusOptions[Math.floor(Math.random() * statusOptions.length)];
+ const isRed = status >= 500;
+ const isYellow = status >= 400 && status < 500;
+ const resource = {
+ time: zoomStartTime + i * 1000 + Math.floor(Math.random() * 500), // Incremental time with randomness
+ name: `https://api.example.com/resource/${i}`,
+ status: status,
+ isRed: isRed,
+ isYellow: isYellow,
+ success: status < 400,
+ tp: Math.random() > 0.5 ? 'graph_ql' : 'fetch',
+ // Additional properties used by your component
+ method: 'GET',
+ duration: Math.floor(Math.random() * 3000) + 500, // Duration between 500ms to 3.5s
+ };
+ fetchList.push(resource);
+}
+// Generate fake exceptionsList data for ERRORS
+const exceptionsList: any[] = [];
+for (let i = 0; i < 50; i++) {
+ const exception = {
+ time: zoomStartTime + i * 2000 + Math.floor(Math.random() * 1000),
+ message: `Error message ${i}`,
+ errorId: `error-${i}`,
+ type: 'ERRORS',
+ // Additional properties if needed
+ stackTrace: `Error at function ${i} in file${i}.js`,
+ };
+ exceptionsList.push(exception);
+}
+// Generate fake eventsList data for EVENTS
+const eventsList: any[] = [];
+for (let i = 0; i < 50; i++) {
+ const event = {
+ time: zoomStartTime + i * 1500 + Math.floor(Math.random() * 500),
+ name: `Custom Event ${i}`,
+ type: 'EVENTS',
+ // Additional properties if needed
+ details: `Details about event ${i}`,
+ };
+ eventsList.push(event);
+}
+// Generate fake performanceChartData data for PERFORMANCE
+const performanceChartData: any[] = [];
+const performanceTypes = ['SLOW_PAGE_LOAD', 'HIGH_MEMORY_USAGE'];
+for (let i = 0; i < 30; i++) {
+ const performanceEvent = {
+ time: zoomStartTime + i * 3000 + Math.floor(Math.random() * 1500),
+ type: performanceTypes[Math.floor(Math.random() * performanceTypes.length)],
+ // Additional properties if needed
+ value: Math.floor(Math.random() * 1000) + 500, // Random value
+ };
+ performanceChartData.push(performanceEvent);
+}
+// Generate fake frustrationsList data for FRUSTRATIONS
+const frustrationsList: any[] = [];
+const frustrationEventTypes = ['CLICK', 'INPUT', 'CLICKRAGE', 'DEAD_CLICK', 'MOUSE_THRASHING'];
+for (let i = 0; i < 70; i++) {
+ const frustrationEvent = {
+ time: zoomStartTime + i * 1200 + Math.floor(Math.random() * 600),
+ type: frustrationEventTypes[Math.floor(Math.random() * frustrationEventTypes.length)],
+ hesitation: Math.floor(Math.random() * 5000) + 1000, // 1s to 6s
+ // Additional properties if needed
+ details: `Frustration event ${i}`,
+ };
+ frustrationsList.push(frustrationEvent);
+}
+
+export const resources = {
+ NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow),
+ ERRORS: exceptionsList,
+ EVENTS: eventsList,
+ PERFORMANCE: performanceChartData,
+ FRUSTRATIONS: frustrationsList,
+};