Repository: elastic/kibana
Pull request #216376: [ResponseOps] ES|QL rules should generate a `context.link` that points to Discover
Author: login: doakalexi
Labels: name: release_note:enhancement
name: Team:ResponseOps
description: Platform ResponseOps team (formerly the Cases and Alerting teams) t//
name: backport:version
description: Backport to applied version labels
name: v9.1.0
name: v8.19.0
Created: 2025-03-28T19:36:09Z | Merged: 2025-04-07T21:59:56Z

Resolves https://github.com/elastic/kibana/issues/188856

## Summary

Currently ES query rule types link back to the alert details in the `context.link` variable, except for the search source type. This PR updates the rest of the ES query rule types to links to the relevant docs in Discover.


### Checklist


- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios

### To verify
1. Set `server.publicBaseUrl` in kibana.yml
2. Create an ES|QL rule and include `context.link` in your action message. Verify that when you go to the link it goes to Discover with the correct query and timestamp
3. Create a DSL ES query rule and include `context.link` in your action message. Verify that when you go to the link it goes to Discover and works as expected
4. For your DSL rule also add some filters and verify that when you go to Discover your filters are correctly applied.

<!--ONMERGE {"backportTargets":["8.x"]} ONMERGE-->


Diff:
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts
index f7565e147c173..e1e080803d75d 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.test.ts
@@ -153,7 +153,6 @@ describe('es_query executor', () => {
         name: 'test-rule-name',
         alertLimit: 1000,
         params: defaultProps,
-        publicBaseUrl: 'https://localhost:5601',
         spacePrefix: '',
         timestamp: undefined,
         services: {
@@ -228,7 +227,6 @@ describe('es_query executor', () => {
           share: undefined,
         },
         spacePrefix: '',
-        publicBaseUrl: 'https://localhost:5601',
         dateStart: new Date().toISOString(),
         dateEnd: new Date().toISOString(),
       });
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts
index 1f349e711c27d..94613a0090404 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/executor.ts
@@ -54,8 +54,7 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
     throw new AlertsClientError();
   }
   const currentTimestamp = new Date().toISOString();
-  const publicBaseUrl = core.http.basePath.publicBaseUrl ?? '';
-  const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : '';
+  const spacePrefix = spaceId !== 'default' ? spaceId : '';
   const alertLimit = alertsClient.getAlertLimitValue();
   const compareFn = ComparatorFns.get(params.thresholdComparator);
   if (compareFn == null) {
@@ -95,7 +94,6 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
         alertLimit,
         params: params as OnlyEsqlQueryRuleParams,
         spacePrefix,
-        publicBaseUrl,
         services: {
           share,
           scopedClusterClient,
@@ -110,9 +108,9 @@ export async function executor(core: CoreSetup, options: ExecutorOptions<EsQuery
         alertLimit,
         params: params as OnlyEsQueryRuleParams,
         timestamp: latestTimestamp,
-        publicBaseUrl,
         spacePrefix,
         services: {
+          share,
           scopedClusterClient,
           logger,
           ruleResultService,
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts
index d778046e5554b..5708929feb9d2 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.test.ts
@@ -7,11 +7,14 @@
 
 import type { OnlyEsQueryRuleParams } from '../types';
 import { Comparator } from '../../../../common/comparator_types';
-import { fetchEsQuery } from './fetch_es_query';
+import { fetchEsQuery, generateLink } from './fetch_es_query';
 import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
 import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
 import { loggerMock } from '@kbn/logging-mocks';
 import { publicRuleResultServiceMock } from '@kbn/alerting-plugin/server/monitoring/rule_result_service.mock';
+import type { SharePluginStart } from '@kbn/share-plugin/server';
+import type { LocatorPublic } from '@kbn/share-plugin/common';
+import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
 
 jest.mock('@kbn/triggers-actions-ui-plugin/common', () => {
   const actual = jest.requireActual('@kbn/triggers-actions-ui-plugin/common');
@@ -53,6 +56,16 @@ describe('fetchEsQuery', () => {
     jest.useRealTimers();
   });
   const services = {
+    // @ts-expect-error
+    share: {
+      url: {
+        locators: {
+          get: jest.fn().mockReturnValue({
+            getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'),
+          } as unknown as LocatorPublic<DiscoverAppLocatorParams>),
+        },
+      },
+    } as SharePluginStart,
     scopedClusterClient: scopedClusterClientMock,
     logger,
     ruleResultService: mockRuleResultService,
@@ -68,7 +81,6 @@ describe('fetchEsQuery', () => {
       timestamp: '2020-02-09T23:15:41.941Z',
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: date,
       dateEnd: date,
     });
@@ -160,7 +172,6 @@ describe('fetchEsQuery', () => {
       timestamp: undefined,
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: date,
       dateEnd: date,
     });
@@ -226,7 +237,6 @@ describe('fetchEsQuery', () => {
       timestamp: '2020-02-09T23:15:41.941Z',
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: date,
       dateEnd: date,
     });
@@ -292,7 +302,6 @@ describe('fetchEsQuery', () => {
       timestamp: undefined,
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: date,
       dateEnd: date,
     });
@@ -391,7 +400,6 @@ describe('fetchEsQuery', () => {
       timestamp: undefined,
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: date,
       dateEnd: date,
     });
@@ -515,7 +523,6 @@ describe('fetchEsQuery', () => {
       timestamp: '2020-02-09T23:15:41.941Z',
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: new Date().toISOString(),
       dateEnd: new Date().toISOString(),
     });
@@ -596,7 +603,6 @@ describe('fetchEsQuery', () => {
       timestamp: '2020-02-09T23:15:41.941Z',
       services,
       spacePrefix: '',
-      publicBaseUrl: '',
       dateStart: new Date().toISOString(),
       dateEnd: new Date().toISOString(),
     });
@@ -608,4 +614,91 @@ describe('fetchEsQuery', () => {
       `Top hits result window is too large, the top hits aggregator [topHitsAgg]'s from + size must be less than or equal to: [100] but was [300]. This limit can be set by changing the [index.max_inner_result_window] index level setting.`
     );
   });
+
+  it('should generate a link', () => {
+    const date = '2020-02-09T23:15:41.941Z';
+    const locatorMock = {
+      getRedirectUrl: jest.fn(() => 'space1/app/r?l=DISCOVER_APP_LOCATOR'),
+    } as unknown as LocatorPublic<DiscoverAppLocatorParams>;
+    const filter = {
+      bool: {
+        filter: [
+          { match_all: {} },
+          {
+            bool: {
+              must_not: [
+                {
+                  bool: {
+                    filter: [
+                      {
+                        range: {
+                          '@timestamp': {
+                            lte: date,
+                            format: 'strict_date_optional_time',
+                          },
+                        },
+                      },
+                    ],
+                  },
+                },
+              ],
+            },
+          },
+        ],
+      },
+    };
+
+    const link = generateLink(defaultParams, filter, locatorMock, date, date, 'space1');
+
+    expect(link).toBe('space1/app/r?l=DISCOVER_APP_LOCATOR');
+    expect(locatorMock.getRedirectUrl).toHaveBeenCalledWith(
+      {
+        dataViewSpec: {
+          id: 'es_query_rule_adhoc_data_view',
+          timeFieldName: '@timestamp',
+          title: 'test-index',
+        },
+        filters: [
+          {
+            $state: { store: 'appState' },
+            bool: {
+              filter: [
+                { match_all: {} },
+                {
+                  bool: {
+                    must_not: [
+                      {
+                        bool: {
+                          filter: [
+                            {
+                              range: {
+                                '@timestamp': {
+                                  format: 'strict_date_optional_time',
+                                  lte: '2020-02-09T23:15:41.941Z',
+                                },
+                              },
+                            },
+                          ],
+                        },
+                      },
+                    ],
+                  },
+                },
+              ],
+            },
+            meta: {
+              alias: 'Rule query DSL',
+              disabled: false,
+              index: 'es_query_rule_adhoc_data_view',
+              negate: false,
+              type: 'custom',
+            },
+          },
+        ],
+        isAlertResults: true,
+        timeRange: { from: '2020-02-09T23:15:41.941Z', to: '2020-02-09T23:15:41.941Z' },
+      },
+      { spaceId: 'space1' }
+    );
+  });
 });
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
index aeee5f2c5dfdd..87dbba432c327 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
@@ -14,6 +14,10 @@ import {
 import { isGroupAggregation } from '@kbn/triggers-actions-ui-plugin/common';
 import { ES_QUERY_ID } from '@kbn/rule-data-utils';
 import type { PublicRuleResultService } from '@kbn/alerting-plugin/server/types';
+import type { SharePluginStart } from '@kbn/share-plugin/server';
+import type { LocatorPublic } from '@kbn/share-plugin/common';
+import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
+import { FilterStateStore, buildCustomFilter } from '@kbn/es-query';
 import { getComparatorScript } from '../../../../common';
 import type { OnlyEsQueryRuleParams } from '../types';
 import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
@@ -24,9 +28,9 @@ export interface FetchEsQueryOpts {
   name: string;
   params: OnlyEsQueryRuleParams;
   timestamp: string | undefined;
-  publicBaseUrl: string;
   spacePrefix: string;
   services: {
+    share: SharePluginStart;
     scopedClusterClient: IScopedClusterClient;
     logger: Logger;
     ruleResultService?: PublicRuleResultService;
@@ -44,14 +48,14 @@ export async function fetchEsQuery({
   name,
   params,
   spacePrefix,
-  publicBaseUrl,
   timestamp,
   services,
   alertLimit,
   dateStart,
   dateEnd,
 }: FetchEsQueryOpts) {
-  const { scopedClusterClient, logger, ruleResultService } = services;
+  const { scopedClusterClient, logger, ruleResultService, share } = services;
+  const discoverLocator = share.url.locators.get<DiscoverAppLocatorParams>('DISCOVER_APP_LOCATOR')!;
   const esClient = scopedClusterClient.asCurrentUser;
   const isGroupAgg = isGroupAggregation(params.termField);
   const isCountAgg = isCountAggregation(params.aggType);
@@ -145,7 +149,7 @@ export async function fetchEsQuery({
     ruleResultService.setLastRunOutcomeMessage(anyShardFailures);
   }
 
-  const link = `${publicBaseUrl}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`;
+  const link = generateLink(params, filter, discoverLocator, dateStart, dateEnd, spacePrefix);
 
   return {
     parsedResults: parseAggregationResults({
@@ -160,3 +164,38 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    'Rule query DSL',
+    FilterStateStore.APP_STATE
+  );
+  const redirectUrlParams: DiscoverAppLocatorParams = {
+    dataViewSpec: {
+      id: dataViewId,
+      title: params.index.join(','),
+      timeFieldName: params.timeField,
+    },
+    filters: [filter],
+    timeRange: { from: dateStart, to: dateEnd },
+    isAlertResults: true,
+  };
+
+  // use `lzCompress` flag for making the link readable during debugging/testing
+  // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { spaceId: spacePrefix });
+
+  return redirectUrl;
+}
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts
index 74d5359291665..e13d1dd3c8529 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.test.ts
@@ -7,11 +7,13 @@
 
 import type { OnlyEsqlQueryRuleParams } from '../types';
 import { Comparator } from '../../../../common/comparator_types';
-import { fetchEsqlQuery, getEsqlQuery, getSourceFields } from './fetch_esql_query';
+import { fetchEsqlQuery, getEsqlQuery, getSourceFields, generateLink } from './fetch_esql_query';
 import { getErrorSource, TaskErrorSource } from '@kbn/task-manager-plugin/server/task_running';
 import type { SharePluginStart } from '@kbn/share-plugin/server';
 import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
 import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
+import type { LocatorPublic } from '@kbn/share-plugin/common';
+import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
 
 const getTimeRange = () => {
   const date = Date.now();
@@ -37,10 +39,18 @@ const defaultParams: OnlyEsqlQueryRuleParams = {
 const logger = loggingSystemMock.create().get();
 
 describe('fetchEsqlQuery', () => {
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  const fakeNow = new Date('2020-02-09T23:15:41.941Z');
+
+  beforeAll(() => {
+    jest.resetAllMocks();
+    global.Date.now = jest.fn(() => fakeNow.getTime());
+  });
+
   describe('fetch', () => {
-    afterEach(() => {
-      jest.resetAllMocks();
-    });
     it('should throw a user error when the error is a verification_exception error', async () => {
       const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
 
@@ -58,10 +68,18 @@ describe('fetchEsqlQuery', () => {
           services: {
             logger,
             scopedClusterClient,
-            share: {} as SharePluginStart,
+            // @ts-expect-error
+            share: {
+              url: {
+                locators: {
+                  get: jest.fn().mockReturnValue({
+                    getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'),
+                  } as unknown as LocatorPublic<DiscoverAppLocatorParams>),
+                },
+              },
+            } as SharePluginStart,
           },
           spacePrefix: '',
-          publicBaseUrl: '',
           dateStart: new Date().toISOString(),
           dateEnd: new Date().toISOString(),
         });
@@ -72,17 +90,6 @@ describe('fetchEsqlQuery', () => {
   });
 
   describe('getEsqlQuery', () => {
-    afterAll(() => {
-      jest.resetAllMocks();
-    });
-
-    const fakeNow = new Date('2020-02-09T23:15:41.941Z');
-
-    beforeAll(() => {
-      jest.resetAllMocks();
-      global.Date.now = jest.fn(() => fakeNow.getTime());
-    });
-
     it('should generate the correct query', async () => {
       const params = defaultParams;
       const { dateStart, dateEnd } = getTimeRange();
@@ -163,4 +170,25 @@ describe('fetchEsqlQuery', () => {
       `);
     });
   });
+
+  describe('generateLink', () => {
+    it('should generate a link', () => {
+      const { dateStart, dateEnd } = getTimeRange();
+      const locatorMock = {
+        getRedirectUrl: jest.fn(() => 'space1/app/r?l=DISCOVER_APP_LOCATOR'),
+      } as unknown as LocatorPublic<DiscoverAppLocatorParams>;
+
+      const link = generateLink(defaultParams, locatorMock, dateStart, dateEnd, 'space1');
+
+      expect(link).toBe('space1/app/r?l=DISCOVER_APP_LOCATOR');
+      expect(locatorMock.getRedirectUrl).toHaveBeenCalledWith(
+        {
+          isAlertResults: true,
+          query: { esql: 'from test' },
+          timeRange: { from: '2020-02-09T23:10:41.941Z', to: '2020-02-09T23:15:41.941Z' },
+        },
+        { spaceId: 'space1' }
+      );
+    });
+  });
 });
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts
index d7e97101fa890..412ee63e77901 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_esql_query.ts
@@ -13,8 +13,6 @@ import { ecsFieldMap, alertFieldMap } from '@kbn/alerts-as-data-utils';
 import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server';
 import type { LocatorPublic } from '@kbn/share-plugin/common';
 import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
-import type { DataViewsContract } from '@kbn/data-views-plugin/common';
-import type { Filter, Query } from '@kbn/es-query';
 import type { EsqlTable } from '../../../../common';
 import { toEsQueryHits } from '../../../../common';
 import type { OnlyEsqlQueryRuleParams } from '../types';
@@ -24,7 +22,6 @@ export interface FetchEsqlQueryOpts {
   alertLimit: number | undefined;
   params: OnlyEsqlQueryRuleParams;
   spacePrefix: string;
-  publicBaseUrl: string;
   services: {
     logger: Logger;
     scopedClusterClient: IScopedClusterClient;
@@ -40,11 +37,11 @@ export async function fetchEsqlQuery({
   params,
   services,
   spacePrefix,
-  publicBaseUrl,
   dateStart,
   dateEnd,
 }: FetchEsqlQueryOpts) {
-  const { logger, scopedClusterClient } = services;
+  const { logger, scopedClusterClient, share } = services;
+  const discoverLocator = share.url.locators.get<DiscoverAppLocatorParams>('DISCOVER_APP_LOCATOR')!;
   const esClient = scopedClusterClient.asCurrentUser;
   const query = getEsqlQuery(params, alertLimit, dateStart, dateEnd);
 
@@ -67,7 +64,7 @@ export async function fetchEsqlQuery({
   const hits = toEsQueryHits(response);
   const sourceFields = getSourceFields(response);
 
-  const link = `${publicBaseUrl}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}`;
+  const link = generateLink(params, discoverLocator, dateStart, dateEnd, spacePrefix);
 
   return {
     link,
@@ -132,30 +129,22 @@ export const getSourceFields = (results: EsqlTable) => {
   return intersectionBy(resultFields, ecsFields, 'label');
 };
 
-export async function generateLink(
-  esqlQuery: Query,
+export function generateLink(
+  params: OnlyEsqlQueryRuleParams,
   discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
-  dataViews: DataViewsContract,
-  dataViewToUpdate: DataView,
   dateStart: string,
   dateEnd: string,
-  spacePrefix: string,
-  filterToExcludeHitsFromPreviousRun: Filter | null
+  spacePrefix: string
 ) {
   const redirectUrlParams: DiscoverAppLocatorParams = {
-    filters: filterToExcludeHitsFromPreviousRun ? [filterToExcludeHitsFromPreviousRun] : [],
     timeRange: { from: dateStart, to: dateEnd },
+    query: params.esqlQuery,
     isAlertResults: true,
-    query: {
-      language: 'esql',
-      query: esqlQuery,
-    },
   };
 
   // use `lzCompress` flag for making the link readable during debugging/testing
   // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
-  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams);
-  const [start, end] = redirectUrl.split('/app');
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { spaceId: spacePrefix });
 
-  return start + spacePrefix + '/app' + end;
+  return redirectUrl;
 }
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
index 7af704d8e87af..d1402109e5fa9 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
@@ -720,7 +720,7 @@ describe('fetchSearchSourceQuery', () => {
       `);
 
       const locatorMock = {
-        getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'),
+        getRedirectUrl: jest.fn(() => 'test1/app/r?l=DISCOVER_APP_LOCATOR'),
       } as unknown as LocatorPublic<DiscoverAppLocatorParams>;
 
       const dataViews = {
@@ -744,7 +744,8 @@ describe('fetchSearchSourceQuery', () => {
       expect(locatorMock.getRedirectUrl).toHaveBeenCalledWith(
         expect.objectContaining({
           filters: [],
-        })
+        }),
+        { spaceId: 'test1' }
       );
 
       const linkWithExcludedRuns = await generateLink(
@@ -754,11 +755,11 @@ describe('fetchSearchSourceQuery', () => {
         dataViewMock,
         dateStart,
         dateEnd,
-        'test2',
+        'test1',
         filterToExcludeHitsFromPreviousRun
       );
 
-      expect(linkWithExcludedRuns).toBe('test2/app/r?l=DISCOVER_APP_LOCATOR');
+      expect(linkWithExcludedRuns).toBe('test1/app/r?l=DISCOVER_APP_LOCATOR');
       expect(locatorMock.getRedirectUrl).toHaveBeenNthCalledWith(
         2,
         expect.objectContaining({
@@ -769,7 +770,8 @@ describe('fetchSearchSourceQuery', () => {
               undefined
             )
           ),
-        })
+        }),
+        { spaceId: 'test1' }
       );
     });
 
@@ -779,7 +781,7 @@ describe('fetchSearchSourceQuery', () => {
       const { dateStart, dateEnd } = getTimeRange();
 
       const locatorMock = {
-        getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'),
+        getRedirectUrl: jest.fn(() => 'test1/app/r?l=DISCOVER_APP_LOCATOR'),
       } as unknown as LocatorPublic<DiscoverAppLocatorParams>;
 
       const dataViews = {
diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
index bb5879820336f..89d9777b06392 100644
--- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
+++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
@@ -248,10 +248,9 @@ export async function generateLink(
 
   // use `lzCompress` flag for making the link readable during debugging/testing
   // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
-  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams);
-  const [start, end] = redirectUrl.split('/app');
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { spaceId: spacePrefix });
 
-  return start + spacePrefix + '/app' + end;
+  return redirectUrl;
 }
 
 export function getSmallerDataViewSpec(
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts
index b72beabd12231..04b091d7db399 100644
--- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts
@@ -280,6 +280,7 @@ export default function ({ getService }: FtrProviderContext) {
         'kibana.alert.consecutive_matches',
         'kibana.alert.severity_improving',
         'kibana.alert.previous_action_group',
+        'kibana.alert.url',
       ];
 
       for (const field of fields) {


Reviews:
user: login: davismcphee
body: Everything looks good from the Discover side, tested locally and it's working well. Thanks for picking this up and glad to see it being added!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T22:00:44Z
user: login: doakalexi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T22:19:51Z
user: login: pmuellr
body: Having finished reviewing this yet. I want to try it out, but figured I'd leave the comment about the split on `/app` ...
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T20:28:17Z
user: login: doakalexi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T20:34:44Z
user: login: doakalexi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T20:38:35Z
user: login: doakalexi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T21:05:00Z
user: login: pmuellr
body: LGTM.  Tested in a default and non-default space, seems to generate the appropriate links to discover.  Nice!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T19:54:38Z

Review comments:
diff_hunk: @@ -160,3 +164,39 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    null,
path: x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
user: login: davismcphee
body: Nit: you could wind up with _really_ long filters in Discover without a custom alias, but not a strong opinion.
![CleanShot 2025-04-01 at 18 58 44@2x](https://github.com/user-attachments/assets/f1ee4738-68d9-4a94-a892-ab200edb00df)
created_at: 2025-04-01T21:59:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,3 +164,39 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    null,
path: x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
user: login: doakalexi
body: That's a good point. I added the alias in this commit, thanks! https://github.com/elastic/kibana/pull/216376/commits/43164265872b7fa07f83e2b05142683dc3563f78
created_at: 2025-04-01T22:19:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,3 +164,39 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    'Rule query DSL',
+    FilterStateStore.APP_STATE
+  );
+  const redirectUrlParams: DiscoverAppLocatorParams = {
+    dataViewSpec: {
+      id: dataViewId,
+      title: params.index.join(','),
+      timeFieldName: params.timeField,
+    },
+    filters: [filter],
+    timeRange: { from: dateStart, to: dateEnd },
+    isAlertResults: true,
+  };
+
+  // use `lzCompress` flag for making the link readable during debugging/testing
+  // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams);
+  const [start, end] = redirectUrl.split('/app');
path: x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
user: login: pmuellr
body: Could this break if the path in their publicBaseURL has `/app` in it somewhere (for example, using some proxy in front of Kibana, like we do in dev).  Or if it's in a non-default space that starts with `app` (for example space "appXYZ" would have a URL like `host:port/s/appXYZ/app/...`
created_at: 2025-04-02T20:25:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,3 +164,39 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    'Rule query DSL',
+    FilterStateStore.APP_STATE
+  );
+  const redirectUrlParams: DiscoverAppLocatorParams = {
+    dataViewSpec: {
+      id: dataViewId,
+      title: params.index.join(','),
+      timeFieldName: params.timeField,
+    },
+    filters: [filter],
+    timeRange: { from: dateStart, to: dateEnd },
+    isAlertResults: true,
+  };
+
+  // use `lzCompress` flag for making the link readable during debugging/testing
+  // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams);
+  const [start, end] = redirectUrl.split('/app');
path: x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
user: login: doakalexi
body: Oh true, that might break something if there is `app` somewhere in the url. Do you think it's safer to split on `/app/`?
created_at: 2025-04-02T20:34:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,3 +164,39 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    'Rule query DSL',
+    FilterStateStore.APP_STATE
+  );
+  const redirectUrlParams: DiscoverAppLocatorParams = {
+    dataViewSpec: {
+      id: dataViewId,
+      title: params.index.join(','),
+      timeFieldName: params.timeField,
+    },
+    filters: [filter],
+    timeRange: { from: dateStart, to: dateEnd },
+    isAlertResults: true,
+  };
+
+  // use `lzCompress` flag for making the link readable during debugging/testing
+  // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams);
+  const [start, end] = redirectUrl.split('/app');
path: x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
user: login: doakalexi
body: Oh you can add the `spaceId` as an option for `getRedirectUrl`, and then we won't need split
created_at: 2025-04-02T20:38:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,3 +164,39 @@ export async function fetchEsQuery({
     index: params.index,
   };
 }
+
+export function generateLink(
+  params: OnlyEsQueryRuleParams,
+  rawFilter: Record<string, unknown>,
+  discoverLocator: LocatorPublic<DiscoverAppLocatorParams>,
+  dateStart: string,
+  dateEnd: string,
+  spacePrefix: string
+) {
+  const dataViewId = 'es_query_rule_adhoc_data_view';
+  const filter = buildCustomFilter(
+    dataViewId,
+    rawFilter,
+    false,
+    false,
+    'Rule query DSL',
+    FilterStateStore.APP_STATE
+  );
+  const redirectUrlParams: DiscoverAppLocatorParams = {
+    dataViewSpec: {
+      id: dataViewId,
+      title: params.index.join(','),
+      timeFieldName: params.timeField,
+    },
+    filters: [filter],
+    timeRange: { from: dateStart, to: dateEnd },
+    isAlertResults: true,
+  };
+
+  // use `lzCompress` flag for making the link readable during debugging/testing
+  // const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams, { lzCompress: false });
+  const redirectUrl = discoverLocator!.getRedirectUrl(redirectUrlParams);
+  const [start, end] = redirectUrl.split('/app');
path: x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_es_query.ts
user: login: doakalexi
body: Updated in this commit, [ceaee64](https://github.com/elastic/kibana/pull/216376/commits/ceaee649316bd9847168e41d8591f8699b1406c4)
created_at: 2025-04-02T21:04:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-03-31T21:28:07Z
body: Pinging @elastic/response-ops (Team:ResponseOps)
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-04-04T17:23:07Z
body: ## :yellow_heart: Build succeeded, but was flaky
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/290367)
* Commit: 38876e42d91b3357ebde27523f7d612102fe9ae3

### Failed CI Steps
* [Jest Tests #6](https://buildkite.com/elastic/kibana-pull-request/builds/290367#01960186-17d6-42ee-ba37-412f223acc10)

### Test Failures
* [[job]](https://buildkite.com/elastic/kibana-pull-request/builds/290367#01960186-17d6-42ee-ba37-412f223acc10) [[logs]](https://buildkite.com/organizations/elastic/pipelines/kibana-pull-request/builds/290367/jobs/01960186-17d6-42ee-ba37-412f223acc10/artifacts/019601ab-7233-48c5-8f50-f592059dd952) Jest Tests #<span></span>6 / Templates renders empty templates correctly

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
> ✅ unchanged

### History
* :green_heart: [Build #289706](https://buildkite.com/elastic/kibana-pull-request/builds/289706) succeeded ca6d5a263e7b30ac40fd786c4dd093aeab992288
* :yellow_heart: [Build #289300](https://buildkite.com/elastic/kibana-pull-request/builds/289300) was flaky f3e5e8898ab7181d44e8057fea3f28ef5ee3fd58
* :broken_heart: [Build #289282](https://buildkite.com/elastic/kibana-pull-request/builds/289282) failed 11b95145ef194fd067c91900337dcdefec7ced7f
* :green_heart: [Build #288254](https://buildkite.com/elastic/kibana-pull-request/builds/288254) succeeded 94796f23a3593062284d37a8c1766644c4790e03
* :broken_heart: [Build #288229](https://buildkite.com/elastic/kibana-pull-request/builds/288229) failed ec825d028cb70893303c34862a7db3a7e939a9b7

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/290367","number":290367,"commit":"38876e42d91b3357ebde27523f7d612102fe9ae3"},{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/289706","number":289706,"commit":"ca6d5a263e7b30ac40fd786c4dd093aeab992288"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/289300","number":289300,"commit":"f3e5e8898ab7181d44e8057fea3f28ef5ee3fd58"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/289282","number":289282,"commit":"11b95145ef194fd067c91900337dcdefec7ced7f"},{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/288254","number":288254,"commit":"94796f23a3593062284d37a8c1766644c4790e03"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/288229","number":288229,"commit":"ec825d028cb70893303c34862a7db3a7e939a9b7"}],"number":290367}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-07T22:00:14Z
body: Starting backport for target branches: 8.x

https://github.com/elastic/kibana/actions/runs/14320322160
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-07T22:08:41Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/217411">](https://github.com/elastic/kibana/pull/217411)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR