diff --git a/.changeset/selfish-rings-sleep.md b/.changeset/selfish-rings-sleep.md new file mode 100644 index 000000000..7b6f33340 --- /dev/null +++ b/.changeset/selfish-rings-sleep.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +feat: Improve Service Maps diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index 2d1d86ffd..cb0f1d8b3 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -14,7 +14,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { useHotkeys } from 'react-hotkeys-hook'; import { TSource } from '@hyperdx/common-utils/dist/types'; import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; -import { Box, Drawer, Stack } from '@mantine/core'; +import { Box, Drawer, Flex, Stack } from '@mantine/core'; import { useClickOutside } from '@mantine/hooks'; import DBRowSidePanelHeader, { @@ -28,6 +28,7 @@ import TabBar from '@/TabBar'; import { SearchConfig } from '@/types'; import { useZIndex, ZIndexContext } from '@/zIndex'; +import ServiceMapSidePanel from './ServiceMap/ServiceMapSidePanel'; import ContextSubpanel from './ContextSidePanel'; import DBInfraPanel from './DBInfraPanel'; import { RowDataPanel, useRowData } from './DBRowDataPanel'; @@ -70,6 +71,7 @@ enum Tab { Parsed = 'parsed', Debug = 'debug', Trace = 'trace', + ServiceMap = 'serviceMap', Context = 'context', Replay = 'replay', Infrastructure = 'infrastructure', @@ -221,6 +223,8 @@ const DBRowSidePanel = ({ const traceSourceId = source.kind === 'trace' ? source.id : source.traceSourceId; + const enableServiceMap = traceId && traceSourceId; + const { rumSessionId, rumServiceName } = useSessionId({ sourceId: traceSourceId, traceId, @@ -303,6 +307,14 @@ const DBRowSidePanel = ({ text: 'Trace', value: Tab.Trace, }, + ...(enableServiceMap + ? [ + { + text: 'Service Map', + value: Tab.ServiceMap, + }, + ] + : []), { text: 'Surrounding Context', value: Tab.Context, @@ -370,6 +382,26 @@ const DBRowSidePanel = ({ )} + {displayedTab === Tab.ServiceMap && enableServiceMap && ( + { + console.error(err); + }} + fallbackRender={() => ( +
+ An error occurred while rendering this event. +
+ )} + > + + + +
+ )} {displayedTab === Tab.Parsed && ( { diff --git a/packages/app/src/components/DBTracePanel.tsx b/packages/app/src/components/DBTracePanel.tsx index 7dc6243ec..4ff92c0cc 100644 --- a/packages/app/src/components/DBTracePanel.tsx +++ b/packages/app/src/components/DBTracePanel.tsx @@ -209,28 +209,6 @@ export default function DBTracePanel({ )} {traceSourceData != null && eventRowWhere != null && ( <> - - - Service Map - - - Beta - - -
- -
- Event Details diff --git a/packages/app/src/components/ServiceMap/ServiceMap.tsx b/packages/app/src/components/ServiceMap/ServiceMap.tsx index 3069ec043..df9550be9 100644 --- a/packages/app/src/components/ServiceMap/ServiceMap.tsx +++ b/packages/app/src/components/ServiceMap/ServiceMap.tsx @@ -15,6 +15,8 @@ import { NodeChange, Position, ReactFlow, + ReactFlowProvider, + useReactFlow, } from '@xyflow/react'; import useServiceMap, { ServiceAggregation } from '@/hooks/useServiceMap'; @@ -74,6 +76,7 @@ interface ServiceMapPresentationProps { error: Error | null; dateRange: [Date, Date]; source: TSource; + isSingleTrace?: boolean; } function ServiceMapPresentation({ @@ -82,9 +85,16 @@ function ServiceMapPresentation({ error, dateRange, source, + isSingleTrace, }: ServiceMapPresentationProps) { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); + const { fitView } = useReactFlow(); + + // Fit the data to the viewport whenever input service information changes + useEffect(() => { + fitView(); + }, [fitView, services]); const onNodesChange = useCallback( (changes: NodeChange[]) => @@ -115,6 +125,7 @@ function ServiceMapPresentation({ dateRange, source, maxErrorPercentage, + isSingleTrace, }, position: { x: index * 150, y: 100 }, type: 'service', @@ -143,6 +154,7 @@ function ServiceMapPresentation({ source, dateRange, serviceName, + isSingleTrace, }, }; }, @@ -153,16 +165,27 @@ function ServiceMapPresentation({ setNodes(nodeWithLayout); setEdges(edges); - }, [services, dateRange, source, maxErrorPercentage]); + }, [services, dateRange, source, maxErrorPercentage, isSingleTrace]); if (isLoading) { return ( -
+
); } + if (services && services.size === 0) { + return ( +
+ + No services found. The Service Map shows links between services with + related Client- and Server-kind spans. + +
+ ); + } + if (error) { return ( @@ -222,6 +245,7 @@ interface ServiceMapProps { traceTableSource: TSource; dateRange: [Date, Date]; samplingFactor?: number; + isSingleTrace?: boolean; } export default function ServiceMap({ @@ -229,6 +253,7 @@ export default function ServiceMap({ traceTableSource, dateRange, samplingFactor = 1, + isSingleTrace, }: ServiceMapProps) { const { isLoading, @@ -252,12 +277,15 @@ export default function ServiceMap({ }, [error]); return ( - + + + ); } diff --git a/packages/app/src/components/ServiceMap/ServiceMapEdge.tsx b/packages/app/src/components/ServiceMap/ServiceMapEdge.tsx index 806a6b50d..a2f826715 100644 --- a/packages/app/src/components/ServiceMap/ServiceMapEdge.tsx +++ b/packages/app/src/components/ServiceMap/ServiceMapEdge.tsx @@ -15,6 +15,7 @@ export type ServiceMapEdgeData = { dateRange: [Date, Date]; source: TSource; serviceName: string; + isSingleTrace?: boolean; }; export default function ServiceMapEdge( @@ -26,8 +27,14 @@ export default function ServiceMapEdge( return null; } - const { totalRequests, errorPercentage, dateRange, serviceName, source } = - props.data; + const { + totalRequests, + errorPercentage, + dateRange, + serviceName, + source, + isSingleTrace, + } = props.data; return ( <> @@ -44,6 +51,7 @@ export default function ServiceMapEdge( source={source} dateRange={dateRange} serviceName={serviceName} + isSingleTrace={isSingleTrace} /> diff --git a/packages/app/src/components/ServiceMap/ServiceMapNode.tsx b/packages/app/src/components/ServiceMap/ServiceMapNode.tsx index 0838a81da..d0233337b 100644 --- a/packages/app/src/components/ServiceMap/ServiceMapNode.tsx +++ b/packages/app/src/components/ServiceMap/ServiceMapNode.tsx @@ -13,6 +13,7 @@ export type ServiceMapNodeData = ServiceAggregation & { dateRange: [Date, Date]; source: TSource; maxErrorPercentage: number; + isSingleTrace?: boolean; }; export default function ServiceMapNode( @@ -28,6 +29,7 @@ export default function ServiceMapNode( source, dateRange, maxErrorPercentage, + isSingleTrace, } = data; const { backgroundColor, borderColor } = getNodeColors( @@ -45,6 +47,7 @@ export default function ServiceMapNode( source={source} dateRange={dateRange} serviceName={serviceName} + isSingleTrace={isSingleTrace} />
diff --git a/packages/app/src/components/ServiceMap/ServiceMapSidePanel.tsx b/packages/app/src/components/ServiceMap/ServiceMapSidePanel.tsx new file mode 100644 index 000000000..dfe192719 --- /dev/null +++ b/packages/app/src/components/ServiceMap/ServiceMapSidePanel.tsx @@ -0,0 +1,47 @@ +import { Badge, Group, Stack, Text } from '@mantine/core'; + +import { useSource } from '@/source'; + +import ServiceMap from './ServiceMap'; + +interface ServiceMapSidePanelProps { + traceId: string; + dateRange: [Date, Date]; + traceTableSourceId: string; +} + +export default function ServiceMapSidePanel({ + traceId, + dateRange, + traceTableSourceId, +}: ServiceMapSidePanelProps) { + const { data: traceTableSource } = useSource({ id: traceTableSourceId }); + + return ( + + + + Service Map + + + Beta + + + {traceTableSource ? ( + + ) : null} + + ); +} diff --git a/packages/app/src/components/ServiceMap/ServiceMapTooltip.tsx b/packages/app/src/components/ServiceMap/ServiceMapTooltip.tsx index 7bb59cac2..c1753852c 100644 --- a/packages/app/src/components/ServiceMap/ServiceMapTooltip.tsx +++ b/packages/app/src/components/ServiceMap/ServiceMapTooltip.tsx @@ -12,12 +12,14 @@ export default function ServiceMapTooltip({ source, dateRange, serviceName, + isSingleTrace, }: { totalRequests: number; errorPercentage: number; source: TSource; dateRange: [Date, Date]; serviceName: string; + isSingleTrace?: boolean; }) { return (
@@ -35,7 +37,8 @@ export default function ServiceMapTooltip({ } className={styles.linkButton} > - {formatApproximateNumber(totalRequests)} request + {isSingleTrace ? totalRequests : formatApproximateNumber(totalRequests)}{' '} + request {totalRequests !== 1 ? 's' : ''} {errorPercentage > 0 ? (