mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 21:22:15 +08:00
Alerting: log some basic user interactions (#55401)
* Log leaving rule group edit without saving * Log filtering alert instances by label Also debouncing the search to prevent sending too many log requests * Log loading the Alert Rules list * Move log messages to centralized location * Add tests on log trackings * Fix linting
This commit is contained in:
5
public/app/features/alerting/unified/Analytics.ts
Normal file
5
public/app/features/alerting/unified/Analytics.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const LogMessages = {
|
||||||
|
filterByLabel: 'filtering alert instances by label',
|
||||||
|
loadedList: 'loaded Alert Rules list',
|
||||||
|
leavingRuleGroupEdit: 'leaving rule group edit without saving',
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import lodash from 'lodash'; // eslint-disable-line lodash/import-scope
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { logInfo } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { LogMessages } from '../../Analytics';
|
||||||
|
|
||||||
|
import { MatcherFilter } from './MatcherFilter';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime');
|
||||||
|
|
||||||
|
describe('Analytics', () => {
|
||||||
|
it('Sends log info when filtering alert instances by label', async () => {
|
||||||
|
lodash.debounce = jest.fn().mockImplementation((fn) => fn);
|
||||||
|
|
||||||
|
render(<MatcherFilter onFilterChange={jest.fn()} />);
|
||||||
|
|
||||||
|
const searchInput = screen.getByTestId('search-query-input');
|
||||||
|
await userEvent.type(searchInput, 'job=');
|
||||||
|
|
||||||
|
expect(logInfo).toHaveBeenCalledWith(LogMessages.filterByLabel);
|
||||||
|
});
|
||||||
|
});
|
@ -1,9 +1,13 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import React, { FormEvent } from 'react';
|
import React, { FormEvent } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { logInfo } from '@grafana/runtime';
|
||||||
import { Label, Tooltip, Input, Icon, useStyles2, Stack } from '@grafana/ui';
|
import { Label, Tooltip, Input, Icon, useStyles2, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { LogMessages } from '../../Analytics';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
queryString?: string;
|
queryString?: string;
|
||||||
@ -13,10 +17,12 @@ interface Props {
|
|||||||
|
|
||||||
export const MatcherFilter = ({ className, onFilterChange, defaultQueryString, queryString }: Props) => {
|
export const MatcherFilter = ({ className, onFilterChange, defaultQueryString, queryString }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const handleSearchChange = (e: FormEvent<HTMLInputElement>) => {
|
const handleSearchChange = debounce((e: FormEvent<HTMLInputElement>) => {
|
||||||
|
logInfo(LogMessages.filterByLabel);
|
||||||
|
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
onFilterChange(target.value);
|
onFilterChange(target.value);
|
||||||
};
|
}, 600);
|
||||||
const searchIcon = <Icon name={'search'} />;
|
const searchIcon = <Icon name={'search'} />;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
@ -18,7 +18,7 @@ import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
|||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
namespace: CombinedRuleNamespace;
|
namespace: CombinedRuleNamespace;
|
||||||
group: CombinedRuleGroup;
|
group: CombinedRuleGroup;
|
||||||
onClose: () => void;
|
onClose: (saved?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
@ -46,7 +46,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
// close modal if successfully saved
|
// close modal if successfully saved
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dispatched && !loading && !error) {
|
if (dispatched && !loading && !error) {
|
||||||
onClose();
|
onClose(true);
|
||||||
}
|
}
|
||||||
}, [dispatched, loading, onClose, error]);
|
}, [dispatched, loading, onClose, error]);
|
||||||
|
|
||||||
@ -123,7 +123,13 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button variant="secondary" type="button" disabled={loading} onClick={onClose} fill="outline">
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => onClose(false)}
|
||||||
|
fill="outline"
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={!isDirty || loading}>
|
<Button type="submit" disabled={!isDirty || loading}>
|
||||||
|
@ -4,17 +4,26 @@ import { Provider } from 'react-redux';
|
|||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { byRole } from 'testing-library-selector';
|
import { byRole } from 'testing-library-selector';
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService, 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 { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { LogMessages } from '../../Analytics';
|
||||||
import { mockCombinedRule, mockDataSource } from '../../mocks';
|
import { mockCombinedRule, mockDataSource } from '../../mocks';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
|
|
||||||
import { RuleListGroupView } from './RuleListGroupView';
|
import { RuleListGroupView } from './RuleListGroupView';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => {
|
||||||
|
const original = jest.requireActual('@grafana/runtime');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
logInfo: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
grafanaRulesHeading: byRole('heading', { name: 'Grafana' }),
|
grafanaRulesHeading: byRole('heading', { name: 'Grafana' }),
|
||||||
cloudRulesHeading: byRole('heading', { name: 'Mimir / Cortex / Loki' }),
|
cloudRulesHeading: byRole('heading', { name: 'Mimir / Cortex / Loki' }),
|
||||||
@ -74,6 +83,17 @@ describe('RuleListGroupView', () => {
|
|||||||
expect(ui.cloudRulesHeading.query()).not.toBeInTheDocument();
|
expect(ui.cloudRulesHeading.query()).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Analytics', () => {
|
||||||
|
it('Sends log info when the list is loaded', () => {
|
||||||
|
const grafanaNamespace = getGrafanaNamespace();
|
||||||
|
const namespaces: CombinedRuleNamespace[] = [grafanaNamespace];
|
||||||
|
|
||||||
|
renderRuleList(namespaces);
|
||||||
|
|
||||||
|
expect(logInfo).toHaveBeenCalledWith(LogMessages.loadedList);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderRuleList(namespaces: CombinedRuleNamespace[]) {
|
function renderRuleList(namespaces: CombinedRuleNamespace[]) {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { logInfo } from '@grafana/runtime';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { LogMessages } from '../../Analytics';
|
||||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
import { Authorize } from '../Authorize';
|
import { Authorize } from '../Authorize';
|
||||||
|
|
||||||
@ -28,6 +30,10 @@ export const RuleListGroupView: FC<Props> = ({ namespaces, expandAll }) => {
|
|||||||
];
|
];
|
||||||
}, [namespaces]);
|
}, [namespaces]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logInfo(LogMessages.loadedList);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Authorize actions={[AccessControlAction.AlertingRuleRead]}>
|
<Authorize actions={[AccessControlAction.AlertingRuleRead]}>
|
||||||
|
@ -1,20 +1,28 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render, 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 { byTestId, byText } from 'testing-library-selector';
|
import { byTestId, byText } from 'testing-library-selector';
|
||||||
|
|
||||||
|
import { 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 { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { LogMessages } from '../../Analytics';
|
||||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||||
import { disableRBAC, mockCombinedRule, mockDataSource } from '../../mocks';
|
import { disableRBAC, mockCombinedRule, mockDataSource } from '../../mocks';
|
||||||
|
|
||||||
import { RulesGroup } from './RulesGroup';
|
import { RulesGroup } from './RulesGroup';
|
||||||
|
|
||||||
jest.mock('../../hooks/useHasRuler');
|
jest.mock('../../hooks/useHasRuler');
|
||||||
|
jest.mock('@grafana/runtime', () => {
|
||||||
|
const original = jest.requireActual('@grafana/runtime');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
logInfo: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
const mocks = {
|
const mocks = {
|
||||||
useHasRuler: jest.mocked(useHasRuler),
|
useHasRuler: jest.mocked(useHasRuler),
|
||||||
};
|
};
|
||||||
@ -130,4 +138,37 @@ describe('Rules group tests', () => {
|
|||||||
expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument();
|
expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Analytics', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
contextSrv.isEditor = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const group: CombinedRuleGroup = {
|
||||||
|
name: 'TestGroup',
|
||||||
|
rules: [mockCombinedRule()],
|
||||||
|
};
|
||||||
|
|
||||||
|
const namespace: CombinedRuleNamespace = {
|
||||||
|
name: 'TestNamespace',
|
||||||
|
rulesSource: mockDataSource(),
|
||||||
|
groups: [group],
|
||||||
|
};
|
||||||
|
|
||||||
|
disableRBAC();
|
||||||
|
|
||||||
|
it('Should log info when closing the edit group rule modal without saving', async () => {
|
||||||
|
mockUseHasRuler(true, true);
|
||||||
|
renderRulesGroup(namespace, group);
|
||||||
|
|
||||||
|
await userEvent.click(ui.editGroupButton.get());
|
||||||
|
|
||||||
|
expect(screen.getByText('Close')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Close'));
|
||||||
|
|
||||||
|
expect(screen.queryByText('Close')).not.toBeInTheDocument();
|
||||||
|
expect(logInfo).toHaveBeenCalledWith(LogMessages.leavingRuleGroupEdit);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,12 @@ import pluralize from 'pluralize';
|
|||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { logInfo } from '@grafana/runtime';
|
||||||
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { LogMessages } from '../../Analytics';
|
||||||
import { useFolder } from '../../hooks/useFolder';
|
import { useFolder } from '../../hooks/useFolder';
|
||||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||||
import { deleteRulesGroupAction } from '../../state/actions';
|
import { deleteRulesGroupAction } from '../../state/actions';
|
||||||
@ -179,6 +181,13 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
|
|||||||
<RuleLocation namespace={namespace.name} group={group.name} />
|
<RuleLocation namespace={namespace.name} group={group.name} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeEditModal = (saved = false) => {
|
||||||
|
if (!saved) {
|
||||||
|
logInfo(LogMessages.leavingRuleGroupEdit);
|
||||||
|
}
|
||||||
|
setIsEditingGroup(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} data-testid="rule-group">
|
<div className={styles.wrapper} data-testid="rule-group">
|
||||||
<div className={styles.header} data-testid="rule-group-header">
|
<div className={styles.header} data-testid="rule-group-header">
|
||||||
@ -215,9 +224,7 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll,
|
|||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<RulesTable showSummaryColumn={true} className={styles.rulesTable} showGuidelines={true} rules={group.rules} />
|
<RulesTable showSummaryColumn={true} className={styles.rulesTable} showGuidelines={true} rules={group.rules} />
|
||||||
)}
|
)}
|
||||||
{isEditingGroup && (
|
{isEditingGroup && <EditCloudGroupModal group={group} namespace={namespace} onClose={() => closeEditModal()} />}
|
||||||
<EditCloudGroupModal group={group} namespace={namespace} onClose={() => setIsEditingGroup(false)} />
|
|
||||||
)}
|
|
||||||
{isReorderingGroup && (
|
{isReorderingGroup && (
|
||||||
<ReorderCloudGroupModal group={group} namespace={namespace} onClose={() => setIsReorderingGroup(false)} />
|
<ReorderCloudGroupModal group={group} namespace={namespace} onClose={() => setIsReorderingGroup(false)} />
|
||||||
)}
|
)}
|
||||||
|
Reference in New Issue
Block a user