[Query Rules] Query rules telemetry (#225146)

## Summary

Adds telemetry to query rules UIs


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ] [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
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Efe Gürkan YALAMAN 2025-06-25 19:14:04 +02:00 committed by GitHub
parent 1ebff0e634
commit cae3861f5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 177 additions and 24 deletions

View file

@ -19,6 +19,7 @@
"console",
"searchNavigation",
"share",
"usageCollection",
],
"requiredBundles": [
"kibanaReact",

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
export enum AnalyticsEvents {
// Empty promp actions
gettingStartedButtonClicked = 'getting_started_button_clicked',
createInConsoleClicked = 'create_in_console_clicked',
emptyPromptLoaded = 'empty_prompt_loaded',
// Error prompt loaded
genericErrorPromptLoaded = 'generic_error_prompt_loaded',
notFoundErrorPromptLoaded = 'not_found_error_prompt_loaded',
missingPermissionsErrorPromptLoaded = 'missing_permissions_error_prompt_loaded',
rulesetCreateClicked = 'ruleset_create_clicked',
rulesetUpdateClicked = 'ruleset_update_clicked',
// ruleset detail page actions
rulesetDetailPageLoaded = 'ruleset_details_page_loaded',
addRuleClicked = 'add_rule_clicked',
editRuleClicked = 'edit_rule_clicked',
deleteRuleClicked = 'delete_rule_clicked',
testInConsoleClicked = 'test_in_console_clicked',
deleteRulesetFromHeaderClicked = 'delete_ruleset_from_header_clicked',
backToRulesetListClicked = 'back_to_ruleset_list_clicked',
rulesReordered = 'rules_reordered',
ruleFlyoutDocumentsReordered = 'rule_flyout_documents_reordered',
// ruleset list page actions
editRulesetInlineNameClicked = 'edit_ruleset_inline_name_clicked',
editRulesetInlineDropdownClicked = 'edit_ruleset_inline_dropdown_clicked',
rulesetListPageLoaded = 'ruleset_list_page_loaded',
deleteRulesetInlineDropdownClicked = 'delete_ruleset_inline_dropdown_clicked',
testRulesetInlineDropdownClicked = 'test_ruleset_inline_dropdown_clicked',
rulesetSearched = 'ruleset_searched',
addRulesetClicked = 'add_ruleset_clicked',
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import {
EuiButton,
@ -35,14 +35,22 @@ import { useKibana } from '../../hooks/use_kibana';
import queryRulesImg from '../../assets/query-rules-context-alt.svg';
import queryRulesDarkImg from '../../assets/query-rules-context-alt-dark.svg';
import backgroundPanelImg from '../../assets/query-rule-panel-background.svg';
import { AnalyticsEvents } from '../../analytics/constants';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import backgroundPaneDarklImg from '../../assets/query-rule-panel-background-dark.svg';
interface EmptyPromptProps {
getStartedAction: () => void;
}
export const EmptyPrompt: React.FC<EmptyPromptProps> = ({ getStartedAction }) => {
const usageTracker = useUsageTracker();
const { application, share, console: consolePlugin } = useKibana().services;
const { euiTheme, colorMode } = useEuiTheme();
useEffect(() => {
usageTracker?.load(AnalyticsEvents.emptyPromptLoaded);
}, [usageTracker]);
const gradientOverlay = css({
background: `linear-gradient(180deg, ${transparentize(
euiTheme.colors.backgroundBasePlain,
@ -101,7 +109,10 @@ export const EmptyPrompt: React.FC<EmptyPromptProps> = ({ getStartedAction }) =>
data-test-subj="searchQueryRulesEmptyPromptGetStartedButton"
color="primary"
fill
onClick={getStartedAction}
onClick={() => {
usageTracker?.click(AnalyticsEvents.gettingStartedButtonClicked);
getStartedAction();
}}
>
<FormattedMessage
id="xpack.queryRules.emptyPrompt.getStartedButton"
@ -240,6 +251,7 @@ export const EmptyPrompt: React.FC<EmptyPromptProps> = ({ getStartedAction }) =>
defaultMessage: 'Create in Console',
})}
showIcon
data-test-subj={AnalyticsEvents.createInConsoleClicked}
/>
</EuiFlexItem>
</EuiHideFor>

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { AnalyticsEvents } from '../../analytics/constants';
const ERROR_MESSAGES = {
notFound: {
@ -19,6 +21,7 @@ const ERROR_MESSAGES = {
defaultMessage="Requested resource was not found. Check if the URL is correct."
/>
),
analyticsEvent: AnalyticsEvents.notFoundErrorPromptLoaded,
},
generic: {
title: <FormattedMessage id="xpack.queryRules.errorTitle" defaultMessage="An error occurred" />,
@ -28,6 +31,7 @@ const ERROR_MESSAGES = {
defaultMessage="An error occurred while fetching query rules. Check Kibana logs for more information."
/>
),
analyticsEvent: AnalyticsEvents.genericErrorPromptLoaded,
},
missingPermissions: {
title: (
@ -42,12 +46,17 @@ const ERROR_MESSAGES = {
defaultMessage="You do not have the necessary permissions to manage query rules. Contact your system administrator."
/>
),
analyticsEvent: AnalyticsEvents.missingPermissionsErrorPromptLoaded,
},
};
export const ErrorPrompt: React.FC<{
errorType: 'missingPermissions' | 'generic' | 'notFound';
}> = ({ errorType }) => {
const useTracker = useUsageTracker();
useEffect(() => {
useTracker?.load?.(ERROR_MESSAGES[errorType].analyticsEvent);
}, [errorType, useTracker]);
return (
<EuiEmptyPrompt
iconType="logoElasticsearch"

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import {
@ -31,24 +31,16 @@ import { QueryRulesSets } from '../query_rules_sets/query_rules_sets';
import { CreateRulesetModal } from './create_ruleset_modal';
import { QueryRulesPageTemplate } from '../../layout/query_rules_page_template';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { AnalyticsEvents } from '../../analytics/constants';
export const QueryRulesOverview = () => {
const usageTracker = useUsageTracker();
const { colorMode } = useEuiTheme();
const {
data: queryRulesData,
isInitialLoading,
isError,
error,
refetch,
} = useFetchQueryRulesSets();
useEffect(() => {
const interval = setInterval(() => {
refetch();
}, 1000);
return () => clearInterval(interval);
}, [refetch]);
const { data: queryRulesData, isInitialLoading, isError, error } = useFetchQueryRulesSets();
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const backgroundProps = css({
backgroundImage: `url(${
colorMode === 'DARK' ? queryRulesBackgroundDark : queryRulesBackground
@ -61,6 +53,7 @@ export const QueryRulesOverview = () => {
alignContent: 'center',
backgroundPosition: 'center center',
});
return (
<QueryRulesPageTemplate restrictWidth={false}>
{!isInitialLoading && !isError && queryRulesData?._meta.totalItemCount !== 0 && (
@ -95,6 +88,7 @@ export const QueryRulesOverview = () => {
fill
iconType="plusInCircle"
onClick={() => {
usageTracker?.click(AnalyticsEvents.addRulesetClicked);
setIsCreateModalVisible(true);
}}
>
@ -143,6 +137,7 @@ export const QueryRulesOverview = () => {
<EuiFlexItem>
<EmptyPrompt
getStartedAction={() => {
usageTracker?.click(AnalyticsEvents.gettingStartedButtonClicked);
setIsCreateModalVisible(true);
}}
/>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { QueryRulesListRulesetsQueryRulesetListItem } from '@elastic/elasticsearch/lib/api/types';
import {
@ -24,8 +24,11 @@ import { useQueryRulesSetsTableData } from '../../hooks/use_query_rules_sets_tab
import { QueryRulesSetsSearch } from './query_rules_sets_search';
import { DeleteRulesetModal } from './delete_ruleset_modal';
import { UseRunQueryRuleset } from '../../hooks/use_run_query_ruleset';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { AnalyticsEvents } from '../../analytics/constants';
export const QueryRulesSets = () => {
const useTracker = useUsageTracker();
const {
services: { application, http },
} = useKibana();
@ -43,6 +46,10 @@ export const QueryRulesSets = () => {
pageSize
);
useEffect(() => {
useTracker?.load?.(AnalyticsEvents.rulesetListPageLoaded);
}, [useTracker]);
if (!queryRulesData) {
return null;
}
@ -57,6 +64,7 @@ export const QueryRulesSets = () => {
<EuiLink
data-test-subj="queryRuleSetName"
onClick={() => {
useTracker?.click?.(AnalyticsEvents.editRulesetInlineNameClicked);
application.navigateToUrl(
http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}/ruleset/${name}`)
);
@ -86,6 +94,9 @@ export const QueryRulesSets = () => {
content={i18n.translate('xpack.queryRules.queryRulesSetTable.actions.run', {
defaultMessage: 'Test in Console',
})}
onClick={() => {
useTracker?.click?.(AnalyticsEvents.testRulesetInlineDropdownClicked);
}}
/>
);
},
@ -102,10 +113,12 @@ export const QueryRulesSets = () => {
icon: 'pencil',
color: 'text',
type: 'icon',
onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) =>
onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) => {
useTracker?.click?.(AnalyticsEvents.editRulesetInlineDropdownClicked);
application.navigateToUrl(
http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}/ruleset/${ruleset.ruleset_id}`)
),
);
},
},
{
name: i18n.translate('xpack.queryRules.queryRulesSetTable.actions.delete', {
@ -121,6 +134,7 @@ export const QueryRulesSets = () => {
type: 'icon',
isPrimary: true,
onClick: (ruleset: QueryRulesListRulesetsQueryRulesetListItem) => {
useTracker?.click?.(AnalyticsEvents.deleteRulesetInlineDropdownClicked);
setRulesetToDelete(ruleset.ruleset_id);
},
},

View file

@ -7,6 +7,8 @@
import { EuiFieldSearch } from '@elastic/eui';
import React, { useCallback } from 'react';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { AnalyticsEvents } from '../../analytics/constants';
interface QueryRulesSetsSearchProps {
searchKey: string;
@ -17,12 +19,15 @@ export const QueryRulesSetsSearch: React.FC<QueryRulesSetsSearchProps> = ({
searchKey,
setSearchKey,
}) => {
const useTracker = useUsageTracker();
const onSearch = useCallback(
(newSearch: string) => {
useTracker?.load?.(AnalyticsEvents.rulesetSearched);
const trimSearch = newSearch.trim();
setSearchKey(trimSearch);
},
[setSearchKey]
[setSearchKey, useTracker]
);
return (

View file

@ -16,6 +16,8 @@ import { QueryRuleFlyout } from './query_rule_flyout/query_rule_flyout';
import { useGenerateRuleId } from '../../hooks/use_generate_rule_id';
import { SearchQueryRulesQueryRule } from '../../types';
import { RulesetDetailEmptyPrompt } from '../empty_prompt/ruleset_detail_empty_prompt';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { AnalyticsEvents } from '../../analytics/constants';
interface QueryRuleDetailPanelProps {
rules: SearchQueryRulesQueryRule[];
@ -46,6 +48,8 @@ export const QueryRuleDetailPanel: React.FC<QueryRuleDetailPanelProps> = ({
const [ruleIdToEdit, setRuleIdToEdit] = React.useState<string | null>(null);
const [flyoutMode, setFlyoutMode] = React.useState<'create' | 'edit'>('edit');
const useTracker = useUsageTracker();
const { mutate: generateRuleId } = useGenerateRuleId(rulesetId);
useEffect(() => {
if (createMode && rules.length === 0) {
@ -94,6 +98,7 @@ export const QueryRuleDetailPanel: React.FC<QueryRuleDetailPanelProps> = ({
color="primary"
data-test-subj="queryRulesetDetailAddRuleButton"
onClick={() => {
useTracker?.click(AnalyticsEvents.addRuleClicked);
generateRuleId(undefined, {
onSuccess: (newRuleId) => {
setFlyoutMode('create');

View file

@ -27,12 +27,14 @@ import { css } from '@emotion/react';
import { QueryRulesQueryRule } from '@elastic/elasticsearch/lib/api/types';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useUsageTracker } from '../../../hooks/use_usage_tracker';
import { SearchQueryRulesQueryRule } from '../../../../common/types';
import { DroppableContainer } from '../styles';
import { QueryRuleDraggableListHeader } from './query_rule_draggable_list_header';
import { QueryRuleDraggableListItemActionTypeBadge } from './query_rule_draggable_item_action_type_badge';
import { QueryRuleDraggableItemCriteriaDisplay } from './query_rule_draggable_item_criteria_display';
import { DeleteRulesetRuleModal } from './delete_ruleset_rule_modal';
import { AnalyticsEvents } from '../../../analytics/constants';
export interface QueryRuleDraggableListItemProps {
rules: SearchQueryRulesQueryRule[];
@ -60,6 +62,7 @@ export const QueryRuleDraggableListItem: React.FC<QueryRuleDraggableListItemProp
isLastItem = false,
}) => {
const { euiTheme } = useEuiTheme();
const useTracker = useUsageTracker();
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const localTourTargetRef = useRef<HTMLDivElement>(null);
const effectiveRef = tourInfo?.tourTargetRef || localTourTargetRef;
@ -174,6 +177,7 @@ export const QueryRuleDraggableListItem: React.FC<QueryRuleDraggableListItemProp
icon="pencil"
data-test-subj="searchQueryRulesQueryRulesetDetailEditButton"
onClick={() => {
useTracker?.click(AnalyticsEvents.editRuleClicked);
onEditRuleFlyoutOpen(queryRule.rule_id);
closePopover();
}}
@ -204,6 +208,7 @@ export const QueryRuleDraggableListItem: React.FC<QueryRuleDraggableListItemProp
`}
data-test-subj="searchQueryRulesQueryRulesetDetailDeleteButton"
onClick={() => {
useTracker?.click(AnalyticsEvents.deleteRuleClicked);
setRuleToDelete(queryRule.rule_id);
closePopover();
}}
@ -263,11 +268,13 @@ export const QueryRuleDraggableList: React.FC<QueryRuleDraggableListProps> = ({
tourInfo,
}) => {
const { euiTheme } = useEuiTheme();
const useTracker = useUsageTracker();
return (
<EuiDragDropContext
onDragEnd={({ source, destination }) => {
if (source && destination) {
useTracker?.click(AnalyticsEvents.rulesReordered);
const items = euiDragDropReorder(rules, source.index, destination.index);
onReorder(items);
}

View file

@ -10,9 +10,11 @@ import { useEffect, useState } from 'react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { euiDragDropReorder } from '@elastic/eui';
import { AnalyticsEvents } from '../../../analytics/constants';
import { QueryRuleEditorForm, SearchQueryRulesQueryRule } from '../../../../common/types';
import { useFetchIndexNames } from '../../../hooks/use_fetch_index_names';
import { isCriteriaAlways } from '../../../utils/query_rules_utils';
import { useUsageTracker } from '../../../hooks/use_usage_tracker';
export const createEmptyRuleset = (
rulesetId: QueryRulesQueryRuleset['ruleset_id']
@ -39,6 +41,7 @@ export const useQueryRuleFlyoutState = ({
setIsFormDirty,
onSave,
}: UseQueryRuleFlyoutStateProps) => {
const usageTracker = useUsageTracker();
const { control, getValues, reset, setValue, formState, trigger } =
useFormContext<QueryRuleEditorForm>();
const {
@ -242,6 +245,7 @@ export const useQueryRuleFlyoutState = ({
const shouldShowCriteriaCallout = criteriaCalloutActive && !isAlways;
const dragEndHandle: OnDragEndResponder<string> = ({ source, destination }) => {
usageTracker?.click(AnalyticsEvents.ruleFlyoutDocumentsReordered);
if (source && destination && (ruleFromRuleset || createMode)) {
if (isDocRule) {
const newActions = euiDragDropReorder(actionFields, source.index, destination.index);

View file

@ -40,6 +40,8 @@ import { DeleteRulesetModal } from '../query_rules_sets/delete_ruleset_modal';
import { QueryRuleDetailPanel } from './query_rule_detail_panel';
import { useQueryRulesetDetailState } from './use_query_ruleset_detail_state';
import { useFetchQueryRulesetExist } from '../../hooks/use_fetch_ruleset_exists';
import { AnalyticsEvents } from '../../analytics/constants';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
export interface QueryRulesetDetailProps {
createMode?: boolean;
@ -55,6 +57,7 @@ export const QueryRulesetDetail: React.FC<QueryRulesetDetailProps> = ({ createMo
}>();
const { data: rulesetExists, isLoading: isFailsafeLoading } =
useFetchQueryRulesetExist(rulesetId);
const useTracker = useUsageTracker();
useEffect(() => {
// This is a failsafe in case user navigates to an existing ruleset via URL directly
@ -63,6 +66,10 @@ export const QueryRulesetDetail: React.FC<QueryRulesetDetailProps> = ({ createMo
}
}, [createMode, rulesetExists, application, http.basePath, rulesetId]);
useEffect(() => {
useTracker?.load?.(AnalyticsEvents.rulesetDetailPageLoaded);
}, [useTracker]);
const blockRender = (createMode && rulesetExists) || isFailsafeLoading;
const { mutate: createRuleset } = usePutRuleset(() => {
@ -186,6 +193,9 @@ export const QueryRulesetDetail: React.FC<QueryRulesetDetailProps> = ({ createMo
const handleSave = () => {
setIsFormDirty(false);
useTracker?.click(
createMode ? AnalyticsEvents.rulesetCreateClicked : AnalyticsEvents.rulesetUpdateClicked
);
createRuleset({
rulesetId,
forceWrite: true,
@ -232,8 +242,10 @@ export const QueryRulesetDetail: React.FC<QueryRulesetDetailProps> = ({ createMo
),
color: 'primary',
'aria-current': false,
onClick: () =>
application.navigateToUrl(http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}`)),
onClick: () => {
useTracker?.click(AnalyticsEvents.backToRulesetListClicked);
application.navigateToUrl(http.basePath.prepend(`${PLUGIN_ROUTE_ROOT}`));
},
},
]}
restrictWidth
@ -331,7 +343,10 @@ export const QueryRulesetDetail: React.FC<QueryRulesetDetailProps> = ({ createMo
content={i18n.translate('xpack.queryRules.queryRulesetDetail.testButton', {
defaultMessage: 'Test in Console',
})}
onClick={finishTour}
onClick={() => {
useTracker?.click(AnalyticsEvents.testInConsoleClicked);
finishTour();
}}
/>
</EuiTourStep>
</EuiFlexItem>

View file

@ -0,0 +1,40 @@
/*
* 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 { useMemo } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import { useKibana } from './use_kibana';
const APP_TRACKER_NAME = 'search_query_rules';
export const useUsageTracker = () => {
const {
services: { usageCollection },
} = useKibana();
return useMemo(() => {
if (usageCollection) {
return {
click: usageCollection.reportUiCounter.bind(
usageCollection,
APP_TRACKER_NAME,
METRIC_TYPE.CLICK
),
count: usageCollection.reportUiCounter.bind(
usageCollection,
APP_TRACKER_NAME,
METRIC_TYPE.COUNT
),
load: usageCollection.reportUiCounter.bind(
usageCollection,
APP_TRACKER_NAME,
METRIC_TYPE.LOADED
),
};
}
}, [usageCollection]);
};

View file

@ -9,6 +9,7 @@ import { SearchNavigationPluginStart } from '@kbn/search-navigation/public';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import type { ConsolePluginStart } from '@kbn/console-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
export * from '../common/types';
export interface AppPluginStartDependencies {
@ -16,6 +17,7 @@ export interface AppPluginStartDependencies {
console?: ConsolePluginStart;
share?: SharePluginStart;
searchNavigation?: SearchNavigationPluginStart;
usageCollection?: UsageCollectionStart;
}
export type AppServicesContext = CoreStart & AppPluginStartDependencies;

View file

@ -30,6 +30,8 @@
"@kbn/search-index-documents",
"@kbn/unsaved-changes-prompt",
"@kbn/deeplinks-analytics",
"@kbn/analytics",
"@kbn/usage-collection-plugin",
],
"exclude": [
"target/**/*",