Skip to content

Commit f5828d1

Browse files
alex-fedotyevAlex Fedotyevclaudeteeohhem
authored
feat: field filtering and priority classification (#1835)
Co-authored-by: Alex Fedotyev <alex@Alexs-CH-Mac.localdomain> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Alexander <teeohhem@gmail.com>
1 parent c5173ba commit f5828d1

4 files changed

Lines changed: 655 additions & 60 deletions

File tree

‎.changeset/bright-queens-shop.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: field filtering and priority classification for Event Deltas

‎packages/app/src/components/DBDeltaChart.tsx‎

Lines changed: 170 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import {
55
ChartConfigWithOptDateRange,
66
Filter,
77
} from '@hyperdx/common-utils/dist/types';
8-
import { Box, Code, Container, Flex, Pagination, Text } from '@mantine/core';
8+
import {
9+
Box,
10+
Code,
11+
Container,
12+
Divider,
13+
Flex,
14+
Pagination,
15+
Text,
16+
} from '@mantine/core';
917
import { useElementSize } from '@mantine/hooks';
1018

1119
import { isAggregateFunction } from '@/ChartUtils';
@@ -15,6 +23,8 @@ import { getFirstTimestampValueExpression } from '@/source';
1523
import { SQLPreview } from './ChartSQLPreview';
1624
import {
1725
getPropertyStatistics,
26+
isDenylisted,
27+
isHighCardinality,
1828
mergeValueStatisticsMaps,
1929
} from './deltaChartUtils';
2030
import {
@@ -194,52 +204,87 @@ export default function DBDeltaChart({
194204
limit: { limit: 1000 },
195205
});
196206

207+
// Column metadata for field classification (from ClickHouse response)
208+
const columnMeta = useMemo<{ name: string; type: string }[]>(
209+
() => outlierData?.meta ?? inlierData?.meta ?? [],
210+
[outlierData?.meta, inlierData?.meta],
211+
);
212+
197213
// TODO: Is loading state
198-
const { sortedProperties, outlierValueOccurences, inlierValueOccurences } =
199-
useMemo(() => {
200-
const { percentageOccurences: outlierValueOccurences } =
201-
getPropertyStatistics(outlierData?.data ?? []);
202-
203-
const { percentageOccurences: inlierValueOccurences } =
204-
getPropertyStatistics(inlierData?.data ?? []);
205-
206-
// Get all the unique keys from the outliers
207-
let uniqueKeys = new Set([...outlierValueOccurences.keys()]);
208-
// If there's no outliers, use inliers as the unique keys
209-
if (uniqueKeys.size === 0) {
210-
uniqueKeys = new Set([...inlierValueOccurences.keys()]);
214+
const {
215+
visibleProperties,
216+
hiddenProperties,
217+
outlierValueOccurences,
218+
inlierValueOccurences,
219+
} = useMemo(() => {
220+
const {
221+
percentageOccurences: outlierValueOccurences,
222+
propertyOccurences: outlierPropertyOccurences,
223+
valueOccurences: outlierRawValueOccurences,
224+
} = getPropertyStatistics(outlierData?.data ?? []);
225+
226+
const {
227+
percentageOccurences: inlierValueOccurences,
228+
propertyOccurences: inlierPropertyOccurences,
229+
valueOccurences: inlierRawValueOccurences,
230+
} = getPropertyStatistics(inlierData?.data ?? []);
231+
232+
// Get all the unique keys from the outliers
233+
let uniqueKeys = new Set([...outlierValueOccurences.keys()]);
234+
// If there's no outliers, use inliers as the unique keys
235+
if (uniqueKeys.size === 0) {
236+
uniqueKeys = new Set([...inlierValueOccurences.keys()]);
237+
}
238+
// Now process the keys to find the ones with the highest delta between outlier and inlier percentages
239+
const sortedProperties = Array.from(uniqueKeys)
240+
.map(key => {
241+
const inlierCount =
242+
inlierValueOccurences.get(key) ?? new Map<string, number>();
243+
const outlierCount =
244+
outlierValueOccurences.get(key) ?? new Map<string, number>();
245+
246+
const mergedArray = mergeValueStatisticsMaps(outlierCount, inlierCount);
247+
let maxValueDelta = 0;
248+
mergedArray.forEach(item => {
249+
const delta = Math.abs(item.outlierCount - item.inlierCount);
250+
if (delta > maxValueDelta) {
251+
maxValueDelta = delta;
252+
}
253+
});
254+
255+
return [key, maxValueDelta] as const;
256+
})
257+
.sort((a, b) => b[1] - a[1])
258+
.map(a => a[0]);
259+
260+
// Split into visible (primary) and hidden (denylisted + high cardinality)
261+
const visibleProperties: string[] = [];
262+
const hiddenProperties: string[] = [];
263+
sortedProperties.forEach(key => {
264+
if (isDenylisted(key, columnMeta)) {
265+
hiddenProperties.push(key);
266+
} else if (
267+
isHighCardinality(
268+
key,
269+
outlierRawValueOccurences,
270+
inlierRawValueOccurences,
271+
outlierPropertyOccurences,
272+
inlierPropertyOccurences,
273+
)
274+
) {
275+
hiddenProperties.push(key);
276+
} else {
277+
visibleProperties.push(key);
211278
}
212-
// Now process the keys to find the ones with the highest delta between outlier and inlier percentages
213-
const sortedProperties = Array.from(uniqueKeys)
214-
.map(key => {
215-
const inlierCount =
216-
inlierValueOccurences.get(key) ?? new Map<string, number>();
217-
const outlierCount =
218-
outlierValueOccurences.get(key) ?? new Map<string, number>();
219-
220-
const mergedArray = mergeValueStatisticsMaps(
221-
outlierCount,
222-
inlierCount,
223-
);
224-
let maxValueDelta = 0;
225-
mergedArray.forEach(item => {
226-
const delta = Math.abs(item.outlierCount - item.inlierCount);
227-
if (delta > maxValueDelta) {
228-
maxValueDelta = delta;
229-
}
230-
});
231-
232-
return [key, maxValueDelta] as const;
233-
})
234-
.sort((a, b) => b[1] - a[1])
235-
.map(a => a[0]);
236-
237-
return {
238-
sortedProperties,
239-
outlierValueOccurences,
240-
inlierValueOccurences,
241-
};
242-
}, [outlierData?.data, inlierData?.data]);
279+
});
280+
281+
return {
282+
visibleProperties,
283+
hiddenProperties,
284+
outlierValueOccurences,
285+
inlierValueOccurences,
286+
};
287+
}, [outlierData?.data, inlierData?.data, columnMeta]);
243288

244289
const [activePage, setPage] = useState(1);
245290

@@ -260,11 +305,9 @@ export default function DBDeltaChart({
260305
(CHART_HEIGHT + CHART_GAP),
261306
),
262307
);
263-
const PAGE_SIZE = columns * rows;
264-
265308
useEffect(() => {
266309
setPage(1);
267-
}, [PAGE_SIZE, xMin, xMax, yMin, yMax]);
310+
}, [columns, rows, xMin, xMax, yMin, yMax]);
268311

269312
if (error) {
270313
return (
@@ -301,7 +344,37 @@ export default function DBDeltaChart({
301344
);
302345
}
303346

304-
const totalPages = Math.ceil(sortedProperties.length / PAGE_SIZE);
347+
// Row-based pagination: visible and hidden sections are separate grids,
348+
// so an incomplete last row of visible fields does not get "filled" with hidden items.
349+
const visibleRows = Math.ceil(visibleProperties.length / columns);
350+
const hiddenRows = Math.ceil(hiddenProperties.length / columns);
351+
const totalRows = visibleRows + hiddenRows;
352+
const totalPages = Math.ceil(totalRows / rows);
353+
354+
const pageRowStart = (activePage - 1) * rows;
355+
const pageRowEnd = activePage * rows;
356+
357+
// Rows occupied by the visible section on this page
358+
const visRowStart = Math.min(pageRowStart, visibleRows);
359+
const visRowEnd = Math.min(pageRowEnd, visibleRows);
360+
const visibleOnPage = visibleProperties.slice(
361+
visRowStart * columns,
362+
Math.min(visRowEnd * columns, visibleProperties.length),
363+
);
364+
365+
// Rows occupied by the hidden section on this page
366+
const hidRowStart = Math.max(0, pageRowStart - visibleRows);
367+
const hidRowEnd = Math.min(hiddenRows, Math.max(0, pageRowEnd - visibleRows));
368+
const hiddenOnPage = hiddenProperties.slice(
369+
hidRowStart * columns,
370+
Math.min(hidRowEnd * columns, hiddenProperties.length),
371+
);
372+
373+
// Show a divider when both sections appear on the same page
374+
const showDivider = visibleOnPage.length > 0 && hiddenOnPage.length > 0;
375+
// Show a header when ONLY hidden fields appear on this page (no divider above)
376+
const showHiddenHeader =
377+
hiddenOnPage.length > 0 && visibleOnPage.length === 0;
305378

306379
return (
307380
<Box
@@ -314,16 +387,16 @@ export default function DBDeltaChart({
314387
flexDirection: 'column',
315388
}}
316389
>
317-
<div
318-
style={{
319-
display: 'grid',
320-
gridTemplateColumns: `repeat(${columns}, 1fr)`,
321-
gap: CHART_GAP,
322-
}}
323-
>
324-
{sortedProperties
325-
.slice((activePage - 1) * PAGE_SIZE, activePage * PAGE_SIZE)
326-
.map(property => (
390+
{/* Primary fields */}
391+
{visibleOnPage.length > 0 && (
392+
<div
393+
style={{
394+
display: 'grid',
395+
gridTemplateColumns: `repeat(${columns}, 1fr)`,
396+
gap: CHART_GAP,
397+
}}
398+
>
399+
{visibleOnPage.map(property => (
327400
<PropertyComparisonChart
328401
name={property}
329402
outlierValueOccurences={
@@ -335,7 +408,44 @@ export default function DBDeltaChart({
335408
key={property}
336409
/>
337410
))}
338-
</div>
411+
</div>
412+
)}
413+
{/* Divider between primary and lower-priority fields */}
414+
{(showDivider || showHiddenHeader) && (
415+
<Divider
416+
mt="lg"
417+
mb="xs"
418+
label={
419+
<Text size="xs" c="dimmed">
420+
Lower-priority fields ({hiddenProperties.length})
421+
</Text>
422+
}
423+
labelPosition="left"
424+
/>
425+
)}
426+
{/* Lower-priority fields — separate grid so rows align independently */}
427+
{hiddenOnPage.length > 0 && (
428+
<div
429+
style={{
430+
display: 'grid',
431+
gridTemplateColumns: `repeat(${columns}, 1fr)`,
432+
gap: CHART_GAP,
433+
}}
434+
>
435+
{hiddenOnPage.map(key => (
436+
<PropertyComparisonChart
437+
name={key}
438+
outlierValueOccurences={
439+
outlierValueOccurences.get(key) ?? new Map()
440+
}
441+
inlierValueOccurences={
442+
inlierValueOccurences.get(key) ?? new Map()
443+
}
444+
key={key}
445+
/>
446+
))}
447+
</div>
448+
)}
339449
<Flex
340450
justify="flex-end"
341451
style={{

0 commit comments

Comments
 (0)