From 183a42b7f6ce44853ea1b48733e8b6a54b0f0a0f Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 28 Feb 2024 16:52:56 +0100 Subject: [PATCH] Alerting: Improve alert rule and search interaction tracking (#83217) * Fix alert rule interaction tracking * Add search component interaction tracking * Add fine-grained search input analytics --- .../features/alerting/unified/Analytics.ts | 67 ++++++++++++++----- .../alert-rule-form/AlertRuleForm.tsx | 28 +++++--- .../unified/components/rules/RulesFilter.tsx | 23 +++++-- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 012bbd988e0..049243309a2 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -1,3 +1,5 @@ +import { isEmpty } from 'lodash'; + import { dateTime } from '@grafana/data'; import { createMonitoringLogger, getBackendSrv } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime/src'; @@ -6,6 +8,9 @@ import { contextSrv } from 'app/core/core'; import { RuleNamespace } from '../../../types/unified-alerting'; import { RulerRulesConfigDTO } from '../../../types/unified-alerting-dto'; +import { getSearchFilterFromQuery, RulesFilter } from './search/rulesSearchParser'; +import { RuleFormType } from './types/rule-form'; + export const USER_CREATION_MIN_DAYS = 7; export const LogMessages = { @@ -150,27 +155,17 @@ export const trackRuleListNavigation = async ( reportInteraction('grafana_alerting_navigation', props); }; -export const trackNewAlerRuleFormSaved = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(); - if (isNew) { - return; - } +export const trackAlertRuleFormSaved = (props: { formAction: 'create' | 'update'; ruleType?: RuleFormType }) => { reportInteraction('grafana_alerting_rule_creation', props); }; -export const trackNewAlerRuleFormCancelled = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(); - if (isNew) { - return; - } +export const trackAlertRuleFormCancelled = (props: { formAction: 'create' | 'update' }) => { reportInteraction('grafana_alerting_rule_aborted', props); }; -export const trackNewAlerRuleFormError = async (props: AlertRuleTrackingProps & { error: string }) => { - const isNew = await isNewUser(); - if (isNew) { - return; - } +export const trackAlertRuleFormError = ( + props: AlertRuleTrackingProps & { error: string; formAction: 'create' | 'update' } +) => { reportInteraction('grafana_alerting_rule_form_error', props); }; @@ -183,6 +178,48 @@ export const trackInsightsFeedback = async (props: { useful: boolean; panel: str reportInteraction('grafana_alerting_insights', { ...defaults, ...props }); }; +interface RulesSearchInteractionPayload { + filter: string; + triggeredBy: 'typing' | 'component'; +} + +function trackRulesSearchInteraction(payload: RulesSearchInteractionPayload) { + reportInteraction('grafana_alerting_rules_search', { ...payload }); +} + +export function trackRulesSearchInputInteraction({ oldQuery, newQuery }: { oldQuery: string; newQuery: string }) { + try { + const oldFilter = getSearchFilterFromQuery(oldQuery); + const newFilter = getSearchFilterFromQuery(newQuery); + + const oldFilterTerms = extractFilterKeys(oldFilter); + const newFilterTerms = extractFilterKeys(newFilter); + + const newTerms = newFilterTerms.filter((term) => !oldFilterTerms.includes(term)); + newTerms.forEach((term) => { + trackRulesSearchInteraction({ filter: term, triggeredBy: 'typing' }); + }); + } catch (e: unknown) { + if (e instanceof Error) { + logError(e); + } + } +} + +function extractFilterKeys(filter: RulesFilter) { + return Object.entries(filter) + .filter(([_, value]) => !isEmpty(value)) + .map(([key]) => key); +} + +export function trackRulesSearchComponentInteraction(filter: keyof RulesFilter) { + trackRulesSearchInteraction({ filter, triggeredBy: 'component' }); +} + +export function trackRulesListViewChange(payload: { view: string }) { + reportInteraction('grafana_alerting_rules_list_mode', { ...payload }); +} + export type AlertRuleTrackingProps = { user_id: number; grafana_version?: string; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index e1f56127f28..645320c32e7 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -14,7 +14,13 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; -import { LogMessages, logInfo, trackNewAlerRuleFormError } from '../../../Analytics'; +import { + LogMessages, + logInfo, + trackAlertRuleFormError, + trackAlertRuleFormCancelled, + trackAlertRuleFormSaved, +} from '../../../Analytics'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; @@ -109,6 +115,9 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { notifyApp.error(conditionErrorMsg); return; } + + trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); + // when creating a new rule, we save the manual routing setting in local storage if (!existing) { if (values.manualRouting) { @@ -154,20 +163,21 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { }; const onInvalid: SubmitErrorHandler = (errors): void => { - if (!existing) { - trackNewAlerRuleFormError({ - grafana_version: config.buildInfo.version, - org_id: contextSrv.user.orgId, - user_id: contextSrv.user.id, - error: Object.keys(errors).toString(), - }); - } + trackAlertRuleFormError({ + grafana_version: config.buildInfo.version, + org_id: contextSrv.user.orgId, + user_id: contextSrv.user.id, + error: Object.keys(errors).toString(), + formAction: existing ? 'update' : 'create', + }); notifyApp.error('There are errors in the form. Please correct them and try again!'); }; const cancelRuleCreation = () => { logInfo(LogMessages.cancelSavingAlertRule); + trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' }); }; + const evaluateEveryInForm = watch('evaluateEvery'); useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx index d462cb653f7..2f77f19184a 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx @@ -8,7 +8,13 @@ import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; -import { logInfo, LogMessages } from '../../Analytics'; +import { + logInfo, + LogMessages, + trackRulesListViewChange, + trackRulesSearchComponentInteraction, + trackRulesSearchInputInteraction, +} from '../../Analytics'; import { useRulesFilter } from '../../hooks/useFilteredRules'; import { RuleHealth } from '../../search/rulesSearchParser'; import { alertStateToReadable } from '../../utils/rules'; @@ -90,10 +96,12 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => }); setFilterKey((key) => key + 1); + trackRulesSearchComponentInteraction('dataSourceNames'); }; const handleDashboardChange = (dashboardUid: string | undefined) => { updateFilters({ ...filterState, dashboardUid }); + trackRulesSearchComponentInteraction('dashboardUid'); }; const clearDataSource = () => { @@ -104,18 +112,17 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => const handleAlertStateChange = (value: PromAlertingRuleState) => { logInfo(LogMessages.clickingAlertStateFilters); updateFilters({ ...filterState, ruleState: value }); - }; - - const handleViewChange = (view: string) => { - setQueryParams({ view }); + trackRulesSearchComponentInteraction('ruleState'); }; const handleRuleTypeChange = (ruleType: PromRuleType) => { updateFilters({ ...filterState, ruleType }); + trackRulesSearchComponentInteraction('ruleType'); }; const handleRuleHealthChange = (ruleHealth: RuleHealth) => { updateFilters({ ...filterState, ruleHealth }); + trackRulesSearchComponentInteraction('ruleHealth'); }; const handleClearFiltersClick = () => { @@ -125,6 +132,11 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => setTimeout(() => setFilterKey(filterKey + 1), 100); }; + const handleViewChange = (view: string) => { + setQueryParams({ view }); + trackRulesListViewChange({ view }); + }; + const searchIcon = ; return (
@@ -211,6 +223,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => onSubmit={handleSubmit((data) => { setSearchQuery(data.searchQuery); searchQueryRef.current?.blur(); + trackRulesSearchInputInteraction({ oldQuery: searchQuery, newQuery: data.searchQuery }); })} >