From 0d348dc0b166e8ed8c687148e3abba5ccf4735ef Mon Sep 17 00:00:00 2001 From: Virginia Cepeda Date: Mon, 3 Oct 2022 11:00:19 -0300 Subject: [PATCH] Alerting: log alert rule creation and clicking state filters (#55698) * Add messages for new trackings * Track clicking on alert state filters * Track creating alert rule from panel * Track creating alert rule from scratch * Track on success and when cancelling a rule creation --- .../features/alerting/unified/Analytics.ts | 5 ++ .../alerting/unified/RuleList.test.tsx | 43 +++++++++++++- .../features/alerting/unified/RuleList.tsx | 3 + .../NewRuleFromPanelButton.test.tsx | 56 +++++++++++++++++++ .../NewRuleFromPanelButton.tsx | 10 +++- .../components/rule-editor/AlertRuleForm.tsx | 10 +++- .../unified/components/rules/NoRulesCTA.tsx | 3 + .../components/rules/RulesFilter.test.tsx | 40 +++++++++++++ .../unified/components/rules/RulesFilter.tsx | 4 +- .../alerting/unified/state/actions.ts | 6 +- 10 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx create mode 100644 public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 586617926a8..d25dd758577 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -2,4 +2,9 @@ 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', + clickingAlertStateFilters: 'clicking alert state filters', + cancelSavingAlertRule: 'user canceled alert rule creation', + successSavingAlertRule: 'alert rule saved successfully', }; diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 1e0cdc8ffce..66fd221e9cf 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -1,17 +1,18 @@ import { SerializedError } from '@reduxjs/toolkit'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; -import { locationService, setDataSourceSrv } from '@grafana/runtime'; +import { locationService, setDataSourceSrv, logInfo } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto'; +import { LogMessages } from './Analytics'; import RuleList from './RuleList'; import { discoverFeatures } from './api/buildInfo'; import { fetchRules } from './api/prometheus'; @@ -44,6 +45,13 @@ jest.mock('app/core/core', () => ({ emit: () => {}, }, })); +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + logInfo: jest.fn(), + }; +}); jest.spyOn(config, 'getAllDataSources'); @@ -745,4 +753,35 @@ describe('RuleList', () => { }); }); }); + + describe('Analytics', () => { + it('Sends log info when creating an alert rule from a scratch', async () => { + enableRBAC(); + + grantUserPermissions([ + AccessControlAction.FoldersRead, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + ]); + + mocks.getAllDataSourcesMock.mockReturnValue([]); + setDataSourceSrv(new MockDataSourceSrv({})); + mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({}); + + renderRuleList(); + + await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + + const button = screen.getByText('New alert rule'); + + button.addEventListener('click', (event) => event.preventDefault(), false); + + expect(button).toBeEnabled(); + + await userEvent.click(button); + + expect(logInfo).toHaveBeenCalledWith(LogMessages.alertRuleFromScratch); + }); + }); }); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index fc3ee1c17e4..9a62f75af33 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -3,10 +3,12 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2, urlUtil } from '@grafana/data'; +import { logInfo } from '@grafana/runtime'; import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; +import { LogMessages } from './Analytics'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { NoRulesSplash } from './components/rules/NoRulesCTA'; import { RuleListErrors } from './components/rules/RuleListErrors'; @@ -101,6 +103,7 @@ const RuleList = withErrorBoundary( logInfo(LogMessages.alertRuleFromScratch)} > New alert rule diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx new file mode 100644 index 00000000000..9950ba64363 --- /dev/null +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { logInfo } from '@grafana/runtime'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; + +import { LogMessages } from '../../Analytics'; + +import { NewRuleFromPanelButton } from './NewRuleFromPanelButton'; + +jest.mock('app/types', () => { + const original = jest.requireActual('app/types'); + return { + ...original, + useSelector: jest.fn(), + }; +}); + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: 'localhost:3000/example/path', + }), +})); + +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + logInfo: jest.fn(), + }; +}); + +jest.mock('react-use', () => ({ + useAsync: () => ({ loading: false, value: {} }), +})); + +describe('Analytics', () => { + it('Sends log info when creating an alert rule from a panel', async () => { + const panel = new PanelModel({ + id: 123, + }); + const dashboard = new DashboardModel({ + id: 1, + }); + render(); + + const button = screen.getByText('Create alert rule from this panel'); + + button.addEventListener('click', (event) => event.preventDefault(), false); + + await userEvent.click(button); + + expect(logInfo).toHaveBeenCalledWith(LogMessages.alertRuleFromPanel); + }); +}); diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx index 20d394bbb65..51ace2c47d6 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx @@ -3,10 +3,12 @@ import { useLocation } from 'react-router-dom'; import { useAsync } from 'react-use'; import { urlUtil } from '@grafana/data'; +import { logInfo } from '@grafana/runtime'; import { Alert, Button, LinkButton } from '@grafana/ui'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { useSelector } from 'app/types'; +import { LogMessages } from '../../Analytics'; import { panelToRuleFormValues } from '../../utils/rule-form'; interface Props { @@ -46,7 +48,13 @@ export const NewRuleFromPanelButton: FC = ({ dashboard, panel, className }); return ( - + logInfo(LogMessages.alertRuleFromPanel)} + href={ruleFormUrl} + className={className} + data-testid="create-alert-rule-button" + > Create alert rule from this panel ); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index 7c8a0d91469..99959162b39 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -4,6 +4,7 @@ import { FormProvider, useForm, UseFormWatch } from 'react-hook-form'; import { Link } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; +import { logInfo } from '@grafana/runtime'; import { Button, ConfirmModal, CustomScrollbar, PageToolbar, Spinner, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useCleanup } from 'app/core/hooks/useCleanup'; @@ -11,6 +12,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; +import { LogMessages } from '../../Analytics'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { deleteRuleAction, saveRuleFormAction } from '../../state/actions'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; @@ -110,7 +112,13 @@ export const AlertRuleForm: FC = ({ existing }) => {
e.preventDefault()} className={styles.form}> - diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx index b90632070f1..03ed69d6cf4 100644 --- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx +++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx @@ -1,8 +1,10 @@ import React, { FC } from 'react'; +import { logInfo } from '@grafana/runtime'; import { CallToActionCard } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import { LogMessages } from '../../Analytics'; import { useRulesAccess } from '../../utils/accessControlHooks'; export const NoRulesSplash: FC = () => { @@ -19,6 +21,7 @@ export const NoRulesSplash: FC = () => { proTipLink="https://grafana.com/docs/" proTipLinkTitle="Learn more" proTipTarget="_blank" + onClick={() => logInfo(LogMessages.alertRuleFromScratch)} /> ); } diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx new file mode 100644 index 00000000000..4f7b1877828 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { logInfo } from '@grafana/runtime'; + +import { LogMessages } from '../../Analytics'; + +import RulesFilter from './RulesFilter'; + +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + logInfo: jest.fn(), + DataSourcePicker: () => <>, + }; +}); + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: 'localhost:3000/example/path', + }), +})); + +jest.mock('../../utils/misc', () => ({ + getFiltersFromUrlParams: jest.fn(() => ({ dataSource: {}, alertState: {}, queryString: '', ruleType: '' })), +})); + +describe('Analytics', () => { + it('Sends log info when clicking alert state filters', async () => { + render(); + + const button = screen.getByText('Pending'); + + await userEvent.click(button); + + expect(logInfo).toHaveBeenCalledWith(LogMessages.clickingAlertStateFilters); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx index 5b2d767c042..7a85d505bc4 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx @@ -3,11 +3,12 @@ import { debounce } from 'lodash'; import React, { FormEvent, useState } from 'react'; import { DataSourceInstanceSettings, GrafanaTheme, SelectableValue } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { DataSourcePicker, logInfo } from '@grafana/runtime'; import { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; +import { LogMessages } from '../../Analytics'; import { getFiltersFromUrlParams } from '../../utils/misc'; import { alertStateToReadable } from '../../utils/rules'; @@ -69,6 +70,7 @@ const RulesFilter = () => { }, 600); const handleAlertStateChange = (value: string) => { + logInfo(LogMessages.clickingAlertStateFilters); setQueryParams({ alertState: value }); }; diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 3043cfede1e..8d3d59a98a2 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -1,7 +1,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { isEmpty } from 'lodash'; -import { locationService } from '@grafana/runtime'; +import { locationService, logInfo } from '@grafana/runtime'; import { AlertmanagerAlert, AlertManagerCortexConfig, @@ -32,6 +32,7 @@ import { } from 'app/types/unified-alerting-dto'; import { backendSrv } from '../../../../core/services/backend_srv'; +import { LogMessages } from '../Analytics'; import { addAlertManagers, createOrUpdateSilence, @@ -422,6 +423,9 @@ export const saveRuleFormAction = createAsyncThunk( } else { throw new Error('Unexpected rule form type'); } + + logInfo(LogMessages.successSavingAlertRule); + if (redirectOnSave) { locationService.push(redirectOnSave); } else {