Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-banks-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---

Show elapsed time and Generated SQL for search timeline view
181 changes: 169 additions & 12 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import HyperDX from '@hyperdx/browser';
import {
ClickHouseQueryError,
ColumnMeta,
Expand Down Expand Up @@ -72,13 +73,14 @@ import { notifications } from '@mantine/notifications';
import {
IconArrowBarToRight,
IconBolt,
IconCode,
IconPlayerPlay,
IconPlus,
IconStack2,
IconTags,
IconX,
} from '@tabler/icons-react';
import { useIsFetching } from '@tanstack/react-query';
import { keepPreviousData, useIsFetching } from '@tanstack/react-query';
import { SortingState } from '@tanstack/react-table';
import CodeMirror from '@uiw/react-codemirror';

Expand Down Expand Up @@ -120,9 +122,14 @@ import {
parseTimeQuery,
useNewTimeQuery,
} from '@/timeQuery';
import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils';
import {
formatDurationMs,
QUERY_LOCAL_STORAGE,
useLocalStorage,
usePrevious,
} from '@/utils';

import { SQLPreview } from './components/ChartSQLPreview';
import ChartSQLPreview, { SQLPreview } from './components/ChartSQLPreview';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import PatternTable from './components/PatternTable';
import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart';
Expand Down Expand Up @@ -330,30 +337,93 @@ function SearchResultsCountGroup({
);
}

function SearchNumRows({
export function SearchNumRows({
config,
sqlConfig,
enabled,
searchElapsedMs,
isSearching,
isLiveTail = false,
}: {
config: ChartConfigWithDateRange;
sqlConfig?: ChartConfigWithDateRange;
enabled: boolean;
searchElapsedMs: number | null;
isSearching: boolean;
isLiveTail?: boolean;
}) {
const [statsOpened, { open: openStats, close: closeStats }] =
useDisclosure(false);
const { data, isLoading, error } = useExplainQuery(config, {
enabled,
// Keep the previous row count on screen while a new EXPLAIN runs so the
// "Scanned Rows" value doesn't flash a loading state on every live-tail
// poll (each poll changes the dateRange, and thus the query key).
placeholderData: keepPreviousData,
});

if (!enabled) {
return null;
}

const numRows = data?.[0]?.rows;
const explainRow = data?.[0];
const numRows = explainRow?.rows;
const hasData = !isLoading && !error && numRows != null;

// During live tail we keep showing the last measured elapsed time and never
// flash the "..." loading state, so the value doesn't flicker between polls.
const showElapsedLoading = isSearching && !isLiveTail;
const showElapsed = showElapsedLoading || searchElapsedMs != null;

return (
<Text size="xs">
{isLoading
? 'Scanned Rows ...'
: error || !numRows
? ''
: `Scanned Rows: ${Number.parseInt(numRows)?.toLocaleString()}`}
</Text>
<>
<Modal
opened={statsOpened}
onClose={closeStats}
title={sqlConfig != null ? 'Generated SQL (Timeline)' : 'Generated SQL'}
size="xl"
>
<ChartSQLPreview config={sqlConfig ?? config} enableCopy />
</Modal>
<Group gap={4} align="center">
<Text size="xs">
{isLoading
? 'Scanned Rows ...'
: error || numRows == null
? ''
: `Scanned Rows: ${Number(numRows).toLocaleString()}`}
</Text>
{showElapsed && (
<>
{(hasData || isLoading) && (
<Text size="xs" c="dimmed">
|
</Text>
)}
<Text size="xs">
{showElapsedLoading
? 'Elapsed Time: ...'
: `Elapsed Time: ${formatDurationMs(searchElapsedMs!)}`}
</Text>
</>
)}
{/* The generated-SQL preview is derived purely from config, not the
explain query, so it renders unconditionally. Gating it on explain
loading/data would make it flicker on every live-tail poll, since
each poll changes the dateRange (and thus the explain queryKey). */}
<Tooltip label="Show Generated SQL" position="top">
<ActionIcon
variant="subtle"
size="sm"
color="gray"
onClick={openStats}
aria-label="Show Generated SQL"
>
<IconCode size={16} />
</ActionIcon>
</Tooltip>
</Group>
</>
);
}

Expand Down Expand Up @@ -810,6 +880,79 @@ const queryStateMap = {
orderBy: parseAsStringEncoded,
};

export function useSearchTelemetry({
isAnyQueryFetching,
isLive,
sourceId,
}: {
isAnyQueryFetching: boolean;
/** When true the hook suppresses recording and emission so live-tail
* background refetches do not flood the metric. */
isLive: boolean;
sourceId: string | null;
}) {
const searchStartTimeRef = useRef<number | null>(null);
const wasFetchingRef = useRef(false);
// Whether the in-flight cycle began as a live-tail refresh, captured on the
// rising edge so a mid-cycle isLive flip can't change how it's treated.
const cycleIsLiveRef = useRef(false);

// Snapshot latency_ms and source_id together so a later sourceId change does
// not cause the emission effect to re-fire with stale latency data. `emit`
// records whether this cycle should be reported to telemetry (user-initiated
// searches only); latency is still surfaced for display in every case.
const [completedSearch, setCompletedSearch] = useState<{
latency_ms: number;
source_id: string;
emit: boolean;
} | null>(null);

useEffect(() => {
if (isAnyQueryFetching) {
// Start the timer once per fetch cycle (for live tail too — we display
// its elapsed time, we just don't emit telemetry for it).
if (!wasFetchingRef.current) {
searchStartTimeRef.current = performance.now();
cycleIsLiveRef.current = isLive;
// Only blank the displayed timer for user-initiated searches. During
// live tail we keep the previous value so it doesn't flicker between
// background refreshes.
if (!isLive) {
setCompletedSearch(null);
}
}
wasFetchingRef.current = true;
} else {
if (searchStartTimeRef.current != null) {
setCompletedSearch({
latency_ms: Math.round(
performance.now() - searchStartTimeRef.current,
),
source_id: sourceId ?? '',
emit: !cycleIsLiveRef.current,
});
searchStartTimeRef.current = null;
}
wasFetchingRef.current = false;
}
}, [isAnyQueryFetching, isLive, sourceId]);
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// completedSearch is the only dep here — sourceId was snapshotted at
// completion time so changing source after a finished search does not
// re-emit the previous run's latency against the new source. Live-tail
// cycles are recorded for display but flagged emit=false so they never flood
// telemetry.
useEffect(() => {
if (completedSearch == null || !completedSearch.emit) return;
HyperDX.addAction('search executed', {
latency_ms: completedSearch.latency_ms,
source_id: completedSearch.source_id,
});
}, [completedSearch]);

return { searchElapsedMs: completedSearch?.latency_ms ?? null };
}

export function DBSearchPage() {
const brandName = useBrandDisplayName();
// Next router is laggy behind window.location, which causes race
Expand Down Expand Up @@ -1339,6 +1482,12 @@ export function DBSearchPage() {
queryKey: [QUERY_KEY_PREFIX],
}) > 0;

const { searchElapsedMs } = useSearchTelemetry({
isAnyQueryFetching,
isLive: isLive ?? false,
sourceId: chartConfig?.source ?? null,
});

const isTabVisible = useDocumentVisibility();

// State for collapsing all expanded rows when resuming live tail
Expand Down Expand Up @@ -2193,7 +2342,11 @@ export function DBSearchPage() {
...chartConfig,
dateRange: searchedTimeRange,
}}
sqlConfig={histogramTimeChartConfig ?? undefined}
enabled={isReady}
searchElapsedMs={searchElapsedMs}
isSearching={isAnyQueryFetching}
isLiveTail={isLive ?? false}
/>
</Group>
</Box>
Expand Down Expand Up @@ -2287,7 +2440,11 @@ export function DBSearchPage() {
...chartConfig,
dateRange: searchedTimeRange,
}}
sqlConfig={histogramTimeChartConfig ?? undefined}
enabled={isReady}
searchElapsedMs={searchElapsedMs}
isSearching={isAnyQueryFetching}
isLiveTail={isLive ?? false}
/>
</Group>
</Group>
Expand Down
Loading
Loading