From 51d50c2cabfb03e560fc462f1815857d9f67535d Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 10 Mar 2023 20:26:40 +0000 Subject: [PATCH] [SecuritySolution] Add UI tracking for grouping options (#151708) ## Summary Relevant issues: Overall telemetry: https://github.com/elastic/kibana/issues/144945 Alerts telemetry: https://github.com/elastic/kibana/issues/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 <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Pablo Neves Machado --- .../components/group_selector/index.test.tsx | 1 + .../src/components/group_selector/index.tsx | 1 + .../src/components/grouping.test.tsx | 34 ++++++ .../src/components/grouping.tsx | 37 ++++++- .../src/hooks/use_get_group_selector.test.tsx | 51 +++++++++ .../src/hooks/use_get_group_selector.tsx | 22 ++++ .../src/hooks/use_grouping.test.tsx | 3 +- .../src/hooks/use_grouping.tsx | 12 ++- .../src/telemetry/const.ts | 26 +++++ .../tsconfig.json | 1 + .../public/common/lib/telemetry/index.ts | 17 ++- .../lib/telemetry/telemetry_client.mock.ts | 14 +++ .../common/lib/telemetry/telemetry_client.ts | 61 +++++++++++ .../common/lib/telemetry/telemetry_events.ts | 102 ++++++++++++++++++ .../lib/telemetry/telemetry_service.mock.ts | 10 ++ .../lib/telemetry/telemetry_service.test.ts | 80 ++++++++++++++ .../common/lib/telemetry/telemetry_service.ts | 57 ++++++++++ .../public/common/lib/telemetry/types.ts | 63 +++++++++++ .../alerts_table/alerts_grouping.tsx | 37 +++++-- .../group_take_action_items.test.tsx | 9 +- .../group_take_action_items.tsx | 89 +++++++++++++-- .../security_solution/public/plugin.tsx | 11 +- .../plugins/security_solution/public/types.ts | 3 +- .../plugins/security_solution/tsconfig.json | 2 + 24 files changed, 719 insertions(+), 24 deletions(-) create mode 100644 packages/kbn-securitysolution-grouping/src/telemetry/const.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts 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", ] }