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
pull/83516/head^2
Konrad Lalik 1 year ago committed by GitHub
parent 07128cfec1
commit 183a42b7f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 67
      public/app/features/alerting/unified/Analytics.ts
  2. 28
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  3. 23
      public/app/features/alerting/unified/components/rules/RulesFilter.tsx

@ -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;

@ -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<RuleFormValues> = (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]);

@ -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 = <Icon name={'search'} />;
return (
<div className={styles.container}>
@ -211,6 +223,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
onSubmit={handleSubmit((data) => {
setSearchQuery(data.searchQuery);
searchQueryRef.current?.blur();
trackRulesSearchInputInteraction({ oldQuery: searchQuery, newQuery: data.searchQuery });
})}
>
<Field

Loading…
Cancel
Save