Skip to content

Commit a7e150c

Browse files
authored
feat: Improve Service Maps (#1353)
# Summary This PR makes a number of minor fixes and improvements to the Service Maps feature: 1. The Service Map now has its own tab in the side panel. This resolves usability issues such as the trace panel capturing scroll events and appearing too large on the side panel. Closes HDX-2785, Closes HDX-2732. 2. On single-trace service maps (eg. the one on the side panel), request counts are now rendered as exact numbers (eg. `1 request`), rather than approximate numbers (eg. `~1 request`). Closes HDX-2741. 3. Service map viewport bounds are now reset when the input data changes (typically when the source or sampling level changes). Closes HDX-2778. 4. Service maps now have an empty state. Closes HDX-2739. <img width="1359" height="902" alt="Screenshot 2025-11-11 at 11 00 05 PM" src="https://github.com/user-attachments/assets/6d8c7fda-bf4e-4dbe-83e4-6395f53511cb" /> <img width="1365" height="910" alt="Screenshot 2025-11-11 at 11 05 13 PM" src="https://github.com/user-attachments/assets/af5218f9-43f8-4536-abee-5ce090cf0438" />
1 parent 5e440ab commit a7e150c

File tree

8 files changed

+139
-35
lines changed

8 files changed

+139
-35
lines changed

.changeset/selfish-rings-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: Improve Service Maps

packages/app/src/components/DBRowSidePanel.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { ErrorBoundary } from 'react-error-boundary';
1414
import { useHotkeys } from 'react-hotkeys-hook';
1515
import { TSource } from '@hyperdx/common-utils/dist/types';
1616
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
17-
import { Box, Drawer, Stack } from '@mantine/core';
17+
import { Box, Drawer, Flex, Stack } from '@mantine/core';
1818
import { useClickOutside } from '@mantine/hooks';
1919

2020
import DBRowSidePanelHeader, {
@@ -28,6 +28,7 @@ import TabBar from '@/TabBar';
2828
import { SearchConfig } from '@/types';
2929
import { useZIndex, ZIndexContext } from '@/zIndex';
3030

31+
import ServiceMapSidePanel from './ServiceMap/ServiceMapSidePanel';
3132
import ContextSubpanel from './ContextSidePanel';
3233
import DBInfraPanel from './DBInfraPanel';
3334
import { RowDataPanel, useRowData } from './DBRowDataPanel';
@@ -70,6 +71,7 @@ enum Tab {
7071
Parsed = 'parsed',
7172
Debug = 'debug',
7273
Trace = 'trace',
74+
ServiceMap = 'serviceMap',
7375
Context = 'context',
7476
Replay = 'replay',
7577
Infrastructure = 'infrastructure',
@@ -221,6 +223,8 @@ const DBRowSidePanel = ({
221223
const traceSourceId =
222224
source.kind === 'trace' ? source.id : source.traceSourceId;
223225

226+
const enableServiceMap = traceId && traceSourceId;
227+
224228
const { rumSessionId, rumServiceName } = useSessionId({
225229
sourceId: traceSourceId,
226230
traceId,
@@ -303,6 +307,14 @@ const DBRowSidePanel = ({
303307
text: 'Trace',
304308
value: Tab.Trace,
305309
},
310+
...(enableServiceMap
311+
? [
312+
{
313+
text: 'Service Map',
314+
value: Tab.ServiceMap,
315+
},
316+
]
317+
: []),
306318
{
307319
text: 'Surrounding Context',
308320
value: Tab.Context,
@@ -370,6 +382,26 @@ const DBRowSidePanel = ({
370382
</Box>
371383
</ErrorBoundary>
372384
)}
385+
{displayedTab === Tab.ServiceMap && enableServiceMap && (
386+
<ErrorBoundary
387+
onError={err => {
388+
console.error(err);
389+
}}
390+
fallbackRender={() => (
391+
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
392+
An error occurred while rendering this event.
393+
</div>
394+
)}
395+
>
396+
<Flex p="sm" flex={1}>
397+
<ServiceMapSidePanel
398+
traceId={traceId}
399+
traceTableSourceId={traceSourceId}
400+
dateRange={oneHourRange}
401+
/>
402+
</Flex>
403+
</ErrorBoundary>
404+
)}
373405
{displayedTab === Tab.Parsed && (
374406
<ErrorBoundary
375407
onError={err => {

packages/app/src/components/DBTracePanel.tsx

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -209,28 +209,6 @@ export default function DBTracePanel({
209209
)}
210210
{traceSourceData != null && eventRowWhere != null && (
211211
<>
212-
<Group>
213-
<Text size="sm" c="dark.2" my="sm">
214-
Service Map
215-
</Text>
216-
<Badge
217-
size="xs"
218-
color="gray.4"
219-
autoContrast
220-
radius="sm"
221-
className="align-text-bottom"
222-
>
223-
Beta
224-
</Badge>
225-
</Group>
226-
<div style={{ height: '300px', width: '100%', display: 'flex' }}>
227-
<ServiceMap
228-
traceId={traceId}
229-
traceTableSource={traceSourceData}
230-
dateRange={dateRange}
231-
/>
232-
</div>
233-
<Divider my="md" />
234212
<Text size="sm" c="dark.2" my="sm">
235213
Event Details
236214
</Text>

packages/app/src/components/ServiceMap/ServiceMap.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
NodeChange,
1616
Position,
1717
ReactFlow,
18+
ReactFlowProvider,
19+
useReactFlow,
1820
} from '@xyflow/react';
1921

2022
import useServiceMap, { ServiceAggregation } from '@/hooks/useServiceMap';
@@ -74,6 +76,7 @@ interface ServiceMapPresentationProps {
7476
error: Error | null;
7577
dateRange: [Date, Date];
7678
source: TSource;
79+
isSingleTrace?: boolean;
7780
}
7881

7982
function ServiceMapPresentation({
@@ -82,9 +85,16 @@ function ServiceMapPresentation({
8285
error,
8386
dateRange,
8487
source,
88+
isSingleTrace,
8589
}: ServiceMapPresentationProps) {
8690
const [nodes, setNodes] = useState<Node[]>([]);
8791
const [edges, setEdges] = useState<Edge[]>([]);
92+
const { fitView } = useReactFlow();
93+
94+
// Fit the data to the viewport whenever input service information changes
95+
useEffect(() => {
96+
fitView();
97+
}, [fitView, services]);
8898

8999
const onNodesChange = useCallback(
90100
(changes: NodeChange<Node>[]) =>
@@ -115,6 +125,7 @@ function ServiceMapPresentation({
115125
dateRange,
116126
source,
117127
maxErrorPercentage,
128+
isSingleTrace,
118129
},
119130
position: { x: index * 150, y: 100 },
120131
type: 'service',
@@ -143,6 +154,7 @@ function ServiceMapPresentation({
143154
source,
144155
dateRange,
145156
serviceName,
157+
isSingleTrace,
146158
},
147159
};
148160
},
@@ -153,16 +165,27 @@ function ServiceMapPresentation({
153165

154166
setNodes(nodeWithLayout);
155167
setEdges(edges);
156-
}, [services, dateRange, source, maxErrorPercentage]);
168+
}, [services, dateRange, source, maxErrorPercentage, isSingleTrace]);
157169

158170
if (isLoading) {
159171
return (
160-
<Center className={`${styles.graphContainer} h-100`}>
172+
<Center className={`${styles.graphContainer} h-100 w-100`}>
161173
<Loader size="lg" />
162174
</Center>
163175
);
164176
}
165177

178+
if (services && services.size === 0) {
179+
return (
180+
<Center className="w-100 h-100">
181+
<Text size="sm" c="gray.5">
182+
No services found. The Service Map shows links between services with
183+
related Client- and Server-kind spans.
184+
</Text>
185+
</Center>
186+
);
187+
}
188+
166189
if (error) {
167190
return (
168191
<Box>
@@ -222,13 +245,15 @@ interface ServiceMapProps {
222245
traceTableSource: TSource;
223246
dateRange: [Date, Date];
224247
samplingFactor?: number;
248+
isSingleTrace?: boolean;
225249
}
226250

227251
export default function ServiceMap({
228252
traceId,
229253
traceTableSource,
230254
dateRange,
231255
samplingFactor = 1,
256+
isSingleTrace,
232257
}: ServiceMapProps) {
233258
const {
234259
isLoading,
@@ -252,12 +277,15 @@ export default function ServiceMap({
252277
}, [error]);
253278

254279
return (
255-
<ServiceMapPresentation
256-
services={services}
257-
isLoading={isLoading}
258-
error={error}
259-
dateRange={dateRange}
260-
source={traceTableSource}
261-
/>
280+
<ReactFlowProvider>
281+
<ServiceMapPresentation
282+
services={services}
283+
isLoading={isLoading}
284+
error={error}
285+
dateRange={dateRange}
286+
source={traceTableSource}
287+
isSingleTrace={isSingleTrace}
288+
/>
289+
</ReactFlowProvider>
262290
);
263291
}

packages/app/src/components/ServiceMap/ServiceMapEdge.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type ServiceMapEdgeData = {
1515
dateRange: [Date, Date];
1616
source: TSource;
1717
serviceName: string;
18+
isSingleTrace?: boolean;
1819
};
1920

2021
export default function ServiceMapEdge(
@@ -26,8 +27,14 @@ export default function ServiceMapEdge(
2627
return null;
2728
}
2829

29-
const { totalRequests, errorPercentage, dateRange, serviceName, source } =
30-
props.data;
30+
const {
31+
totalRequests,
32+
errorPercentage,
33+
dateRange,
34+
serviceName,
35+
source,
36+
isSingleTrace,
37+
} = props.data;
3138

3239
return (
3340
<>
@@ -44,6 +51,7 @@ export default function ServiceMapEdge(
4451
source={source}
4552
dateRange={dateRange}
4653
serviceName={serviceName}
54+
isSingleTrace={isSingleTrace}
4755
/>
4856
</EdgeToolbar>
4957
</>

packages/app/src/components/ServiceMap/ServiceMapNode.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type ServiceMapNodeData = ServiceAggregation & {
1313
dateRange: [Date, Date];
1414
source: TSource;
1515
maxErrorPercentage: number;
16+
isSingleTrace?: boolean;
1617
};
1718

1819
export default function ServiceMapNode(
@@ -28,6 +29,7 @@ export default function ServiceMapNode(
2829
source,
2930
dateRange,
3031
maxErrorPercentage,
32+
isSingleTrace,
3133
} = data;
3234

3335
const { backgroundColor, borderColor } = getNodeColors(
@@ -45,6 +47,7 @@ export default function ServiceMapNode(
4547
source={source}
4648
dateRange={dateRange}
4749
serviceName={serviceName}
50+
isSingleTrace={isSingleTrace}
4851
/>
4952
</NodeToolbar>
5053
<div className={`${styles.serviceNode}`}>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Badge, Group, Stack, Text } from '@mantine/core';
2+
3+
import { useSource } from '@/source';
4+
5+
import ServiceMap from './ServiceMap';
6+
7+
interface ServiceMapSidePanelProps {
8+
traceId: string;
9+
dateRange: [Date, Date];
10+
traceTableSourceId: string;
11+
}
12+
13+
export default function ServiceMapSidePanel({
14+
traceId,
15+
dateRange,
16+
traceTableSourceId,
17+
}: ServiceMapSidePanelProps) {
18+
const { data: traceTableSource } = useSource({ id: traceTableSourceId });
19+
20+
return (
21+
<Stack w="100%">
22+
<Group gap={0}>
23+
<Text size="sm" c="gray.2" ps="sm">
24+
Service Map
25+
</Text>
26+
<Badge
27+
size="xs"
28+
ms="xs"
29+
color="gray.4"
30+
autoContrast
31+
radius="sm"
32+
className="align-text-bottom"
33+
>
34+
Beta
35+
</Badge>
36+
</Group>
37+
{traceTableSource ? (
38+
<ServiceMap
39+
traceTableSource={traceTableSource}
40+
traceId={traceId}
41+
dateRange={dateRange}
42+
isSingleTrace
43+
/>
44+
) : null}
45+
</Stack>
46+
);
47+
}

packages/app/src/components/ServiceMap/ServiceMapTooltip.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ export default function ServiceMapTooltip({
1212
source,
1313
dateRange,
1414
serviceName,
15+
isSingleTrace,
1516
}: {
1617
totalRequests: number;
1718
errorPercentage: number;
1819
source: TSource;
1920
dateRange: [Date, Date];
2021
serviceName: string;
22+
isSingleTrace?: boolean;
2123
}) {
2224
return (
2325
<div className={styles.toolbar}>
@@ -35,7 +37,8 @@ export default function ServiceMapTooltip({
3537
}
3638
className={styles.linkButton}
3739
>
38-
{formatApproximateNumber(totalRequests)} request
40+
{isSingleTrace ? totalRequests : formatApproximateNumber(totalRequests)}{' '}
41+
request
3942
{totalRequests !== 1 ? 's' : ''}
4043
</UnstyledButton>
4144
{errorPercentage > 0 ? (

0 commit comments

Comments
 (0)