Skip to content

Commit

Permalink
[SecuritySolution] Add UI tracking for grouping options (elastic#151708)
Browse files Browse the repository at this point in the history
## Summary

Relevant issues:
Overall telemetry: elastic#144945
Alerts telemetry: elastic#150656

Preview dashboard:

https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/dashboards#/view/40755fc0-b454-11ed-a6e6-d32d2209b7b7?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-7d%2Fd,to:now))

**UI counter added** - overall counts
1. alerts_table_group_by_{tableId}_{groupByField} -
`alerts_table_group_by_alerts-page_host.name ` triggered on grouping
option changed.
2. alerts_table_toggled_{on|off}_{tableId}_group-{groupNumber} -
`alerts_table_toggled_off_alerts-page_group-0` sent when grouped alerts
toggled
3. alerts_table_{tableId}_group-{groupNumber}_mark-{status} -
`alerts_table_alerts-page_group-0_mark-open` sent when group actions
taken

**Event based telemetry added** - extra info from `properties` can be
aggregated / visualised
1. Alerts grouping take action - sent when group actions taken
2. Alerts grouping toggled - sent when grouped alerts toggled
3. Alerts grouping changed - triggered on grouping option changed

[Example
events](https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/discover#/view/9b0f2080-bcd1-11ed-a6e6-d32d2209b7b7?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-7d%2Fd,to:now))&_a=(columns:!(context.applicationId,properties,properties.groupingId,properties.groupNumber,properties.status,event_type,properties.tableId),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:c5dc7cd0-2950-4e51-b428-d0451b1b8d9d,key:context.applicationId,negate:!f,params:(query:securitySolutionUI),type:phrase),query:(match_phrase:(context.applicationId:securitySolutionUI)))),grid:(),hideChart:!f,index:c5dc7cd0-2950-4e51-b428-d0451b1b8d9d,interval:auto,query:(language:kuery,query:'event_type%20:%20Alerts%20Grouping*'),sort:!(!(timestamp,desc))))

**Steps to verify:**
1. add telemetry.optIn: true to kibana.dev.yml
2. Visit alerts page or rule details page, change the grouping , toggle
each group, and take actions to grouped alerts
3. Usually the event would be sent every hour to
[staging](https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/discover#/view/9b0f2080-bcd1-11ed-a6e6-d32d2209b7b7?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-7d%2Fd,to:now))&_a=(columns:!(context.applicationId,properties,properties.groupingId,properties.groupNumber,properties.status,event_type,properties.tableId),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:c5dc7cd0-2950-4e51-b428-d0451b1b8d9d,key:context.applicationId,negate:!f,params:(query:securitySolutionUI),type:phrase),query:(match_phrase:(context.applicationId:securitySolutionUI)))),grid:(),hideChart:!f,index:c5dc7cd0-2950-4e51-b428-d0451b1b8d9d,interval:auto,query:(language:kuery,query:'event_type%20:%20Alerts%20Grouping*'),sort:!(!(timestamp,desc)))),
if not, visit staging again on the next day.

### Checklist

Delete any items that are not applicable to this PR.


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Pablo Neves Machado <[email protected]>
  • Loading branch information
3 people authored and bmorelli25 committed Mar 10, 2023
1 parent f5ab39e commit 51d50c2
Show file tree
Hide file tree
Showing 24 changed files with 719 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React from 'react';

const onGroupChange = jest.fn();
const testProps = {
groupingId: 'test-grouping-id',
fields: [
{
name: 'kibana.alert.rule.name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { StyledContextMenu, StyledEuiButtonEmpty } from '../styles';
export interface GroupSelectorProps {
'data-test-subj'?: string;
fields: FieldSpec[];
groupingId: string;
groupSelected: string;
onGroupChange: (groupSelection: string) => void;
options: Array<{ key: string; label: string }>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { Grouping } from './grouping';
import { createGroupFilter } from './accordion_panel/helpers';
import { METRIC_TYPE } from '@kbn/analytics';
import { getTelemetryEvent } from '../telemetry/const';

const renderChildComponent = jest.fn();
const takeActionItems = jest.fn();
const mockTracker = jest.fn();
const rule1Name = 'Rule 1 name';
const rule1Desc = 'Rule 1 description';
const rule2Name = 'Rule 2 name';
Expand Down Expand Up @@ -98,6 +101,7 @@ const testProps = {
value: 2,
},
},
groupingId: 'test-grouping-id',
isLoading: false,
pagination: {
pageIndex: 0,
Expand All @@ -109,6 +113,7 @@ const testProps = {
renderChildComponent,
selectedGroup: 'kibana.alert.rule.name',
takeActionItems,
tracker: mockTracker,
};

describe('grouping container', () => {
Expand Down Expand Up @@ -171,4 +176,33 @@ describe('grouping container', () => {
createGroupFilter(testProps.selectedGroup, rule2Name)
);
});

it('Send Telemetry when each group is clicked', () => {
const { getAllByTestId } = render(
<I18nProvider>
<Grouping {...testProps} />
</I18nProvider>
);
const group1 = within(getAllByTestId('grouping-accordion')[0]).getAllByRole('button')[0];
fireEvent.click(group1);
expect(mockTracker).toHaveBeenNthCalledWith(
1,
METRIC_TYPE.CLICK,
getTelemetryEvent.groupToggled({
isOpen: true,
groupingId: testProps.groupingId,
groupNumber: 0,
})
);
fireEvent.click(group1);
expect(mockTracker).toHaveBeenNthCalledWith(
2,
METRIC_TYPE.CLICK,
getTelemetryEvent.groupToggled({
isOpen: false,
groupingId: testProps.groupingId,
groupNumber: 0,
})
);
});
});
37 changes: 33 additions & 4 deletions packages/kbn-securitysolution-grouping/src/components/grouping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import React, { useMemo, useState } from 'react';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { defaultUnit, firstNonNullValue } from '../helpers';
import { createGroupFilter } from './accordion_panel/helpers';
import type { BadgeMetric, CustomMetric } from './accordion_panel';
Expand All @@ -24,15 +25,23 @@ import { EmptyGroupingComponent } from './empty_results_panel';
import { groupingContainerCss, countCss } from './styles';
import { GROUPS_UNIT } from './translations';
import type { GroupingAggregation, GroupingFieldTotalAggregation, RawBucket } from './types';
import { getTelemetryEvent } from '../telemetry/const';

export interface GroupingProps<T> {
badgeMetricStats?: (fieldBucket: RawBucket<T>) => BadgeMetric[];
customMetricStats?: (fieldBucket: RawBucket<T>) => CustomMetric[];
data?: GroupingAggregation<T> & GroupingFieldTotalAggregation;
groupingId: string;
groupPanelRenderer?: (fieldBucket: RawBucket<T>) => JSX.Element | undefined;
groupSelector?: JSX.Element;
inspectButton?: JSX.Element;
isLoading: boolean;
onToggleCallback?: (params: {
isOpen: boolean;
groupName?: string | undefined;
groupNumber: number;
groupingId: string;
}) => void;
pagination: {
pageIndex: number;
pageSize: number;
Expand All @@ -42,22 +51,30 @@ export interface GroupingProps<T> {
};
renderChildComponent: (groupFilter: Filter[]) => React.ReactNode;
selectedGroup: string;
takeActionItems: (groupFilters: Filter[]) => JSX.Element[];
takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[];
tracker?: (
type: UiCounterMetricType,
event: string | string[],
count?: number | undefined
) => void;
unit?: (n: number) => string;
}

const GroupingComponent = <T,>({
badgeMetricStats,
customMetricStats,
data,
groupingId,
groupPanelRenderer,
groupSelector,
inspectButton,
isLoading,
onToggleCallback,
pagination,
renderChildComponent,
selectedGroup,
takeActionItems,
tracker,
unit = defaultUnit,
}: GroupingProps<T>) => {
const [trigger, setTrigger] = useState<
Expand All @@ -77,17 +94,20 @@ const GroupingComponent = <T,>({

const groupPanels = useMemo(
() =>
data?.stackByMultipleFields0?.buckets?.map((groupBucket) => {
data?.stackByMultipleFields0?.buckets?.map((groupBucket, groupNumber) => {
const group = firstNonNullValue(groupBucket.key);
const groupKey = `group0-${group}`;
const groupKey = `group-${groupNumber}-${group}`;

return (
<span key={groupKey}>
<GroupPanel
extraAction={
<GroupStats
bucket={groupBucket}
takeActionItems={takeActionItems(createGroupFilter(selectedGroup, group))}
takeActionItems={takeActionItems(
createGroupFilter(selectedGroup, group),
groupNumber
)}
badgeMetricStats={badgeMetricStats && badgeMetricStats(groupBucket)}
customMetricStats={customMetricStats && customMetricStats(groupBucket)}
/>
Expand All @@ -97,13 +117,19 @@ const GroupingComponent = <T,>({
groupPanelRenderer={groupPanelRenderer && groupPanelRenderer(groupBucket)}
isLoading={isLoading}
onToggleGroup={(isOpen) => {
// built-in telemetry: UI-counter
tracker?.(
METRIC_TYPE.CLICK,
getTelemetryEvent.groupToggled({ isOpen, groupingId, groupNumber })
);
setTrigger({
// ...trigger, -> this change will keep only one group at a time expanded and one table displayed
[groupKey]: {
state: isOpen ? 'open' : 'closed',
selectedBucket: groupBucket,
},
});
onToggleCallback?.({ isOpen, groupName: group, groupNumber, groupingId });
}}
renderChildComponent={
trigger[groupKey] && trigger[groupKey].state === 'open'
Expand All @@ -121,10 +147,13 @@ const GroupingComponent = <T,>({
customMetricStats,
data?.stackByMultipleFields0?.buckets,
groupPanelRenderer,
groupingId,
isLoading,
onToggleCallback,
renderChildComponent,
selectedGroup,
takeActionItems,
tracker,
trigger,
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { act, renderHook } from '@testing-library/react-hooks';
import { useGetGroupSelector } from './use_get_group_selector';
import { initialState } from './state';
import { ActionType, defaultGroup } from '..';
import { METRIC_TYPE } from '@kbn/analytics';

const defaultGroupingOptions = [
{ label: 'ruleName', key: 'kibana.alert.rule.name' },
Expand All @@ -25,6 +26,8 @@ const defaultArgs = {
fields: [],
groupingId,
groupingState: initialState,
tracker: jest.fn(),
onGroupChangeCallback: jest.fn(),
};
const customField = 'custom.field';
describe('useGetGroupSelector', () => {
Expand Down Expand Up @@ -123,6 +126,54 @@ describe('useGetGroupSelector', () => {
expect(dispatch).toHaveBeenCalledTimes(2);
});

it('On group change, sends telemetry', () => {
const testGroup = {
[groupingId]: {
...defaultGroup,
options: defaultGroupingOptions,
activeGroup: 'host.name',
},
};
const { result } = renderHook((props) => useGetGroupSelector(props), {
initialProps: {
...defaultArgs,
groupingState: {
groupById: testGroup,
},
},
});
act(() => result.current.props.onGroupChange(customField));
expect(defaultArgs.tracker).toHaveBeenCalledTimes(1);
expect(defaultArgs.tracker).toHaveBeenCalledWith(
METRIC_TYPE.CLICK,
`alerts_table_group_by_test-table_${customField}`
);
});

it('On group change, executes callback', () => {
const testGroup = {
[groupingId]: {
...defaultGroup,
options: defaultGroupingOptions,
activeGroup: 'host.name',
},
};
const { result } = renderHook((props) => useGetGroupSelector(props), {
initialProps: {
...defaultArgs,
groupingState: {
groupById: testGroup,
},
},
});
act(() => result.current.props.onGroupChange(customField));
expect(defaultArgs.onGroupChangeCallback).toHaveBeenCalledTimes(1);
expect(defaultArgs.onGroupChangeCallback).toHaveBeenCalledWith({
tableId: groupingId,
groupByField: customField,
});
});

it('On group change to custom field, updates options', () => {
const testGroup = {
[groupingId]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@
import type { FieldSpec } from '@kbn/data-views-plugin/common';
import { useCallback, useEffect } from 'react';

import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { getGroupSelector, isNoneGroup } from '../..';
import { groupActions, groupByIdSelector } from './state';
import type { GroupOption } from './types';
import { Action, defaultGroup, GroupMap } from './types';
import { getTelemetryEvent } from '../telemetry/const';

export interface UseGetGroupSelectorArgs {
defaultGroupingOptions: GroupOption[];
dispatch: React.Dispatch<Action>;
fields: FieldSpec[];
groupingId: string;
groupingState: GroupMap;
onGroupChangeCallback?: (param: { groupByField: string; tableId: string }) => void;
tracker: (
type: UiCounterMetricType,
event: string | string[],
count?: number | undefined
) => void;
}

export const useGetGroupSelector = ({
Expand All @@ -28,6 +36,8 @@ export const useGetGroupSelector = ({
fields,
groupingId,
groupingState,
onGroupChangeCallback,
tracker,
}: UseGetGroupSelectorArgs) => {
const { activeGroup: selectedGroup, options } =
groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup;
Expand Down Expand Up @@ -61,6 +71,14 @@ export const useGetGroupSelector = ({
setGroupsActivePage(0);
setSelectedGroup(groupSelection);

// built-in telemetry: UI-counter
tracker?.(
METRIC_TYPE.CLICK,
getTelemetryEvent.groupChanged({ groupingId, selected: groupSelection })
);

onGroupChangeCallback?.({ tableId: groupingId, groupByField: groupSelection });

// only update options if the new selection is a custom field
if (
!isNoneGroup(groupSelection) &&
Expand All @@ -77,11 +95,14 @@ export const useGetGroupSelector = ({
},
[
defaultGroupingOptions,
groupingId,
onGroupChangeCallback,
options,
selectedGroup,
setGroupsActivePage,
setOptions,
setSelectedGroup,
tracker,
]
);

Expand All @@ -106,6 +127,7 @@ export const useGetGroupSelector = ({
}, [defaultGroupingOptions, options.length, selectedGroup, setOptions]);

return getGroupSelector({
groupingId,
groupSelected: selectedGroup,
'data-test-subj': 'alerts-table-group-selector',
onGroupChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const defaultArgs = {
defaultGroupingOptions,
fields: [],
groupingId,
tracker: jest.fn(),
};

const groupingArgs = {
Expand All @@ -36,7 +37,7 @@ const groupingArgs = {
renderChildComponent: jest.fn(),
runtimeMappings: {},
signalIndexName: 'test',
tableId: groupingId,
groupingId,
takeActionItems: jest.fn(),
to: '2020-07-08T08:20:18.966Z',
};
Expand Down
Loading

0 comments on commit 51d50c2

Please sign in to comment.