diff --git a/.changeset/silent-pandas-search.md b/.changeset/silent-pandas-search.md new file mode 100644 index 0000000000..9d9ef3e761 --- /dev/null +++ b/.changeset/silent-pandas-search.md @@ -0,0 +1,6 @@ +--- +'@hyperdx/api': patch +--- + +Apply saved-search pinned filters to alert notification sample queries so sample +log lines come from the same row set the alert counted. diff --git a/packages/api/src/tasks/checkAlerts/__tests__/renderAlertTemplate.int.test.ts b/packages/api/src/tasks/checkAlerts/__tests__/renderAlertTemplate.int.test.ts index d5e05842ad..d7ffdcdf10 100644 --- a/packages/api/src/tasks/checkAlerts/__tests__/renderAlertTemplate.int.test.ts +++ b/packages/api/src/tasks/checkAlerts/__tests__/renderAlertTemplate.int.test.ts @@ -81,12 +81,20 @@ const sampleLogsCsv = [ '"2023-03-17 22:12:30","error","Retry limit exceeded"', ].join('\n'); +const sampleLogsResultSet = () => ({ + json: jest.fn().mockResolvedValue({ data: [] }), + text: jest.fn().mockResolvedValue(sampleLogsCsv), +}); + +// Typed separately from the client so tests can swap the implementation +// without spying through the `any` cast below, which erases the arg types. +const defaultClickhouseQuery = async (_props: { query: string }) => + sampleLogsResultSet(); +const mockClickhouseQuery = jest.fn(defaultClickhouseQuery); + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const mockClickhouseClient = { - query: jest.fn().mockResolvedValue({ - json: jest.fn().mockResolvedValue({ data: [] }), - text: jest.fn().mockResolvedValue(sampleLogsCsv), - }), + query: mockClickhouseQuery, } as any; const startTime = new Date('2023-03-17T22:10:00.000Z'); @@ -713,6 +721,35 @@ describe('enriched message fields', () => { }); }); + it('applies the saved search pinned filters to the sample log query', async () => { + const seenQueries: string[] = []; + mockClickhouseQuery.mockImplementation(async ({ query }) => { + seenQueries.push(query); + return sampleLogsResultSet(); + }); + + try { + await renderAlertTemplate({ + alertProvider, + clickhouseClient: mockClickhouseClient, + metadata: mockMetadata, + state: AlertState.ALERT, + template: null, + title: 'Test Alert Title', + view: makeSearchView({ + filters: [{ type: 'sql', condition: "ServiceName = 'checkout'" }], + }), + teamId: TEST_TEAM_ID, + teamWebhooksById: new Map(), + }); + + const fullQuery = seenQueries.join('\n'); + expect(fullQuery).toContain("ServiceName = 'checkout'"); + } finally { + mockClickhouseQuery.mockImplementation(defaultClickhouseQuery); + } + }); + it('reports an empty sourceQuery when the chart carries no condition', async () => { const { dispatched } = await renderWithWebhook( AlertState.ALERT, diff --git a/packages/api/src/tasks/checkAlerts/template.ts b/packages/api/src/tasks/checkAlerts/template.ts index b6440f897e..a2850c6207 100644 --- a/packages/api/src/tasks/checkAlerts/template.ts +++ b/packages/api/src/tasks/checkAlerts/template.ts @@ -1,6 +1,7 @@ import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { Metadata } from '@hyperdx/common-utils/dist/core/metadata'; import { renderChartConfig } from '@hyperdx/common-utils/dist/core/renderChartConfig'; +import { buildSearchChartConfig } from '@hyperdx/common-utils/dist/core/searchChartConfig'; import { formatDate, objectHash } from '@hyperdx/common-utils/dist/core/utils'; import { isPromqlSavedChartConfig, @@ -11,10 +12,8 @@ import { AlertThresholdType, BuilderChartConfigWithOptDateRange, ChartConfigWithOptDateRange, - DisplayType, Filter, isRangeThresholdType, - pickSampleWeightExpressionProps, SavedChartConfig, SourceKind, zAlertChannelType, @@ -531,33 +530,23 @@ export const fetchSampleLines = async ({ metadata: Metadata; savedSearch: Pick< ISavedSearch, - 'id' | 'select' | 'where' | 'whereLanguage' | 'orderBy' + 'id' | 'select' | 'where' | 'whereLanguage' | 'orderBy' | 'filters' >; source: ISource; startTime: Date; }): Promise => { - const isEventSource = - source.kind === SourceKind.Log || source.kind === SourceKind.Trace; const chartConfig: ChartConfigWithOptDateRange = { - connection: '', // no need for the connection id since clickhouse client is already initialized - displayType: DisplayType.Search, - dateRange: [startTime, endTime], - from: source.from, - select: - savedSearch.select || - (isEventSource && source.defaultTableSelectExpression) || - '', - where: savedSearch.where, - whereLanguage: savedSearch.whereLanguage, - implicitColumnExpression: isEventSource - ? source.implicitColumnExpression - : undefined, - useTextIndexForImplicitColumn: isEventSource - ? source.useTextIndexForImplicitColumn - : undefined, - ...pickSampleWeightExpressionProps(source), - timestampValueExpression: source.timestampValueExpression, - orderBy: savedSearch.orderBy, + ...buildSearchChartConfig(source, { + connection: '', // no need for the connection id since clickhouse client is already initialized + dateRange: [startTime, endTime], + select: savedSearch.select, + where: savedSearch.where, + whereLanguage: savedSearch.whereLanguage, + filters: savedSearch.filters, + orderBy: savedSearch.orderBy, + dateRangeStartInclusive: true, + dateRangeEndInclusive: false, + }), limit: { limit: 5, offset: 0,