Skip to content

Commit 1bb16ed

Browse files
[8.19] [Incident management] Callout for alerts that triggered around the same time (#223473) (#225026)
# Backport This will backport the following commits from `main` to `8.19`: - [[Incident management] Callout for alerts that triggered around the same time (#223473)](#223473) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Bailey Cash","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-06-24T09:03:20Z","message":"[Incident management] Callout for alerts that triggered around the same time (#223473)\n\n## Summary\n\nImplements #213020\nPartially implements filter bar seen with #213015\n\n\nThis PR adds a callout on the alert details page to encourage users to\nvisit the related alerts page when at least one alert was triggered\nwithin 30 minutes of the current alert. If no alerts were triggered, the\nmessage remains without a call to action.\n\n\nhttps://github.com/user-attachments/assets/23b2d3e9-353b-45e1-a007-d188db5617fc\n\n\n\n## Testing\n\nThe related alert query usually find alerts that were raised within a\nday of each other. To find alerts that were raised within a few minutes,\ntry creating an SLO with a chosen groupBy field that will easily violate\na burn rate rule. Alerts should be triggered for each instance within\nseconds. Once the filter is executed, these alerts should appear without\nalerts that were triggered earlier in the day.","sha":"7da827e8d9b1d354c3d0093941e72ca79e821c3d","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-management","backport:version","v9.1.0","v8.19.0"],"title":"[Incident management] Callout for alerts that triggered around the same time","number":223473,"url":"https://github.com/elastic/kibana/pull/223473","mergeCommit":{"message":"[Incident management] Callout for alerts that triggered around the same time (#223473)\n\n## Summary\n\nImplements #213020\nPartially implements filter bar seen with #213015\n\n\nThis PR adds a callout on the alert details page to encourage users to\nvisit the related alerts page when at least one alert was triggered\nwithin 30 minutes of the current alert. If no alerts were triggered, the\nmessage remains without a call to action.\n\n\nhttps://github.com/user-attachments/assets/23b2d3e9-353b-45e1-a007-d188db5617fc\n\n\n\n## Testing\n\nThe related alert query usually find alerts that were raised within a\nday of each other. To find alerts that were raised within a few minutes,\ntry creating an SLO with a chosen groupBy field that will easily violate\na burn rate rule. Alerts should be triggered for each instance within\nseconds. Once the filter is executed, these alerts should appear without\nalerts that were triggered earlier in the day.","sha":"7da827e8d9b1d354c3d0093941e72ca79e821c3d"}},"sourceBranch":"main","suggestedTargetBranches":["8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/223473","number":223473,"mergeCommit":{"message":"[Incident management] Callout for alerts that triggered around the same time (#223473)\n\n## Summary\n\nImplements #213020\nPartially implements filter bar seen with #213015\n\n\nThis PR adds a callout on the alert details page to encourage users to\nvisit the related alerts page when at least one alert was triggered\nwithin 30 minutes of the current alert. If no alerts were triggered, the\nmessage remains without a call to action.\n\n\nhttps://github.com/user-attachments/assets/23b2d3e9-353b-45e1-a007-d188db5617fc\n\n\n\n## Testing\n\nThe related alert query usually find alerts that were raised within a\nday of each other. To find alerts that were raised within a few minutes,\ntry creating an SLO with a chosen groupBy field that will easily violate\na burn rate rule. Alerts should be triggered for each instance within\nseconds. Once the filter is executed, these alerts should appear without\nalerts that were triggered earlier in the day.","sha":"7da827e8d9b1d354c3d0093941e72ca79e821c3d"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Bailey Cash <[email protected]>
1 parent ffba2a7 commit 1bb16ed

File tree

12 files changed

+331
-22
lines changed

12 files changed

+331
-22
lines changed

src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ export interface SearchAlertsParams {
6868
* The page size to fetch
6969
*/
7070
pageSize: number;
71+
/**
72+
* Force using the default context, otherwise use the AlertQueryContext
73+
*/
74+
skipAlertsQueryContext?: boolean;
7175
/**
7276
* The minimum score to apply to the query
7377
*/

src/platform/packages/shared/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export const queryKeyPrefix = ['alerts', searchAlerts.name];
2727
* When testing components that depend on this hook, prefer mocking the {@link searchAlerts} function instead of the hook itself.
2828
* @external https://tanstack.com/query/v4/docs/framework/react/guides/testing
2929
*/
30-
export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryParams) => {
30+
export const useSearchAlertsQuery = ({
31+
data,
32+
skipAlertsQueryContext,
33+
...params
34+
}: UseSearchAlertsQueryParams) => {
3135
const {
3236
ruleTypeIds,
3337
consumers,
@@ -64,7 +68,7 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa
6468
trackScores,
6569
}),
6670
refetchOnWindowFocus: false,
67-
context: AlertsQueryContext,
71+
context: skipAlertsQueryContext ? undefined : AlertsQueryContext,
6872
enabled: ruleTypeIds.length > 0,
6973
// To avoid flash of empty state with pagination, see https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata
7074
keepPreviousData: true,

x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ describe('Alert details', () => {
196196
expect(alertDetails.queryByTestId('alert-summary-container')).toBeFalsy();
197197
expect(alertDetails.queryByTestId('overviewTab')).toBeTruthy();
198198
expect(alertDetails.queryByTestId('metadataTab')).toBeTruthy();
199+
expect(alertDetails.queryByTestId('relatedAlertsTab')).toBeTruthy();
199200
});
200201

201202
it('should show Metadata tab', async () => {

x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import React, { useCallback, useEffect, useState } from 'react';
9-
import { useHistory, useLocation, useParams } from 'react-router-dom';
9+
import { useParams } from 'react-router-dom';
1010
import { i18n } from '@kbn/i18n';
1111
import { FormattedMessage } from '@kbn/i18n-react';
1212
import {
@@ -59,6 +59,8 @@ import StaleAlert from './components/stale_alert';
5959
import { RelatedDashboards } from './components/related_dashboards';
6060
import { getAlertTitle } from '../../utils/format_alert_title';
6161
import { AlertSubtitle } from './components/alert_subtitle';
62+
import { ProximalAlertsCallout } from './proximal_alerts_callout';
63+
import { useTabId } from './hooks/use_tab_id';
6264
import { useRelatedDashboards } from './hooks/use_related_dashboards';
6365

6466
interface AlertDetailsPathParams {
@@ -73,7 +75,6 @@ const defaultBreadcrumb = i18n.translate('xpack.observability.breadcrumbs.alertD
7375
export const LOG_DOCUMENT_COUNT_RULE_TYPE_ID = 'logs.alert.document.count';
7476
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
7577
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
76-
const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId';
7778

7879
const TAB_IDS = [
7980
'overview',
@@ -90,6 +91,7 @@ const isTabId = (value: string): value is TabId => {
9091
};
9192

9293
export function AlertDetails() {
94+
const { services } = useKibana();
9395
const {
9496
cases: {
9597
helpers: { canUseCases },
@@ -100,12 +102,11 @@ export function AlertDetails() {
100102
observabilityAIAssistant,
101103
uiSettings,
102104
serverless,
103-
} = useKibana().services;
104-
105-
const { search } = useLocation();
106-
const history = useHistory();
105+
} = services;
107106
const { ObservabilityPageTemplate, config } = usePluginContext();
108107
const { alertId } = useParams<AlertDetailsPathParams>();
108+
const { getUrlTabId, setUrlTabId } = useTabId();
109+
const urlTabId = getUrlTabId();
109110
const {
110111
isLoadingRelatedDashboards,
111112
suggestedDashboards,
@@ -130,17 +131,17 @@ export function AlertDetails() {
130131
const { euiTheme } = useEuiTheme();
131132
const [sources, setSources] = useState<AlertDetailsSource[]>();
132133
const [activeTabId, setActiveTabId] = useState<TabId>();
134+
133135
const handleSetTabId = async (tabId: TabId) => {
134136
setActiveTabId(tabId);
135137

136-
let searchParams = new URLSearchParams(search);
137138
if (tabId === 'related_alerts') {
138-
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
139+
setUrlTabId(tabId, true, {
140+
filterProximal: 'true',
141+
});
139142
} else {
140-
searchParams = new URLSearchParams();
141-
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
143+
setUrlTabId(tabId, true);
142144
}
143-
history.replace({ search: searchParams.toString() });
144145
};
145146

146147
useEffect(() => {
@@ -166,11 +167,9 @@ export function AlertDetails() {
166167
if (alertDetail) {
167168
setRuleTypeModel(ruleTypeRegistry.get(alertDetail?.formatted.fields[ALERT_RULE_TYPE_ID]!));
168169
setAlertStatus(alertDetail?.formatted?.fields[ALERT_STATUS] as AlertStatus);
169-
const searchParams = new URLSearchParams(search);
170-
const urlTabId = searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY);
171170
setActiveTabId(urlTabId && isTabId(urlTabId) ? urlTabId : 'overview');
172171
}
173-
}, [alertDetail, ruleTypeRegistry, search]);
172+
}, [alertDetail, ruleTypeRegistry, urlTabId]);
174173

175174
useBreadcrumbs(
176175
[
@@ -194,6 +193,10 @@ export function AlertDetails() {
194193
setAlertStatus(ALERT_STATUS_UNTRACKED);
195194
}, []);
196195

196+
const showRelatedAlertsFromCallout = () => {
197+
handleSetTabId('related_alerts');
198+
};
199+
197200
usePageReady({
198201
isRefreshing: isLoading,
199202
isReady: !isLoading && !!alertDetail && activeTabId === 'overview',
@@ -248,6 +251,10 @@ export function AlertDetails() {
248251

249252
<EuiSpacer size="m" />
250253
<EuiFlexGroup direction="column" gutterSize="m">
254+
<ProximalAlertsCallout
255+
alertDetail={alertDetail}
256+
switchTabs={showRelatedAlertsFromCallout}
257+
/>
251258
<SourceBar alert={alertDetail.formatted} sources={sources} />
252259
<AlertDetailContextualInsights alert={alertDetail} />
253260
{rule && alertDetail.formatted && (
@@ -268,6 +275,11 @@ export function AlertDetails() {
268275
</>
269276
) : (
270277
<EuiPanel hasShadow={false} data-test-subj="overviewTabPanel" paddingSize="none">
278+
<EuiSpacer size="l" />
279+
<ProximalAlertsCallout
280+
alertDetail={alertDetail}
281+
switchTabs={showRelatedAlertsFromCallout}
282+
/>
271283
<EuiSpacer size="l" />
272284
<AlertDetailContextualInsights alert={alertDetail} />
273285
<EuiSpacer size="l" />

x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_alerts/related_alerts_table.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ALERT_START, ALERT_UUID } from '@kbn/rule-data-utils';
1111
import { AlertsTable } from '@kbn/response-ops-alerts-table';
1212
import { SortOrder } from '@elastic/elasticsearch/lib/api/types';
1313
import { getRelatedColumns } from './get_related_columns';
14-
import { useBuildRelatedAlertsQuery } from '../../hooks/related_alerts/use_build_related_alerts_query';
14+
import { getBuildRelatedAlertsQuery } from '../../hooks/related_alerts/get_build_related_alerts_query';
1515
import { AlertData } from '../../../../hooks/use_fetch_alert_detail';
1616
import {
1717
GetObservabilityAlertsTableProp,
@@ -25,6 +25,8 @@ import { AlertsFlyoutFooter } from '../../../../components/alerts_flyout/alerts_
2525
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../../../common/constants';
2626
import { AlertsTableCellValue } from '../../../../components/alerts_table/common/cell_value';
2727
import { casesFeatureIdV2 } from '../../../../../common';
28+
import { useFilterProximalParam } from '../../hooks/use_filter_proximal_param';
29+
import { RelatedAlertsTableFilter } from './related_alerts_table_filter';
2830

2931
interface Props {
3032
alertData: AlertData;
@@ -52,14 +54,15 @@ const RELATED_ALERTS_TABLE_ID = 'xpack.observability.alerts.relatedAlerts';
5254

5355
export function RelatedAlertsTable({ alertData }: Props) {
5456
const { formatted: alert } = alertData;
55-
const esQuery = useBuildRelatedAlertsQuery({ alert });
57+
const { filterProximal } = useFilterProximalParam();
58+
const esQuery = getBuildRelatedAlertsQuery({ alert, filterProximal });
5659
const { observabilityRuleTypeRegistry, config } = usePluginContext();
57-
5860
const services = useKibana().services;
5961

6062
return (
6163
<EuiFlexGroup direction="column" gutterSize="m">
6264
<EuiSpacer size="s" />
65+
<RelatedAlertsTableFilter />
6366
<AlertsTable<ObservabilityAlertsTableContext>
6467
id={RELATED_ALERTS_TABLE_ID}
6568
query={esQuery}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { EuiCheckbox, EuiFlexGroup, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui';
9+
import React from 'react';
10+
import { i18n } from '@kbn/i18n';
11+
import { useFilterProximalParam } from '../../hooks/use_filter_proximal_param';
12+
13+
export function RelatedAlertsTableFilter() {
14+
const { filterProximal, setProximalFilterParam } = useFilterProximalParam();
15+
16+
return (
17+
<EuiPanel paddingSize="m" hasShadow={false} color="subdued">
18+
<EuiFlexGroup direction="row" alignItems="center" justifyContent="flexStart">
19+
<EuiText size="s">
20+
<strong>
21+
{i18n.translate('xpack.observability.alerts.relatedAlerts.filtersLabel', {
22+
defaultMessage: 'Filters',
23+
})}
24+
</strong>
25+
</EuiText>
26+
<EuiFormRow fullWidth>
27+
<EuiCheckbox
28+
label={i18n.translate(
29+
'xpack.observability.alerts.relatedAlerts.proximityCheckboxLabel',
30+
{
31+
defaultMessage: 'Created around the same time',
32+
}
33+
)}
34+
checked={filterProximal}
35+
onChange={(event) => {
36+
setProximalFilterParam(event.target.checked);
37+
}}
38+
id={'proximal-alerts-checkbox'}
39+
data-test-subj="proximal-alerts-checkbox"
40+
/>
41+
</EuiFormRow>
42+
</EuiFlexGroup>
43+
</EuiPanel>
44+
);
45+
}
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ import { TopAlert } from '../../../../typings/alerts';
2323

2424
interface Props {
2525
alert: TopAlert<ObservabilityFields>;
26+
filterProximal: boolean;
2627
}
2728

28-
export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryContainer {
29+
export function getBuildRelatedAlertsQuery({
30+
alert,
31+
filterProximal,
32+
}: Props): QueryDslQueryContainer {
2933
const groups = alert.fields[ALERT_GROUP];
3034
const shouldGroups: QueryDslQueryContainer[] = [];
3135
groups?.forEach(({ field, value }) => {
@@ -58,14 +62,22 @@ export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryConta
5862
const tags = alert.fields[ALERT_RULE_TAGS] ?? [];
5963
const instanceId = alert.fields[ALERT_INSTANCE_ID]?.split(',') ?? [];
6064

65+
const range = filterProximal ? [30, 'minutes'] : [1, 'days'];
66+
6167
return {
6268
bool: {
6369
filter: [
6470
{
6571
range: {
6672
[ALERT_START]: {
67-
gte: startDate.clone().subtract(1, 'days').toISOString(),
68-
lte: startDate.clone().add(1, 'days').toISOString(),
73+
gte: startDate
74+
.clone()
75+
.subtract(...range)
76+
.toISOString(),
77+
lte: startDate
78+
.clone()
79+
.add(...range)
80+
.toISOString(),
6981
},
7082
},
7183
},
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { useHistory, useLocation } from 'react-router-dom';
9+
10+
export const useFilterProximalParam = () => {
11+
const { search } = useLocation();
12+
const searchParams = new URLSearchParams(search);
13+
const history = useHistory();
14+
15+
const setProximalFilterParam = (proximalFilter: boolean) => {
16+
searchParams.set('filterProximal', String(proximalFilter));
17+
history.replace({ search: searchParams.toString() });
18+
};
19+
20+
const filterProximal = searchParams.get('filterProximal') === 'true';
21+
22+
return { filterProximal, setProximalFilterParam };
23+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { useSearchAlertsQuery } from '@kbn/alerts-ui-shared/src/common/hooks/use_search_alerts_query';
9+
import {
10+
OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
11+
observabilityAlertFeatureIds,
12+
} from '../../../../common/constants';
13+
import { AlertData } from '../../../hooks/use_fetch_alert_detail';
14+
import { useKibana } from '../../../utils/kibana_react';
15+
import { getBuildRelatedAlertsQuery } from './related_alerts/get_build_related_alerts_query';
16+
17+
export const useFindProximalAlerts = (alertDetail: AlertData) => {
18+
const { services } = useKibana();
19+
20+
const esQuery = getBuildRelatedAlertsQuery({
21+
alert: alertDetail.formatted,
22+
filterProximal: true,
23+
});
24+
25+
return useSearchAlertsQuery({
26+
data: services.data,
27+
ruleTypeIds: OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
28+
consumers: observabilityAlertFeatureIds,
29+
query: esQuery,
30+
skipAlertsQueryContext: true,
31+
});
32+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { useHistory, useLocation } from 'react-router-dom';
9+
10+
const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId';
11+
12+
export const useTabId = () => {
13+
const { search } = useLocation();
14+
const history = useHistory();
15+
16+
const getUrlTabId = () => {
17+
const searchParams = new URLSearchParams(search);
18+
return searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY);
19+
};
20+
21+
const setUrlTabId = (
22+
tabId: string,
23+
overrideSearchState?: boolean,
24+
newSearchState?: Record<string, string>
25+
) => {
26+
const searchParams = new URLSearchParams(overrideSearchState ? undefined : search);
27+
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
28+
29+
if (newSearchState) {
30+
Object.entries(newSearchState).forEach(([key, value]) => {
31+
searchParams.set(key, value);
32+
});
33+
}
34+
35+
history.replace({ search: searchParams.toString() });
36+
};
37+
38+
return {
39+
getUrlTabId,
40+
setUrlTabId,
41+
};
42+
};

0 commit comments

Comments
 (0)