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
This commit is contained in:
Virginia Cepeda 2022-10-03 11:00:19 -03:00 committed by GitHub
parent 09f8e026a1
commit 0d348dc0b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 174 additions and 6 deletions

View File

@ -2,4 +2,9 @@ export const LogMessages = {
filterByLabel: 'filtering alert instances by label', filterByLabel: 'filtering alert instances by label',
loadedList: 'loaded Alert Rules list', loadedList: 'loaded Alert Rules list',
leavingRuleGroupEdit: 'leaving rule group edit without saving', 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',
}; };

View File

@ -1,17 +1,18 @@
import { SerializedError } from '@reduxjs/toolkit'; 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 userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; 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 { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore'; import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto'; import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto';
import { LogMessages } from './Analytics';
import RuleList from './RuleList'; import RuleList from './RuleList';
import { discoverFeatures } from './api/buildInfo'; import { discoverFeatures } from './api/buildInfo';
import { fetchRules } from './api/prometheus'; import { fetchRules } from './api/prometheus';
@ -44,6 +45,13 @@ jest.mock('app/core/core', () => ({
emit: () => {}, emit: () => {},
}, },
})); }));
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
logInfo: jest.fn(),
};
});
jest.spyOn(config, 'getAllDataSources'); 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);
});
});
}); });

View File

@ -3,10 +3,12 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { GrafanaTheme2, urlUtil } from '@grafana/data'; import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { logInfo } from '@grafana/runtime';
import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui'; import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types'; import { useDispatch } from 'app/types';
import { LogMessages } from './Analytics';
import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoRulesSplash } from './components/rules/NoRulesCTA'; import { NoRulesSplash } from './components/rules/NoRulesCTA';
import { RuleListErrors } from './components/rules/RuleListErrors'; import { RuleListErrors } from './components/rules/RuleListErrors';
@ -101,6 +103,7 @@ const RuleList = withErrorBoundary(
<LinkButton <LinkButton
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })} href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
icon="plus" icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
> >
New alert rule New alert rule
</LinkButton> </LinkButton>

View File

@ -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(<NewRuleFromPanelButton panel={panel} dashboard={dashboard} />);
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);
});
});

View File

@ -3,10 +3,12 @@ import { useLocation } from 'react-router-dom';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { urlUtil } from '@grafana/data'; import { urlUtil } from '@grafana/data';
import { logInfo } from '@grafana/runtime';
import { Alert, Button, LinkButton } from '@grafana/ui'; import { Alert, Button, LinkButton } from '@grafana/ui';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { LogMessages } from '../../Analytics';
import { panelToRuleFormValues } from '../../utils/rule-form'; import { panelToRuleFormValues } from '../../utils/rule-form';
interface Props { interface Props {
@ -46,7 +48,13 @@ export const NewRuleFromPanelButton: FC<Props> = ({ dashboard, panel, className
}); });
return ( return (
<LinkButton icon="bell" href={ruleFormUrl} className={className} data-testid="create-alert-rule-button"> <LinkButton
icon="bell"
onClick={() => logInfo(LogMessages.alertRuleFromPanel)}
href={ruleFormUrl}
className={className}
data-testid="create-alert-rule-button"
>
Create alert rule from this panel Create alert rule from this panel
</LinkButton> </LinkButton>
); );

View File

@ -4,6 +4,7 @@ import { FormProvider, useForm, UseFormWatch } from 'react-hook-form';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { logInfo } from '@grafana/runtime';
import { Button, ConfirmModal, CustomScrollbar, PageToolbar, Spinner, useStyles2 } from '@grafana/ui'; import { Button, ConfirmModal, CustomScrollbar, PageToolbar, Spinner, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
@ -11,6 +12,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types'; import { useDispatch } from 'app/types';
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
import { LogMessages } from '../../Analytics';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { deleteRuleAction, saveRuleFormAction } from '../../state/actions'; import { deleteRuleAction, saveRuleFormAction } from '../../state/actions';
import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form';
@ -110,7 +112,13 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
<form onSubmit={(e) => e.preventDefault()} className={styles.form}> <form onSubmit={(e) => e.preventDefault()} className={styles.form}>
<PageToolbar title={`${existing ? 'Edit' : 'Create'} alert rule`} pageIcon="bell"> <PageToolbar title={`${existing ? 'Edit' : 'Create'} alert rule`} pageIcon="bell">
<Link to={returnTo}> <Link to={returnTo}>
<Button variant="secondary" disabled={submitState.loading} type="button" fill="outline"> <Button
variant="secondary"
disabled={submitState.loading}
type="button"
fill="outline"
onClick={() => logInfo(LogMessages.cancelSavingAlertRule)}
>
Cancel Cancel
</Button> </Button>
</Link> </Link>

View File

@ -1,8 +1,10 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { logInfo } from '@grafana/runtime';
import { CallToActionCard } from '@grafana/ui'; import { CallToActionCard } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { LogMessages } from '../../Analytics';
import { useRulesAccess } from '../../utils/accessControlHooks'; import { useRulesAccess } from '../../utils/accessControlHooks';
export const NoRulesSplash: FC = () => { export const NoRulesSplash: FC = () => {
@ -19,6 +21,7 @@ export const NoRulesSplash: FC = () => {
proTipLink="https://grafana.com/docs/" proTipLink="https://grafana.com/docs/"
proTipLinkTitle="Learn more" proTipLinkTitle="Learn more"
proTipTarget="_blank" proTipTarget="_blank"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
/> />
); );
} }

View File

@ -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(<RulesFilter />);
const button = screen.getByText('Pending');
await userEvent.click(button);
expect(logInfo).toHaveBeenCalledWith(LogMessages.clickingAlertStateFilters);
});
});

View File

@ -3,11 +3,12 @@ import { debounce } from 'lodash';
import React, { FormEvent, useState } from 'react'; import React, { FormEvent, useState } from 'react';
import { DataSourceInstanceSettings, GrafanaTheme, SelectableValue } from '@grafana/data'; 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 { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
import { LogMessages } from '../../Analytics';
import { getFiltersFromUrlParams } from '../../utils/misc'; import { getFiltersFromUrlParams } from '../../utils/misc';
import { alertStateToReadable } from '../../utils/rules'; import { alertStateToReadable } from '../../utils/rules';
@ -69,6 +70,7 @@ const RulesFilter = () => {
}, 600); }, 600);
const handleAlertStateChange = (value: string) => { const handleAlertStateChange = (value: string) => {
logInfo(LogMessages.clickingAlertStateFilters);
setQueryParams({ alertState: value }); setQueryParams({ alertState: value });
}; };

View File

@ -1,7 +1,7 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { locationService } from '@grafana/runtime'; import { locationService, logInfo } from '@grafana/runtime';
import { import {
AlertmanagerAlert, AlertmanagerAlert,
AlertManagerCortexConfig, AlertManagerCortexConfig,
@ -32,6 +32,7 @@ import {
} from 'app/types/unified-alerting-dto'; } from 'app/types/unified-alerting-dto';
import { backendSrv } from '../../../../core/services/backend_srv'; import { backendSrv } from '../../../../core/services/backend_srv';
import { LogMessages } from '../Analytics';
import { import {
addAlertManagers, addAlertManagers,
createOrUpdateSilence, createOrUpdateSilence,
@ -422,6 +423,9 @@ export const saveRuleFormAction = createAsyncThunk(
} else { } else {
throw new Error('Unexpected rule form type'); throw new Error('Unexpected rule form type');
} }
logInfo(LogMessages.successSavingAlertRule);
if (redirectOnSave) { if (redirectOnSave) {
locationService.push(redirectOnSave); locationService.push(redirectOnSave);
} else { } else {