Skip to content

[Incident management] Suggested dashboards are returned only for custom threshold alerts #224458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,51 @@
*/

import { omit } from 'lodash';
import { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold';
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
import { DataViewSpec } from '@kbn/response-ops-rule-params/common';
import {
ALERT_RULE_PARAMETERS,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
fields as TECHNICAL_ALERT_FIELDS,
} from '@kbn/rule-data-utils';
import { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold';
import { DataViewSpec } from '@kbn/response-ops-rule-params/common';
import {
isSuggestedDashboardsValidRuleTypeId,
SuggestedDashboardsValidRuleTypeIds,
} from './helpers';

// TS will make sure that if we add a new supported rule type id we had the corresponding function to get the relevant rule fields
const getRelevantRuleFieldsMap: Record<
SuggestedDashboardsValidRuleTypeIds,
(ruleParams: { [key: string]: unknown }) => Set<string>
> = {
[OBSERVABILITY_THRESHOLD_RULE_TYPE_ID]: (customThresholdParams) => {
const relevantFields = new Set<string>();
const metrics = (customThresholdParams as CustomThresholdParams).criteria[0].metrics;
metrics.forEach((metric) => {
// The property "field" is of type string | never but it collapses to just string
// We should probably avoid typing field as never and just omit it from the type to avoid situations like this one
if ('field' in metric) relevantFields.add(metric.field);
});
return relevantFields;
},
};

const getRuleQueryIndexMap: Record<
SuggestedDashboardsValidRuleTypeIds,
(ruleParams: { [key: string]: unknown }) => string | null
> = {
[OBSERVABILITY_THRESHOLD_RULE_TYPE_ID]: (customThresholdParams) => {
const {
searchConfiguration: { index },
} = customThresholdParams as CustomThresholdParams;
if (typeof index === 'object') return (index as DataViewSpec)?.id || null;
if (typeof index === 'string') return index;
return null;
},
};

export class AlertData {
constructor(private alert: Awaited<ReturnType<AlertsClient['get']>>) {}
Expand All @@ -30,21 +65,14 @@ export class AlertData {

getRelevantRuleFields(): Set<string> {
const ruleParameters = this.getRuleParameters();
const relevantFields = new Set<string>();
if (!ruleParameters) {
throw new Error('No rule parameters found');
}
switch (this.getRuleTypeId()) {
case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID:
const customThresholdParams = ruleParameters as CustomThresholdParams;
const metrics = customThresholdParams.criteria[0].metrics;
metrics.forEach((metric) => {
relevantFields.add(metric.field);
});
return relevantFields;
default:
return relevantFields;
}
const ruleTypeId = this.getRuleTypeId();

return isSuggestedDashboardsValidRuleTypeId(ruleTypeId)
? getRelevantRuleFieldsMap[ruleTypeId](ruleParameters)
: new Set<string>();
}

getRelevantAADFields(): string[] {
Expand Down Expand Up @@ -74,17 +102,10 @@ export class AlertData {
if (!ruleParameters) {
throw new Error('No rule parameters found');
}
switch (ruleTypeId) {
case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID:
const customThresholdParams = ruleParameters as CustomThresholdParams;
if (typeof customThresholdParams.searchConfiguration.index === 'object')
return (customThresholdParams.searchConfiguration.index as DataViewSpec)?.id || null;
if (typeof customThresholdParams.searchConfiguration.index === 'string')
return customThresholdParams.searchConfiguration.index;
return null;
default:
return null;
}

return isSuggestedDashboardsValidRuleTypeId(ruleTypeId)
? getRuleQueryIndexMap[ruleTypeId](ruleParameters)
: null;
}

getRuleTypeId(): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';

const SUGGESTED_DASHBOARDS_VALID_RULE_TYPE_IDS = [OBSERVABILITY_THRESHOLD_RULE_TYPE_ID] as const;

export type SuggestedDashboardsValidRuleTypeIds =
(typeof SUGGESTED_DASHBOARDS_VALID_RULE_TYPE_IDS)[number];

export const isSuggestedDashboardsValidRuleTypeId = (
ruleTypeId?: string
): ruleTypeId is SuggestedDashboardsValidRuleTypeIds => {
return (
ruleTypeId !== undefined &&
Object.values<string>(SUGGESTED_DASHBOARDS_VALID_RULE_TYPE_IDS).includes(ruleTypeId)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Logger } from '@kbn/core/server';
import { IContentClient } from '@kbn/content-management-plugin/server/types';
import { InvestigateAlertsClient } from './investigate_alerts_client';
import { AlertData } from './alert_data';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';

describe('RelatedDashboardsClient', () => {
let logger: jest.Mocked<Logger>;
Expand All @@ -21,6 +22,7 @@ describe('RelatedDashboardsClient', () => {
getAllRelevantFields: jest.fn().mockReturnValue(['field1', 'field2']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
getRuleId: jest.fn().mockReturnValue('rule-id'),
getRuleTypeId: jest.fn().mockReturnValue(OBSERVABILITY_THRESHOLD_RULE_TYPE_ID),
} as unknown as AlertData;

beforeEach(() => {
Expand Down Expand Up @@ -59,6 +61,8 @@ describe('RelatedDashboardsClient', () => {
alertId = 'test-alert-id';

client = new RelatedDashboardsClient(logger, dashboardClient, alertsClient, alertId);

jest.clearAllMocks();
});

describe('fetchSuggestedDashboards', () => {
Expand Down Expand Up @@ -378,6 +382,20 @@ describe('RelatedDashboardsClient', () => {
},
]);
});

it('should not fetch suggested dashboards when the rule type id is not supported', async () => {
const mockAlert = {
...baseMockAlert,
getRuleTypeId: jest.fn().mockReturnValue('unsupported-type-id'),
} as unknown as AlertData;
alertsClient.getAlertById.mockResolvedValue(mockAlert);

const result = await client.fetchRelatedDashboards();

expect(result.suggestedDashboards).toEqual([]);
expect(mockAlert.getRuleTypeId).toHaveBeenCalled();
expect(mockAlert.getAllRelevantFields).not.toHaveBeenCalled();
});
});

describe('fetchDashboards', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
} from '@kbn/observability-schema';
import type { InvestigateAlertsClient } from './investigate_alerts_client';
import type { AlertData } from './alert_data';
import { isSuggestedDashboardsValidRuleTypeId } from './helpers';

type Dashboard = SavedObjectsFindResult<DashboardAttributes>;
export class RelatedDashboardsClient {
Expand Down Expand Up @@ -71,6 +72,8 @@ export class RelatedDashboardsClient {

private async fetchSuggestedDashboards(): Promise<SuggestedDashboard[]> {
const alert = this.checkAlert();
if (!isSuggestedDashboardsValidRuleTypeId(alert.getRuleTypeId())) return [];

const allSuggestedDashboards = new Set<SuggestedDashboard>();
const relevantDashboardsById = new Map<string, SuggestedDashboard>();
const index = this.getRuleQueryIndex();
Expand Down