diff --git a/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx b/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx index d24344456..35e0e0af4 100644 --- a/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx +++ b/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx @@ -21,30 +21,37 @@ function CardIssues() { const isMounted = useIsMounted(); const { showModal } = useModal(); - const fetchIssues = (filter: any) => { + const fetchIssues = async (filter: any) => { if (!isMounted()) return; + setLoading(true); + const mapSeries = (item: any) => { + const filters = item.filter.filters + .map((f: any) => f.toJson()); + + return { + ...item, + filter: { + ...item.filter, + filters + } + }; + }; + const newFilter = { ...filter, - series: filter.series.map((item: any) => { - return { - ...item, - filter: { - ...item.filter, - filters: item.filter.filters.filter((filter: any, index: any) => { - const stage = widget.data.funnel.stages[index]; - return stage && stage.isActive; - }).map((f: any) => f.toJson()) - } - }; - }) + series: filter.series.map(mapSeries) }; - widget.fetchIssues(newFilter).then((res: any) => { + + try { + const res = await widget.fetchIssues(newFilter); setData(res); - }).finally(() => { + } catch (error) { + console.error('Error fetching issues:', error); + } finally { setLoading(false); - }); + } }; const handleClick = (issue: any) => { @@ -77,7 +84,7 @@ function CardIssues() { - + {data.issues.map((item: any, index: any) => (
handleClick(item)} key={index}> diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index 8e02bcfa0..13f89586d 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -37,7 +37,7 @@ function MetricTypeDropdown(props: Props) { } setTimeout(() => onChange(type.value), 0); } - setTimeout(() => onChange(USER_PATH), 0); + // setTimeout(() => onChange(USER_PATH), 0); }, []); const onChange = (type: string) => { diff --git a/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx b/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx index 398fec2e4..651a496f4 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Layer } from 'recharts'; +import { Layer, Rectangle } from 'recharts'; function CustomLink(props: any) { const [fill, setFill] = React.useState('url(#linkGradient)'); @@ -11,12 +11,17 @@ function CustomLink(props: any) { const onClick = () => { if (props.onClick) { - props.onClick(props); + props.onClick(props.payload); } }; return ( - + { setFill('rgba(57, 78, 255, 0.5)'); }} diff --git a/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx b/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx index d8382c902..e6d122971 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/NodeButton.tsx @@ -9,7 +9,6 @@ interface Props { function NodeButton(props: Props) { const { payload } = props; const [show, setShow] = React.useState(false); - console.log('payload', payload, props) const toggleMenu = (e: React.MouseEvent) => { setShow(!show); diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx index 6e07ab7c8..5c0fc3ec4 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx @@ -1,10 +1,11 @@ -import React, { useEffect } from 'react'; +import React, { useState } from 'react'; import { Sankey, ResponsiveContainer } from 'recharts'; import CustomLink from './CustomLink'; import CustomNode from './CustomNode'; import { NoContent } from 'UI'; interface Node { + id: number; // Assuming you missed this from your interface name: string; eventType: string; avgTimeFromPrevious: number | null; @@ -24,60 +25,69 @@ interface Data { interface Props { data: Data; - nodePadding?: number; nodeWidth?: number; - onChartClick?: (data: any) => void; height?: number; } +const SankeyChart: React.FC = ({ + data, + height = 240 + }: Props) => { + const [highlightedLinks, setHighlightedLinks] = useState([]); -function SankeyChart(props: Props) { - const { data, nodeWidth = 10, height = 240 } = props; - const [activeLink, setActiveLink] = React.useState(null); + const handleLinkMouseEnter = (linkData: any) => { + const { payload } = linkData; + const fullPathArray: Node[] = []; - useEffect(() => { - if (!activeLink) return; - const { source, target } = activeLink.payload; - const filters = []; - if (source) { - filters.push({ - operator: 'is', - type: source.eventType, - value: [source.name], - isEvent: true - }); + console.log('linkData', linkData.index); + + // Add the source node of the current link + fullPathArray.push(payload.source); + fullPathArray.push(payload.target); + + + if (payload.source.sourceLinks.length > 0) { + let prevLink = data.links[payload.source.sourceLinks[0]]; + // fullPathArray.unshift(prevLink); + fullPathArray.unshift(data.nodes[prevLink.source]); + + if (prevLink.source) { + let prevLinkPrev = data.links[prevLink.source]; + // fullPathArray.unshift(prevLinkPrev); + fullPathArray.unshift(data.nodes[prevLinkPrev.source]); + } } - if (target) { - filters.push({ - operator: 'is', - type: target.eventType, - value: [target.name], - isEvent: true - }); - } + setHighlightedLinks(fullPathArray); + + console.log('fullPathArray', fullPathArray.map(node => node)); + }; - props.onChartClick?.(filters); - }, [activeLink]); return ( + show={!data.nodes.length || !data.links.length} + title={'No data for the selected time period.'} + > } - nodeWidth={nodeWidth} - sort={false} - margin={{ - left: 0, - right: 200, - top: 0, - bottom: 10 + sort={true} + onClick={(data) => { + }} - link={ setActiveLink(props)} activeLink={activeLink} />} + link={({ source, target, ...linkProps }, index) => ( + handleLinkMouseEnter(linkProps)} + onMouseLeave={() => setHighlightedLinks([])} + /> + )} + margin={{ right: 200 }} > @@ -89,6 +99,6 @@ function SankeyChart(props: Props) { ); -} +}; export default SankeyChart; diff --git a/frontend/app/constants/filterOptions.js b/frontend/app/constants/filterOptions.js index 139f96f7f..d2150f5d1 100644 --- a/frontend/app/constants/filterOptions.js +++ b/frontend/app/constants/filterOptions.js @@ -28,7 +28,7 @@ export const options = [ { key: 'not equal', label: 'not equal', value: 'not equal' }, { key: 'onSelector', label: 'on selector', value: 'onSelector' }, { key: 'onText', label: 'on text', value: 'onText' }, - { key: 'onComponent', label: 'on component', value: 'onComponent' }, + { key: 'onComponent', label: 'on component', value: 'onComponent' } ]; const filterKeys = ['is', 'isNot']; @@ -38,47 +38,47 @@ const stringFilterKeysPerformance = ['is', 'inAnyPage', 'isNot', 'contains', 'st const targetFilterKeys = ['on', 'notOn', 'onAny', 'contains', 'startsWith', 'endsWith', 'notContains']; const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; -const pageUrlFilter = ['contains', 'startsWith', 'endsWith'] +const pageUrlFilter = ['contains', 'startsWith', 'endsWith']; const getOperatorsByKeys = (keys) => { return options.filter(option => keys.includes(option.key)); }; -export const baseOperators = options.filter(({key}) => filterKeys.includes(key)); -export const stringOperatorsLimited = options.filter(({key}) => stringFilterKeysLimited.includes(key)); -export const stringOperators = options.filter(({key}) => stringFilterKeys.includes(key)); -export const stringOperatorsPerformance = options.filter(({key}) => stringFilterKeysPerformance.includes(key)); -export const targetOperators = options.filter(({key}) => targetFilterKeys.includes(key)); +export const baseOperators = options.filter(({ key }) => filterKeys.includes(key)); +export const stringOperatorsLimited = options.filter(({ key }) => stringFilterKeysLimited.includes(key)); +export const stringOperators = options.filter(({ key }) => stringFilterKeys.includes(key)); +export const stringOperatorsPerformance = options.filter(({ key }) => stringFilterKeysPerformance.includes(key)); +export const targetOperators = options.filter(({ key }) => targetFilterKeys.includes(key)); export const booleanOperators = [ { key: 'true', label: 'true', value: 'true' }, - { key: 'false', label: 'false', value: 'false' }, -] -export const pageUrlOperators = options.filter(({key}) => pageUrlFilter.includes(key)) + { key: 'false', label: 'false', value: 'false' } +]; +export const pageUrlOperators = options.filter(({ key }) => pageUrlFilter.includes(key)); export const customOperators = [ { key: '=', label: '=', value: '=' }, { key: '<', label: '<', value: '<' }, { key: '>', label: '>', value: '>' }, { key: '<=', label: '<=', value: '<=' }, - { key: '>=', label: '>=', value: '>=' }, -] + { key: '>=', label: '>=', value: '>=' } +]; export const metricTypes = [ { label: 'Timeseries', value: 'timeseries' }, { label: 'Table', value: 'table' }, - { label: 'Funnel', value: 'funnel' }, + { label: 'Funnel', value: 'funnel' } // { label: 'Errors', value: 'errors' }, // { label: 'Sessions', value: 'sessions' }, ]; export const tableColumnName = { - [FilterKey.USERID]: 'Users', - [FilterKey.ISSUE]: 'Issues', - [FilterKey.USER_BROWSER]: 'Browser', - [FilterKey.USER_DEVICE]: 'Devices', - [FilterKey.USER_COUNTRY]: 'Countries', - [FilterKey.LOCATION]: 'URLs', -} + [FilterKey.USERID]: 'Users', + [FilterKey.ISSUE]: 'Issues', + [FilterKey.USER_BROWSER]: 'Browser', + [FilterKey.USER_DEVICE]: 'Devices', + [FilterKey.USER_COUNTRY]: 'Countries', + [FilterKey.LOCATION]: 'URLs' +}; export const metricOf = [ { label: 'Session Count', value: 'sessionCount', type: 'timeseries' }, @@ -89,9 +89,9 @@ export const metricOf = [ { label: 'Browser', value: FilterKey.USER_BROWSER, type: 'table' }, { label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' }, { label: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' }, - { label: 'URLs', value: FilterKey.LOCATION, type: 'table' }, + { label: 'URLs', value: FilterKey.LOCATION, type: 'table' } -] +]; export const methodOptions = [ { label: 'GET', value: 'GET' }, @@ -102,8 +102,8 @@ export const methodOptions = [ { label: 'HEAD', value: 'HEAD' }, { label: 'OPTIONS', value: 'OPTIONS' }, { label: 'TRACE', value: 'TRACE' }, - { label: 'CONNECT', value: 'CONNECT' }, -] + { label: 'CONNECT', value: 'CONNECT' } +]; export const issueOptions = [ { label: 'Click Rage', value: IssueType.CLICK_RAGE }, @@ -119,19 +119,26 @@ export const issueOptions = [ { label: 'Custom', value: IssueType.CUSTOM }, { label: 'Error', value: IssueType.JS_EXCEPTION }, { label: 'Mouse Thrashing', value: IssueType.MOUSE_THRASHING } -] +]; export const issueCategories = [ { label: 'Resources', value: IssueCategory.RESOURCES }, { label: 'Network Request', value: IssueCategory.NETWORK }, { label: 'Click Rage', value: IssueCategory.RAGE }, - { label: 'JS Errors', value: IssueCategory.ERRORS }, -] + { label: 'JS Errors', value: IssueCategory.ERRORS } +]; -export const issueCategoriesMap = issueCategories.reduce((acc, {value, label}) => { +export const pathAnalysisEvents = [ + { value: FilterKey.LOCATION, label: 'Pages' }, + { value: FilterKey.CLICK, label: 'Clicks' }, + { value: FilterKey.INPUT, label: 'Input' }, + { value: FilterKey.CUSTOM, label: 'Custom' } +]; + +export const issueCategoriesMap = issueCategories.reduce((acc, { value, label }) => { acc[value] = label; return acc; -}, {}) +}, {}); export default { options, @@ -148,5 +155,5 @@ export default { issueOptions, issueCategories, methodOptions, - pageUrlOperators, -} + pageUrlOperators +}; diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index ad714384d..623fc971f 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -91,6 +91,7 @@ export default class FilterItem { this.completed = json.completed; this.dropped = json.dropped; + return this; } diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 710847743..4f9bc078a 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -3,7 +3,7 @@ import FilterSeries from './filterSeries'; import { DateTime } from 'luxon'; import Session from 'App/mstore/types/session'; import Funnelissue from 'App/mstore/types/funnelIssue'; -import { issueOptions, issueCategories, issueCategoriesMap } from 'App/constants/filterOptions'; +import { issueOptions, issueCategories, issueCategoriesMap, pathAnalysisEvents } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import Period, { LAST_24_HOURS } from 'Types/app/period'; import Funnel from '../types/funnel'; @@ -69,7 +69,7 @@ export default class Widget { name: string = 'Untitled Card'; metricType: string = 'timeseries'; metricOf: string = 'sessionCount'; - metricValue: string = ''; + metricValue: any = ''; viewType: string = 'lineChart'; metricFormat: string = 'sessionCount'; series: FilterSeries[] = []; @@ -85,7 +85,6 @@ export default class Widget { thumbnail?: string; params: any = { density: 70 }; startType: string = 'start'; - // startPoint: FilterItem = filtersMap[FilterKey.LOCATION]; startPoint: FilterItem = new FilterItem(filtersMap[FilterKey.LOCATION]); excludes: FilterItem[] = []; hideExcess?: boolean = false; @@ -162,6 +161,25 @@ export default class Widget { this.series[0].filter.eventsOrderSupport = ['then']; } + if (this.metricType === USER_PATH) { + this.hideExcess = json.hideExcess; + this.startType = json.startType; + if (json.startPoint) { + if (Array.isArray(json.startPoint) && json.startPoint.length > 0) { + this.startPoint = new FilterItem().fromJson(json.startPoint[0]); + } + + if (json.startPoint == typeof Object) { + this.startPoint = json.startPoint; + } + } + + // TODO change this to excludes after the api change + if (json.exclude) { + this.series[0].filter.excludes = json.exclude.map((i: any) => new FilterItem().fromJson(i)); + } + } + if (period) { this.period = period; } @@ -238,14 +256,17 @@ export default class Widget { return this.metricId !== undefined; } + setData(data: any, period: any) { const _data: any = { ...data }; if (this.metricType === USER_PATH) { _data['links'] = data.links.map((s: any) => ({ - ...s, + ...s // value: Math.round(s.value), })); + + Object.assign(this.data, _data); return _data; } if (this.metricOf === FilterKey.ERRORS) { @@ -311,34 +332,33 @@ export default class Widget { } - fetchIssues(card: any): Promise { - return new Promise((resolve) => { - metricService.fetchIssues(card) - .then((response: any) => { - if (card.metricType === USER_PATH) { - resolve({ - total: response.count, - issues: response.values.map((issue: any) => new Issue().fromJSON(issue)) - }); - } else { - const significantIssues = response.issues.significant - ? response.issues.significant.map((issue: any) => new Funnelissue().fromJSON(issue)) - : []; - const insignificantIssues = response.issues.insignificant - ? response.issues.insignificant.map((issue: any) => new Funnelissue().fromJSON(issue)) - : []; - resolve({ - issues: significantIssues.length > 0 ? significantIssues : insignificantIssues - }); - } - }).finally(() => { - resolve({ - issues: [] - }); - }); - }); + async fetchIssues(card: any): Promise { + try { + const response = await metricService.fetchIssues(card); + + if (card.metricType === USER_PATH) { + return { + total: response.count, + issues: response.values.map((issue: any) => new Issue().fromJSON(issue)) + }; + } else { + const mapIssue = (issue: any) => new Funnelissue().fromJSON(issue); + const significantIssues = response.issues.significant?.map(mapIssue) || []; + const insignificantIssues = response.issues.insignificant?.map(mapIssue) || []; + + return { + issues: significantIssues.length > 0 ? significantIssues : insignificantIssues + }; + } + } catch (error) { + console.error('Error fetching issues:', error); + return { + issues: [] + }; + } } + fetchIssue(funnelId: any, issueId: any, params: any): Promise { return new Promise((resolve, reject) => { metricService @@ -361,6 +381,8 @@ export default class Widget { return issueOptions.filter((i: any) => metricValue.includes(i.value)); } else if (metricType === INSIGHTS) { return issueCategories.filter((i: any) => metricValue.includes(i.value)); + } else if (metricType === USER_PATH) { + return pathAnalysisEvents.filter((i: any) => metricValue.includes(i.value)); } } diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index 2ab32a9eb..28da47249 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -82,7 +82,7 @@ export default class MetricService { } const path = isWidget ? `/cards/${metric.metricId}/chart` : `/cards/try`; if (metric.metricType === USER_PATH) { - data.density = 4; + data.density = 5; data.metricOf = 'sessionCount'; } try {