grafana/public/app/features/alerting/unified/Analytics.ts

241 lines
7.3 KiB
TypeScript

import { isEmpty } from 'lodash';
import { dateTime } from '@grafana/data';
import { createMonitoringLogger, getBackendSrv } from '@grafana/runtime';
import { config, reportInteraction } from '@grafana/runtime/src';
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 = {
filterByLabel: 'filtering alert instances by label',
loadedList: 'loaded Alert Rules list',
leavingRuleGroupEdit: 'leaving rule group edit without saving',
alertRuleFromPanel: 'creating alert rule from panel',
alertRuleFromScratch: 'creating alert rule from scratch',
recordingRuleFromScratch: 'creating recording rule from scratch',
clickingAlertStateFilters: 'clicking alert state filters',
cancelSavingAlertRule: 'user canceled alert rule creation',
successSavingAlertRule: 'alert rule saved successfully',
unknownMessageFromError: 'unknown messageFromError',
};
const { logInfo, logError, logMeasurement } = createMonitoringLogger('features.alerting', { module: 'Alerting' });
export { logInfo, logError, logMeasurement };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withPerformanceLogging<TFunc extends (...args: any[]) => Promise<any>>(
type: string,
func: TFunc,
context: Record<string, string>
): (...args: Parameters<TFunc>) => Promise<Awaited<ReturnType<TFunc>>> {
return async function (...args) {
const startLoadingTs = performance.now();
const response = await func(...args);
const loadTimesMs = performance.now() - startLoadingTs;
logMeasurement(
type,
{
loadTimesMs,
},
context
);
return response;
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withPromRulesMetadataLogging<TFunc extends (...args: any[]) => Promise<RuleNamespace[]>>(
type: string,
func: TFunc,
context: Record<string, string>
) {
return async (...args: Parameters<TFunc>) => {
const startLoadingTs = performance.now();
const response = await func(...args);
const { namespacesCount, groupsCount, rulesCount } = getPromRulesMetadata(response);
logMeasurement(
type,
{
loadTimeMs: performance.now() - startLoadingTs,
namespacesCount,
groupsCount,
rulesCount,
},
context
);
return response;
};
}
function getPromRulesMetadata(promRules: RuleNamespace[]) {
const namespacesCount = promRules.length;
const groupsCount = promRules.flatMap((ns) => ns.groups).length;
const rulesCount = promRules.flatMap((ns) => ns.groups).flatMap((g) => g.rules).length;
const metadata = {
namespacesCount: namespacesCount,
groupsCount: groupsCount,
rulesCount: rulesCount,
};
return metadata;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function withRulerRulesMetadataLogging<TFunc extends (...args: any[]) => Promise<RulerRulesConfigDTO>>(
type: string,
func: TFunc,
context: Record<string, string>
) {
return async (...args: Parameters<TFunc>) => {
const startLoadingTs = performance.now();
const response = await func(...args);
const { namespacesCount, groupsCount, rulesCount } = getRulerRulesMetadata(response);
logMeasurement(
type,
{
namespacesCount,
groupsCount,
rulesCount,
loadTimeMs: performance.now() - startLoadingTs,
},
context
);
return response;
};
}
function getRulerRulesMetadata(rulerRules: RulerRulesConfigDTO) {
const namespaces = Object.keys(rulerRules);
const groups = Object.values(rulerRules).flatMap((groups) => groups);
const rules = groups.flatMap((group) => group.rules);
return {
namespacesCount: namespaces.length,
groupsCount: groups.length,
rulesCount: rules.length,
};
}
export async function isNewUser() {
try {
const { createdAt } = await getBackendSrv().get(`/api/user`);
const limitDateForNewUser = dateTime().subtract(USER_CREATION_MIN_DAYS, 'days');
const userCreationDate = dateTime(createdAt);
const isNew = limitDateForNewUser.isBefore(userCreationDate);
return isNew;
} catch {
return true; //if no date is returned, we assume the user is new to prevent tracking actions
}
}
export const trackRuleListNavigation = async (
props: AlertRuleTrackingProps = {
grafana_version: config.buildInfo.version,
org_id: contextSrv.user.orgId,
user_id: contextSrv.user.id,
}
) => {
const isNew = await isNewUser();
if (isNew) {
return;
}
reportInteraction('grafana_alerting_navigation', props);
};
export const trackAlertRuleFormSaved = (props: { formAction: 'create' | 'update'; ruleType?: RuleFormType }) => {
reportInteraction('grafana_alerting_rule_creation', props);
};
export const trackAlertRuleFormCancelled = (props: { formAction: 'create' | 'update' }) => {
reportInteraction('grafana_alerting_rule_aborted', props);
};
export const trackAlertRuleFormError = (
props: AlertRuleTrackingProps & { error: string; formAction: 'create' | 'update' }
) => {
reportInteraction('grafana_alerting_rule_form_error', props);
};
export const trackInsightsFeedback = async (props: { useful: boolean; panel: string }) => {
const defaults = {
grafana_version: config.buildInfo.version,
org_id: contextSrv.user.orgId,
user_id: contextSrv.user.id,
};
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 function trackSwitchToSimplifiedRouting() {
reportInteraction('grafana_alerting_switch_to_simplified_routing');
}
export function trackSwitchToPoliciesRouting() {
reportInteraction('grafana_alerting_switch_to_policies_routing');
}
export type AlertRuleTrackingProps = {
user_id: number;
grafana_version?: string;
org_id?: number;
};