diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx
index 4706247f2f0a7..daa58396df70b 100644
--- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx
@@ -12,6 +12,7 @@ import React from 'react';
const onGroupChange = jest.fn();
const testProps = {
+ groupingId: 'test-grouping-id',
fields: [
{
name: 'kibana.alert.rule.name',
diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx
index 210ffef50e381..f0274f7c73ab7 100644
--- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx
@@ -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 }>;
diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx
index 6b1d360e50468..149e05513ac94 100644
--- a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx
@@ -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';
@@ -98,6 +101,7 @@ const testProps = {
value: 2,
},
},
+ groupingId: 'test-grouping-id',
isLoading: false,
pagination: {
pageIndex: 0,
@@ -109,6 +113,7 @@ const testProps = {
renderChildComponent,
selectedGroup: 'kibana.alert.rule.name',
takeActionItems,
+ tracker: mockTracker,
};
describe('grouping container', () => {
@@ -171,4 +176,33 @@ describe('grouping container', () => {
createGroupFilter(testProps.selectedGroup, rule2Name)
);
});
+
+ it('Send Telemetry when each group is clicked', () => {
+ const { getAllByTestId } = render(
+
+
+
+ );
+ 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,
+ })
+ );
+ });
});
diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx
index 420656c69db41..995c9c0fcb401 100644
--- a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx
@@ -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';
@@ -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 {
badgeMetricStats?: (fieldBucket: RawBucket) => BadgeMetric[];
customMetricStats?: (fieldBucket: RawBucket) => CustomMetric[];
data?: GroupingAggregation & GroupingFieldTotalAggregation;
+ groupingId: string;
groupPanelRenderer?: (fieldBucket: RawBucket) => 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;
@@ -42,7 +51,12 @@ export interface GroupingProps {
};
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;
}
@@ -50,14 +64,17 @@ const GroupingComponent = ({
badgeMetricStats,
customMetricStats,
data,
+ groupingId,
groupPanelRenderer,
groupSelector,
inspectButton,
isLoading,
+ onToggleCallback,
pagination,
renderChildComponent,
selectedGroup,
takeActionItems,
+ tracker,
unit = defaultUnit,
}: GroupingProps) => {
const [trigger, setTrigger] = useState<
@@ -77,9 +94,9 @@ const GroupingComponent = ({
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 (
@@ -87,7 +104,10 @@ const GroupingComponent = ({
extraAction={
@@ -97,6 +117,11 @@ const GroupingComponent = ({
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]: {
@@ -104,6 +129,7 @@ const GroupingComponent = ({
selectedBucket: groupBucket,
},
});
+ onToggleCallback?.({ isOpen, groupName: group, groupNumber, groupingId });
}}
renderChildComponent={
trigger[groupKey] && trigger[groupKey].state === 'open'
@@ -121,10 +147,13 @@ const GroupingComponent = ({
customMetricStats,
data?.stackByMultipleFields0?.buckets,
groupPanelRenderer,
+ groupingId,
isLoading,
+ onToggleCallback,
renderChildComponent,
selectedGroup,
takeActionItems,
+ tracker,
trigger,
]
);
diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx
index 2a3e6d39d7a41..7a659a55eba08 100644
--- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx
@@ -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' },
@@ -25,6 +26,8 @@ const defaultArgs = {
fields: [],
groupingId,
groupingState: initialState,
+ tracker: jest.fn(),
+ onGroupChangeCallback: jest.fn(),
};
const customField = 'custom.field';
describe('useGetGroupSelector', () => {
@@ -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]: {
diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx
index 3509e095e6cc8..2b52ca141b55f 100644
--- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx
+++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx
@@ -9,10 +9,12 @@
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[];
@@ -20,6 +22,12 @@ export interface UseGetGroupSelectorArgs {
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 = ({
@@ -28,6 +36,8 @@ export const useGetGroupSelector = ({
fields,
groupingId,
groupingState,
+ onGroupChangeCallback,
+ tracker,
}: UseGetGroupSelectorArgs) => {
const { activeGroup: selectedGroup, options } =
groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup;
@@ -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) &&
@@ -77,11 +95,14 @@ export const useGetGroupSelector = ({
},
[
defaultGroupingOptions,
+ groupingId,
+ onGroupChangeCallback,
options,
selectedGroup,
setGroupsActivePage,
setOptions,
setSelectedGroup,
+ tracker,
]
);
@@ -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,
diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx
index 9d585ce4fc454..c1e1839754869 100644
--- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx
@@ -21,6 +21,7 @@ const defaultArgs = {
defaultGroupingOptions,
fields: [],
groupingId,
+ tracker: jest.fn(),
};
const groupingArgs = {
@@ -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',
};
diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx
index d81c19cafda91..c621695d6b17c 100644
--- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx
+++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx
@@ -8,6 +8,7 @@
import { FieldSpec } from '@kbn/data-views-plugin/common';
import React, { useCallback, useMemo, useReducer } from 'react';
+import { UiCounterMetricType } from '@kbn/analytics';
import { groupsReducerWithStorage, initialState } from './state/reducer';
import { GroupingProps, GroupSelectorProps } from '..';
import { useGroupingPagination } from './use_grouping_pagination';
@@ -31,14 +32,21 @@ interface Grouping {
interface GroupingArgs {
defaultGroupingOptions: GroupOption[];
-
fields: FieldSpec[];
groupingId: string;
+ onGroupChangeCallback?: (param: { groupByField: string; tableId: string }) => void;
+ tracker: (
+ type: UiCounterMetricType,
+ event: string | string[],
+ count?: number | undefined
+ ) => void;
}
export const useGrouping = ({
defaultGroupingOptions,
fields,
groupingId,
+ onGroupChangeCallback,
+ tracker,
}: GroupingArgs): Grouping => {
const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState);
@@ -53,6 +61,8 @@ export const useGrouping = ({
fields,
groupingId,
groupingState,
+ onGroupChangeCallback,
+ tracker,
});
const pagination = useGroupingPagination({ groupingId, groupingState, dispatch });
diff --git a/packages/kbn-securitysolution-grouping/src/telemetry/const.ts b/packages/kbn-securitysolution-grouping/src/telemetry/const.ts
new file mode 100644
index 0000000000000..5397d98514901
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/src/telemetry/const.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+enum TELEMETRY_EVENT {
+ GROUP_TOGGLED = 'alerts_table_toggled_',
+ GROUPED_ALERTS = 'alerts_table_group_by_',
+}
+
+export const getTelemetryEvent = {
+ groupToggled: ({
+ isOpen,
+ groupingId,
+ groupNumber,
+ }: {
+ isOpen: boolean;
+ groupingId: string;
+ groupNumber: number;
+ }) =>
+ `${TELEMETRY_EVENT.GROUP_TOGGLED}${isOpen ? 'on' : 'off'}_${groupingId}_group-${groupNumber}`,
+ groupChanged: ({ groupingId, selected }: { groupingId: string; selected: string }) =>
+ `${TELEMETRY_EVENT.GROUPED_ALERTS}${groupingId}_${selected}`,
+};
diff --git a/packages/kbn-securitysolution-grouping/tsconfig.json b/packages/kbn-securitysolution-grouping/tsconfig.json
index 5767aafe7051a..ab98ec47e3c93 100644
--- a/packages/kbn-securitysolution-grouping/tsconfig.json
+++ b/packages/kbn-securitysolution-grouping/tsconfig.json
@@ -24,5 +24,6 @@
"@kbn/kibana-react-plugin",
"@kbn/shared-svg",
"@kbn/ui-theme",
+ "@kbn/analytics"
]
}
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
index 19aaf93417a3e..5158ac09c50e7 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
@@ -9,9 +9,13 @@ import type { UiCounterMetricType } from '@kbn/analytics';
import { METRIC_TYPE } from '@kbn/analytics';
import type { SetupPlugins } from '../../../types';
+import type { AlertWorkflowStatus } from '../../types';
export { telemetryMiddleware } from './middleware';
export { METRIC_TYPE };
+export * from './telemetry_client';
+export * from './telemetry_service';
+export * from './types';
type TrackFn = (type: UiCounterMetricType, event: string | string[], count?: number) => void;
@@ -40,7 +44,6 @@ export enum TELEMETRY_EVENT {
SIEM_RULE_DISABLED = 'siem_rule_disabled',
CUSTOM_RULE_ENABLED = 'custom_rule_enabled',
CUSTOM_RULE_DISABLED = 'custom_rule_disabled',
-
// ML
SIEM_JOB_ENABLED = 'siem_job_enabled',
SIEM_JOB_DISABLED = 'siem_job_disabled',
@@ -67,3 +70,15 @@ export enum TELEMETRY_EVENT {
BREADCRUMB = 'breadcrumb_',
LEGACY_NAVIGATION = 'legacy_navigation_',
}
+
+export const getTelemetryEvent = {
+ groupedAlertsTakeAction: ({
+ tableId,
+ groupNumber,
+ status,
+ }: {
+ tableId: string;
+ groupNumber: number;
+ status: AlertWorkflowStatus;
+ }) => `alerts_table_${tableId}_group-${groupNumber}_mark-${status}`,
+};
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts
new file mode 100644
index 0000000000000..7b1c33eac9f0f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { TelemetryClientStart } from './types';
+
+export const createTelemetryClientMock = (): jest.Mocked => ({
+ reportAlertsGroupingChanged: jest.fn(),
+ reportAlertsGroupingToggled: jest.fn(),
+ reportAlertsGroupingTakeAction: jest.fn(),
+});
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts
new file mode 100644
index 0000000000000..aebe7b5b3aa55
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
+import type {
+ TelemetryClientStart,
+ ReportAlertsGroupingChangedParams,
+ ReportAlertsGroupingToggledParams,
+ ReportAlertsTakeActionParams,
+} from './types';
+import { TelemetryEventTypes } from './types';
+
+/**
+ * Client which aggregate all the available telemetry tracking functions
+ * for the plugin
+ */
+export class TelemetryClient implements TelemetryClientStart {
+ constructor(private analytics: AnalyticsServiceSetup) {}
+
+ public reportAlertsGroupingChanged = ({
+ tableId,
+ groupByField,
+ }: ReportAlertsGroupingChangedParams) => {
+ this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingChanged, {
+ tableId,
+ groupByField,
+ });
+ };
+
+ public reportAlertsGroupingToggled = ({
+ isOpen,
+ tableId,
+ groupNumber,
+ groupName,
+ }: ReportAlertsGroupingToggledParams) => {
+ this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingToggled, {
+ isOpen,
+ tableId,
+ groupNumber,
+ groupName,
+ });
+ };
+
+ public reportAlertsGroupingTakeAction = ({
+ tableId,
+ groupNumber,
+ status,
+ groupByField,
+ }: ReportAlertsTakeActionParams) => {
+ this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingTakeAction, {
+ tableId,
+ groupNumber,
+ status,
+ groupByField,
+ });
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts
new file mode 100644
index 0000000000000..d37957d2508d5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type { TelemetryEvent } from './types';
+import { TelemetryEventTypes } from './types';
+
+const alertsGroupingToggledEvent: TelemetryEvent = {
+ eventType: TelemetryEventTypes.AlertsGroupingToggled,
+ schema: {
+ isOpen: {
+ type: 'boolean',
+ _meta: {
+ description: 'on or off',
+ optional: false,
+ },
+ },
+ tableId: {
+ type: 'text',
+ _meta: {
+ description: 'Table ID',
+ optional: false,
+ },
+ },
+ groupNumber: {
+ type: 'integer',
+ _meta: {
+ description: 'Group number',
+ optional: false,
+ },
+ },
+ groupName: {
+ type: 'keyword',
+ _meta: {
+ description: 'Group value',
+ optional: true,
+ },
+ },
+ },
+};
+
+const alertsGroupingChangedEvent: TelemetryEvent = {
+ eventType: TelemetryEventTypes.AlertsGroupingChanged,
+ schema: {
+ tableId: {
+ type: 'keyword',
+ _meta: {
+ description: 'Table ID',
+ optional: false,
+ },
+ },
+ groupByField: {
+ type: 'keyword',
+ _meta: {
+ description: 'Selected field',
+ optional: false,
+ },
+ },
+ },
+};
+
+const alertsGroupingTakeActionEvent: TelemetryEvent = {
+ eventType: TelemetryEventTypes.AlertsGroupingTakeAction,
+ schema: {
+ tableId: {
+ type: 'keyword',
+ _meta: {
+ description: 'Table ID',
+ optional: false,
+ },
+ },
+ groupNumber: {
+ type: 'integer',
+ _meta: {
+ description: 'Group number',
+ optional: false,
+ },
+ },
+ status: {
+ type: 'keyword',
+ _meta: {
+ description: 'Alert status',
+ optional: false,
+ },
+ },
+ groupByField: {
+ type: 'keyword',
+ _meta: {
+ description: 'Selected field',
+ optional: false,
+ },
+ },
+ },
+};
+
+export const telemetryEvents = [
+ alertsGroupingToggledEvent,
+ alertsGroupingChangedEvent,
+ alertsGroupingTakeActionEvent,
+];
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts
new file mode 100644
index 0000000000000..519ba4527560b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { createTelemetryClientMock } from './telemetry_client.mock';
+
+export const createTelemetryServiceMock = () => createTelemetryClientMock();
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts
new file mode 100644
index 0000000000000..0c2cd9c508289
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { coreMock } from '@kbn/core/server/mocks';
+import { telemetryEvents } from './telemetry_events';
+
+import { TelemetryService } from './telemetry_service';
+import { TelemetryEventTypes } from './types';
+
+describe('TelemetryService', () => {
+ let service: TelemetryService;
+
+ beforeEach(() => {
+ service = new TelemetryService();
+ });
+
+ const getSetupParams = () => {
+ const mockCoreStart = coreMock.createSetup();
+ return {
+ analytics: mockCoreStart.analytics,
+ };
+ };
+
+ describe('#setup()', () => {
+ it('should register all the custom events', () => {
+ const setupParams = getSetupParams();
+ service.setup(setupParams);
+
+ expect(setupParams.analytics.registerEventType).toHaveBeenCalledTimes(telemetryEvents.length);
+
+ telemetryEvents.forEach((eventConfig, pos) => {
+ expect(setupParams.analytics.registerEventType).toHaveBeenNthCalledWith(
+ pos + 1,
+ eventConfig
+ );
+ });
+ });
+ });
+
+ describe('#start()', () => {
+ it('should return all the available tracking methods', () => {
+ const setupParams = getSetupParams();
+ service.setup(setupParams);
+ const telemetry = service.start();
+
+ expect(telemetry).toHaveProperty('reportAlertsGroupingChanged');
+ expect(telemetry).toHaveProperty('reportAlertsGroupingToggled');
+ expect(telemetry).toHaveProperty('reportAlertsGroupingTakeAction');
+ });
+ });
+
+ describe('#reportAlertsGroupingTakeAction', () => {
+ it('should report hosts entry click with properties', async () => {
+ const setupParams = getSetupParams();
+ service.setup(setupParams);
+ const telemetry = service.start();
+
+ telemetry.reportAlertsGroupingTakeAction({
+ tableId: 'test-groupingId',
+ groupNumber: 0,
+ status: 'closed',
+ groupByField: 'host.name',
+ });
+
+ expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
+ expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
+ TelemetryEventTypes.AlertsGroupingTakeAction,
+ {
+ tableId: 'test-groupingId',
+ groupNumber: 0,
+ status: 'closed',
+ groupByField: 'host.name',
+ }
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts
new file mode 100644
index 0000000000000..58d1c3d7d7418
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
+import { of } from 'rxjs';
+
+import type {
+ TelemetryServiceSetupParams,
+ TelemetryClientStart,
+ TelemetryEventParams,
+} from './types';
+import { telemetryEvents } from './telemetry_events';
+import { TelemetryClient } from './telemetry_client';
+
+/**
+ * Service that interacts with the Core's analytics module
+ * to trigger custom event for the Infra plugin features
+ */
+export class TelemetryService {
+ constructor(private analytics: AnalyticsServiceSetup | null = null) {}
+
+ public setup({ analytics }: TelemetryServiceSetupParams, context?: Record) {
+ this.analytics = analytics;
+ if (context) {
+ const context$ = of(context);
+
+ analytics.registerContextProvider({
+ name: 'detection_response',
+ // RxJS Observable that emits every time the context changes.
+ context$,
+ // Similar to the `reportEvent` API, schema defining the structure of the expected output of the context$ observable.
+ schema: {
+ prebuiltRulesPackageVersion: {
+ type: 'keyword',
+ _meta: { description: 'The version of prebuilt rules', optional: true },
+ },
+ },
+ });
+ }
+ telemetryEvents.forEach((eventConfig) =>
+ analytics.registerEventType(eventConfig)
+ );
+ }
+
+ public start(): TelemetryClientStart {
+ if (!this.analytics) {
+ throw new Error(
+ 'The TelemetryService.setup() method has not been invoked, be sure to call it during the plugin setup.'
+ );
+ }
+
+ return new TelemetryClient(this.analytics);
+ }
+}
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts
new file mode 100644
index 0000000000000..2610dc51c1e41
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { RootSchema } from '@kbn/analytics-client';
+import type { AnalyticsServiceSetup } from '@kbn/core/public';
+
+export interface TelemetryServiceSetupParams {
+ analytics: AnalyticsServiceSetup;
+}
+
+export enum TelemetryEventTypes {
+ AlertsGroupingChanged = 'Alerts Grouping Changed',
+ AlertsGroupingToggled = 'Alerts Grouping Toggled',
+ AlertsGroupingTakeAction = 'Alerts Grouping Take Action',
+}
+
+export interface ReportAlertsGroupingChangedParams {
+ tableId: string;
+ groupByField: string;
+}
+
+export interface ReportAlertsGroupingToggledParams {
+ isOpen: boolean;
+ tableId: string;
+ groupNumber: number;
+ groupName?: string | undefined;
+}
+
+export interface ReportAlertsTakeActionParams {
+ tableId: string;
+ groupNumber: number;
+ status: 'open' | 'closed' | 'acknowledged';
+ groupByField: string;
+}
+
+export type TelemetryEventParams =
+ | ReportAlertsGroupingChangedParams
+ | ReportAlertsGroupingToggledParams
+ | ReportAlertsTakeActionParams;
+
+export interface TelemetryClientStart {
+ reportAlertsGroupingChanged(params: ReportAlertsGroupingChangedParams): void;
+ reportAlertsGroupingToggled(params: ReportAlertsGroupingToggledParams): void;
+ reportAlertsGroupingTakeAction(params: ReportAlertsTakeActionParams): void;
+}
+
+export type TelemetryEvent =
+ | {
+ eventType: TelemetryEventTypes.AlertsGroupingToggled;
+ schema: RootSchema;
+ }
+ | {
+ eventType: TelemetryEventTypes.AlertsGroupingChanged;
+ schema: RootSchema;
+ }
+ | {
+ eventType: TelemetryEventTypes.AlertsGroupingTakeAction;
+ schema: RootSchema;
+ };
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx
index 57ef10a5285e6..448bd297c50c0 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx
@@ -45,6 +45,7 @@ import {
useGroupTakeActionsItems,
} from './grouping_settings';
import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions';
+import { track } from '../../../common/lib/telemetry';
const ALERTS_GROUPING_ID = 'alerts-grouping';
@@ -82,16 +83,19 @@ export const GroupedAlertsTableComponent: React.FC =
renderChildComponent,
}) => {
const dispatch = useDispatch();
+
const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView(
SourcererScopeName.detections
);
- const kibana = useKibana();
+ const {
+ services: { uiSettings, telemetry },
+ } = useKibana();
const getGlobalQuery = useCallback(
(customFilters: Filter[]) => {
if (browserFields != null && indexPattern != null) {
return combineQueries({
- config: getEsQueryConfig(kibana.services.uiSettings),
+ config: getEsQueryConfig(uiSettings),
dataProviders: [],
indexPattern,
browserFields,
@@ -107,13 +111,22 @@ export const GroupedAlertsTableComponent: React.FC =
}
return null;
},
- [browserFields, defaultFilters, globalFilters, globalQuery, indexPattern, kibana, to, from]
+ [browserFields, indexPattern, uiSettings, defaultFilters, globalFilters, from, to, globalQuery]
+ );
+
+ const onGroupChangeCallback = useCallback(
+ (param) => {
+ telemetry.reportAlertsGroupingChanged(param);
+ },
+ [telemetry]
);
const { groupSelector, getGrouping, selectedGroup, pagination } = useGrouping({
defaultGroupingOptions: getDefaultGroupingOptions(tableId),
groupingId: tableId,
fields: indexPattern.fields,
+ onGroupChangeCallback,
+ tracker: track,
});
const resetPagination = pagination.reset;
@@ -221,9 +234,14 @@ export const GroupedAlertsTableComponent: React.FC =
});
const getTakeActionItems = useCallback(
- (groupFilters: Filter[]) =>
- takeActionItems(getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery),
- [defaultFilters, getGlobalQuery, takeActionItems]
+ (groupFilters: Filter[], groupNumber: number) =>
+ takeActionItems({
+ query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery,
+ tableId,
+ groupNumber,
+ selectedGroup,
+ }),
+ [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems]
);
const groupedAlerts = useMemo(
@@ -236,12 +254,17 @@ export const GroupedAlertsTableComponent: React.FC =
customMetricStats: (fieldBucket: RawBucket) =>
getSelectedGroupCustomMetrics(selectedGroup, fieldBucket),
data: alertsGroupsData?.aggregations,
+ groupingId: tableId,
groupPanelRenderer: (fieldBucket: RawBucket) =>
getSelectedGroupButtonContent(selectedGroup, fieldBucket),
inspectButton: inspect,
isLoading: loading || isLoadingGroups,
+ onToggleCallback: (param) => {
+ telemetry.reportAlertsGroupingToggled({ ...param, tableId: param.groupingId });
+ },
renderChildComponent,
takeActionItems: getTakeActionItems,
+ tracker: track,
unit: defaultUnit,
}),
[
@@ -253,6 +276,8 @@ export const GroupedAlertsTableComponent: React.FC =
loading,
renderChildComponent,
selectedGroup,
+ tableId,
+ telemetry,
]
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx
index 4bd1d0b6005f4..f84305dcb3b37 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx
@@ -25,6 +25,11 @@ describe('useGroupTakeActionsItems', () => {
const wrapperContainer: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
{children}
);
+ const getActionItemsParams = {
+ tableId: 'mock-id',
+ groupNumber: 0,
+ selectedGroup: 'test',
+ };
it('returns array take actions items available for alerts table if showAlertStatusActions is true', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
@@ -38,7 +43,7 @@ describe('useGroupTakeActionsItems', () => {
}
);
await waitForNextUpdate();
- expect(result.current().length).toEqual(3);
+ expect(result.current(getActionItemsParams).length).toEqual(3);
});
});
@@ -55,7 +60,7 @@ describe('useGroupTakeActionsItems', () => {
}
);
await waitForNextUpdate();
- expect(result.current().length).toEqual(0);
+ expect(result.current(getActionItemsParams).length).toEqual(0);
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx
index d1e4d61e30dd5..d2baadb99d124 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx
@@ -7,6 +7,7 @@
import React, { useMemo, useCallback } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Status } from '../../../../../common/detection_engine/schemas/common';
import type { inputsModel } from '../../../../common/store';
import { inputsSelectors } from '../../../../common/store';
@@ -27,7 +28,8 @@ import {
import { FILTER_ACKNOWLEDGED, FILTER_CLOSED, FILTER_OPEN } from '../../../../../common/types';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import * as i18n from '../translations';
-
+import { getTelemetryEvent, METRIC_TYPE, track } from '../../../../common/lib/telemetry';
+import type { StartServices } from '../../../../types';
export interface TakeActionsProps {
currentStatus?: Status;
indexName: string;
@@ -47,6 +49,21 @@ export const useGroupTakeActionsItems = ({
const refetchQuery = useCallback(() => {
globalQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)());
}, [globalQueries]);
+ const {
+ services: { telemetry },
+ } = useKibana();
+
+ const reportAlertsGroupingTakeActionClick = useCallback(
+ (params: {
+ tableId: string;
+ groupNumber: number;
+ status: 'open' | 'closed' | 'acknowledged';
+ groupByField: string;
+ }) => {
+ telemetry.reportAlertsGroupingTakeAction(params);
+ },
+ [telemetry]
+ );
const onUpdateSuccess = useCallback(
(updated: number, conflicts: number, newStatus: AlertWorkflowStatus) => {
@@ -113,13 +130,36 @@ export const useGroupTakeActionsItems = ({
);
const onClickUpdate = useCallback(
- async (status: AlertWorkflowStatus, query?: string) => {
+ async ({
+ groupNumber,
+ query,
+ status,
+ tableId,
+ selectedGroup,
+ }: {
+ groupNumber: number;
+ query?: string;
+ status: AlertWorkflowStatus;
+ tableId: string;
+ selectedGroup: string;
+ }) => {
if (query) {
startTransaction({ name: APM_USER_INTERACTIONS.BULK_QUERY_STATUS_UPDATE });
} else {
startTransaction({ name: APM_USER_INTERACTIONS.STATUS_UPDATE });
}
+ track(
+ METRIC_TYPE.CLICK,
+ getTelemetryEvent.groupedAlertsTakeAction({ tableId, groupNumber, status })
+ );
+ reportAlertsGroupingTakeActionClick({
+ tableId,
+ groupNumber,
+ status,
+ groupByField: selectedGroup,
+ });
+
try {
const response = await updateAlertStatus({
index: indexName,
@@ -133,16 +173,27 @@ export const useGroupTakeActionsItems = ({
}
},
[
+ startTransaction,
+ reportAlertsGroupingTakeActionClick,
updateAlertStatus,
indexName,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
- startTransaction,
]
);
const items = useMemo(() => {
- const getActionItems = (query?: string) => {
+ const getActionItems = ({
+ query,
+ tableId,
+ groupNumber,
+ selectedGroup,
+ }: {
+ query?: string;
+ tableId: string;
+ groupNumber: number;
+ selectedGroup: string;
+ }) => {
const actionItems: JSX.Element[] = [];
if (showAlertStatusActions) {
if (currentStatus !== FILTER_OPEN) {
@@ -150,7 +201,15 @@ export const useGroupTakeActionsItems = ({
onClickUpdate(FILTER_OPEN as AlertWorkflowStatus, query)}
+ onClick={() =>
+ onClickUpdate({
+ groupNumber,
+ query,
+ selectedGroup,
+ status: FILTER_OPEN as AlertWorkflowStatus,
+ tableId,
+ })
+ }
>
{BULK_ACTION_OPEN_SELECTED}
@@ -161,7 +220,15 @@ export const useGroupTakeActionsItems = ({
onClickUpdate(FILTER_ACKNOWLEDGED as AlertWorkflowStatus, query)}
+ onClick={() =>
+ onClickUpdate({
+ groupNumber,
+ query,
+ selectedGroup,
+ status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus,
+ tableId,
+ })
+ }
>
{BULK_ACTION_ACKNOWLEDGED_SELECTED}
@@ -172,7 +239,15 @@ export const useGroupTakeActionsItems = ({
onClickUpdate(FILTER_CLOSED as AlertWorkflowStatus, query)}
+ onClick={() =>
+ onClickUpdate({
+ groupNumber,
+ query,
+ selectedGroup,
+ status: FILTER_CLOSED as AlertWorkflowStatus,
+ tableId,
+ })
+ }
>
{BULK_ACTION_CLOSE_SELECTED}
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 78680086d45c0..634e488bf14ce 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -30,7 +30,7 @@ import type {
StartedSubPlugins,
StartPluginsDependencies,
} from './types';
-import { initTelemetry } from './common/lib/telemetry';
+import { initTelemetry, TelemetryService } from './common/lib/telemetry';
import { KibanaServices } from './common/lib/kibana/services';
import { SOLUTION_NAME } from './common/translations';
@@ -83,6 +83,8 @@ export class Plugin implements IPlugin();
@@ -120,6 +124,10 @@ export class Plugin implements IPlugin SecuritySolutionTemplateWrapper,
},
+ telemetry: this.telemetry.start(),
};
return services;
};
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index cb16591da9fb4..9ee21755d61ad 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -60,7 +60,7 @@ import type { CloudDefend } from './cloud_defend';
import type { ThreatIntelligence } from './threat_intelligence';
import type { SecuritySolutionTemplateWrapper } from './app/home/template_wrapper';
import type { Explore } from './explore';
-
+import type { TelemetryClientStart } from './common/lib/telemetry';
export interface SetupPlugins {
home?: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
@@ -119,6 +119,7 @@ export type StartServices = CoreStart &
securityLayout: {
getPluginWrapper: () => typeof SecuritySolutionTemplateWrapper;
};
+ telemetry: TelemetryClientStart;
};
export interface PluginSetup {
diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json
index c7b13f92ff070..346b2bf3289cf 100644
--- a/x-pack/plugins/security_solution/tsconfig.json
+++ b/x-pack/plugins/security_solution/tsconfig.json
@@ -147,6 +147,8 @@
"@kbn/alerts-as-data-utils",
"@kbn/expandable-flyout",
"@kbn/securitysolution-grouping",
+ "@kbn/core-analytics-server",
+ "@kbn/analytics-client",
"@kbn/security-solution-side-nav",
]
}