mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Closes HDX-3124 # Summary This PR makes the following changes 1. Date ranges for all MV queries are now aligned to the MV Granularity 2. Each chart type now has an indicator when the date range has been adjusted to align with either the MV Granularity or (in the case of Line/Bar charts) the Chart Granularity. 3. The useQueriedChartConfig, useRenderedSqlChartConfig, and useOffsetPaginatedQuery hooks have been updated to get the MV-optimized chart configuration from the useMVOptimizationExplanation, which allows us to share the `EXPLAIN ESTIMATE` query results between the MV Optimization Indicator (the lightning bolt icon on each chart) and the chart itself. This roughly halves the number of EXPLAIN ESTIMATE queries that are made. ## Demo <img width="1628" height="1220" alt="Screenshot 2026-01-08 at 11 42 39 AM" src="https://github.com/user-attachments/assets/80a06e3a-bbfc-4193-b6b7-5e0056c588d3" /> <img width="1627" height="1131" alt="Screenshot 2026-01-08 at 11 40 54 AM" src="https://github.com/user-attachments/assets/69879e3d-3a83-4c4d-9604-0552a01c17d7" /> ## Testing To test locally with an MV, you can use the following DDL <details> <summary>DDL For an MV</summary> ```sql CREATE TABLE default.metrics_rollup_1m ( `Timestamp` DateTime, `ServiceName` LowCardinality(String), `SpanKind` LowCardinality(String), `StatusCode` LowCardinality(String), `count` SimpleAggregateFunction(sum, UInt64), `sum__Duration` SimpleAggregateFunction(sum, UInt64), `avg__Duration` AggregateFunction(avg, UInt64), `quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64), `min__Duration` SimpleAggregateFunction(min, UInt64), `max__Duration` SimpleAggregateFunction(max, UInt64) ) ENGINE = AggregatingMergeTree PARTITION BY toDate(Timestamp) ORDER BY (Timestamp, StatusCode, SpanKind, ServiceName) SETTINGS index_granularity = 8192; CREATE MATERIALIZED VIEW default.metrics_rollup_1m_mv TO default.metrics_rollup_1m ( `Timestamp` DateTime, `ServiceName` LowCardinality(String), `SpanKind` LowCardinality(String), `version` LowCardinality(String), `StatusCode` LowCardinality(String), `count` UInt64, `sum__Duration` Int64, `avg__Duration` AggregateFunction(avg, UInt64), `quantile__Duration` AggregateFunction(quantileTDigest(0.5), UInt64), `min__Duration` SimpleAggregateFunction(min, UInt64), `max__Duration` SimpleAggregateFunction(max, UInt64) ) AS SELECT toStartOfMinute(Timestamp) AS Timestamp, ServiceName, SpanKind, StatusCode, count() AS count, sum(Duration) AS sum__Duration, avgState(Duration) AS avg__Duration, quantileTDigestState(0.5)(Duration) AS quantile__Duration, minSimpleState(Duration) AS min__Duration, maxSimpleState(Duration) AS max__Duration FROM default.otel_traces GROUP BY Timestamp, ServiceName, SpanKind, StatusCode; ``` </details>
178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
import React from 'react';
|
|
import objectHash from 'object-hash';
|
|
import {
|
|
ChartConfigWithDateRange,
|
|
DisplayType,
|
|
} from '@hyperdx/common-utils/dist/types';
|
|
|
|
import { DBTimeChart } from '@/components/DBTimeChart';
|
|
import SearchTotalCountChart from '@/components/SearchTotalCountChart';
|
|
|
|
// Mock the API and hooks
|
|
jest.mock('@/api', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
useMe: () => ({
|
|
data: { team: { parallelizeWhenPossible: false } },
|
|
isLoading: false,
|
|
}),
|
|
},
|
|
}));
|
|
|
|
jest.mock('@/hooks/useMVOptimizationExplanation', () => ({
|
|
useMVOptimizationExplanation: jest.fn().mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
isPlaceholderData: false,
|
|
}),
|
|
}));
|
|
|
|
jest.mock('@/hooks/useChartConfig', () => ({
|
|
useQueriedChartConfig: jest.fn(() => ({
|
|
data: { data: [], isComplete: true },
|
|
isLoading: false,
|
|
isError: false,
|
|
isPlaceholderData: false,
|
|
isSuccess: true,
|
|
})),
|
|
}));
|
|
|
|
jest.mock('@/source', () => ({
|
|
useSource: () => ({ data: null, isLoading: false }),
|
|
}));
|
|
|
|
jest.mock('@/ChartUtils', () => ({
|
|
useTimeChartSettings: () => ({
|
|
displayType: DisplayType.StackedBar,
|
|
dateRange: [new Date('2024-01-01'), new Date('2024-01-02')],
|
|
granularity: '30 minutes',
|
|
fillNulls: true,
|
|
}),
|
|
formatResponseForTimeChart: () => ({
|
|
graphResults: [],
|
|
timestampColumn: undefined,
|
|
lineData: [],
|
|
groupColumns: [],
|
|
valueColumns: [],
|
|
isSingleValueColumn: true,
|
|
}),
|
|
getPreviousDateRange: (dateRange: [Date, Date]) => [
|
|
new Date('2023-12-31'),
|
|
new Date('2024-01-01'),
|
|
],
|
|
getAlignedDateRange: (dateRange: [Date, Date]) => dateRange,
|
|
convertToTimeChartConfig:
|
|
jest.requireActual('@/ChartUtils').convertToTimeChartConfig,
|
|
}));
|
|
|
|
describe('DBSearchPage QueryKey Consistency', () => {
|
|
let mockUseQueriedChartConfig: jest.Mock;
|
|
|
|
beforeEach(async () => {
|
|
mockUseQueriedChartConfig = (await import('@/hooks/useChartConfig'))
|
|
.useQueriedChartConfig as any;
|
|
mockUseQueriedChartConfig.mockClear();
|
|
});
|
|
|
|
it('should use matching queryKeys between SearchTotalCountChart and DBTimeChart', () => {
|
|
const config: ChartConfigWithDateRange = {
|
|
select: 'count()',
|
|
from: { databaseName: 'test', tableName: 'logs' },
|
|
where: '',
|
|
timestampValueExpression: 'timestamp',
|
|
connection: 'test-connection',
|
|
displayType: DisplayType.StackedBar,
|
|
dateRange: [new Date('2024-01-01'), new Date('2024-01-02')],
|
|
};
|
|
|
|
const queryKeyPrefix = 'search';
|
|
|
|
// Render SearchTotalCountChart
|
|
renderWithMantine(
|
|
<SearchTotalCountChart
|
|
config={config}
|
|
queryKeyPrefix={queryKeyPrefix}
|
|
enableParallelQueries={true}
|
|
/>,
|
|
);
|
|
|
|
// Render DBTimeChart
|
|
renderWithMantine(
|
|
<DBTimeChart
|
|
config={config}
|
|
queryKeyPrefix={queryKeyPrefix}
|
|
enableParallelQueries={true}
|
|
/>,
|
|
);
|
|
|
|
// Get all calls to useQueriedChartConfig
|
|
const calls = mockUseQueriedChartConfig.mock.calls;
|
|
|
|
// Should have at least 2 calls (one for each component)
|
|
expect(calls.length).toBeGreaterThanOrEqual(2);
|
|
|
|
// Extract queryKey from each call
|
|
const searchTotalCountQueryKey = calls[0][1]?.queryKey;
|
|
const dbTimeChartQueryKey = calls[1][1]?.queryKey;
|
|
|
|
// Both should exist
|
|
expect(searchTotalCountQueryKey).toBeDefined();
|
|
expect(dbTimeChartQueryKey).toBeDefined();
|
|
|
|
// The key structure should be identical for both components
|
|
// This ensures React Query can properly dedupe the queries
|
|
expect(searchTotalCountQueryKey).toEqual(dbTimeChartQueryKey);
|
|
|
|
// Additional object hash check for deep equality verification
|
|
const searchQueryKeyHash = objectHash(searchTotalCountQueryKey);
|
|
const chartQueryKeyHash = objectHash(dbTimeChartQueryKey);
|
|
expect(searchQueryKeyHash).toBe(chartQueryKeyHash);
|
|
});
|
|
|
|
it('should use consistent queryKeys when disableQueryChunking is set', () => {
|
|
const config: ChartConfigWithDateRange = {
|
|
select: 'count()',
|
|
from: { databaseName: 'test', tableName: 'logs' },
|
|
where: '',
|
|
timestampValueExpression: 'timestamp',
|
|
connection: 'test-connection',
|
|
displayType: DisplayType.StackedBar,
|
|
dateRange: [new Date('2024-01-01'), new Date('2024-01-02')],
|
|
};
|
|
|
|
const queryKeyPrefix = 'search';
|
|
|
|
// Render both components with disableQueryChunking
|
|
renderWithMantine(
|
|
<SearchTotalCountChart
|
|
config={config}
|
|
queryKeyPrefix={queryKeyPrefix}
|
|
disableQueryChunking={true}
|
|
/>,
|
|
);
|
|
|
|
renderWithMantine(
|
|
<DBTimeChart
|
|
config={config}
|
|
queryKeyPrefix={queryKeyPrefix}
|
|
disableQueryChunking={true}
|
|
/>,
|
|
);
|
|
|
|
const calls = mockUseQueriedChartConfig.mock.calls;
|
|
const searchQueryKey = calls[0][1]?.queryKey;
|
|
const chartQueryKey = calls[1][1]?.queryKey;
|
|
|
|
// Verify the options include disableQueryChunking
|
|
expect(searchQueryKey[3]).toHaveProperty('disableQueryChunking', true);
|
|
expect(chartQueryKey[3]).toHaveProperty('disableQueryChunking', true);
|
|
|
|
// Keys should still match
|
|
expect(searchQueryKey).toEqual(chartQueryKey);
|
|
|
|
// Additional object hash check for deep equality verification
|
|
const searchQueryKeyHash = objectHash(searchQueryKey);
|
|
const chartQueryKeyHash = objectHash(chartQueryKey);
|
|
expect(searchQueryKeyHash).toBe(chartQueryKeyHash);
|
|
});
|
|
});
|