From 730f7c73b275aa16c8099f9e6b5409b69dbe70b1 Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:52:43 +0100
Subject: [PATCH] [SecuritySolution][Onboarding] Send Telemetry when
integration tabs or cards clicked (#196291)
## Summary
https://github.com/elastic/kibana/issues/196145
To verify:
1. Add these lines to `kibana.dev.yml`
```
logging.browser.root.level: debug
telemetry.optIn: true
```
2. In the onboarding hub, expand the integration card.
It should log `onboarding_tab_${tabId}` on tabs clicked.
https://github.com/user-attachments/assets/bd30c9ed-7c99-4ca0-93e7-6d9bf0146e62
It should log `onboarding_card_${integrationId}` on integration cards
clicked.
https://github.com/user-attachments/assets/58750d88-7bbf-4b27-8e54-587f3f6f32c2
3. Manage integrations callout link clicked::
`onboarding_manage_integrations`;
4. Endpoint callout link clicked: `onboarding_endpoint_learn_more`;
5. Agentless callout link clicked: `onboarding_agentless_learn_more`;
6. Agent still required callout link clicked:
`onboarding_agent_required`;
### 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
(cherry picked from commit 2b270897a3f22ed5ca04ef173895cfa8660f9ea2)
---
.../public/common/lib/telemetry/constants.ts | 2 +
.../common/lib/__mocks__/telemetry.ts | 8 ++++
.../public/onboarding/common/lib/telemetry.ts | 12 ++++++
.../callouts/agent_required_callout.test.tsx | 10 +++++
.../callouts/agent_required_callout.tsx | 14 ++++--
.../agentless_available_callout.test.tsx | 16 +++++--
.../callouts/agentless_available_callout.tsx | 9 +++-
.../callouts/endpoint_callout.test.tsx | 43 +++++++++++++++++++
.../callouts/endpoint_callout.tsx | 8 +++-
.../callouts/manage_integrations_callout.tsx | 15 ++++++-
.../cards/integrations/constants.ts | 6 +++
.../integration_card_grid_tabs.test.tsx | 29 +++++++++++++
.../integration_card_grid_tabs.tsx | 4 ++
.../use_integration_card_list.test.ts | 17 +++++++-
.../integrations/use_integration_card_list.ts | 4 ++
15 files changed, 185 insertions(+), 12 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/onboarding/common/lib/__mocks__/telemetry.ts
create mode 100644 x-pack/plugins/security_solution/public/onboarding/common/lib/telemetry.ts
create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts
index 5126d75178f5f..cb247891d79b3 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts
@@ -35,6 +35,8 @@ export enum TELEMETRY_EVENT {
DASHBOARD = 'navigate_to_dashboard',
CREATE_DASHBOARD = 'create_dashboard',
+ ONBOARDING = 'onboarding',
+
// value list
OPEN_VALUE_LIST_MODAL = 'open_value_list_modal',
CREATE_VALUE_LIST_ITEM = 'create_value_list_item',
diff --git a/x-pack/plugins/security_solution/public/onboarding/common/lib/__mocks__/telemetry.ts b/x-pack/plugins/security_solution/public/onboarding/common/lib/__mocks__/telemetry.ts
new file mode 100644
index 0000000000000..5d1c3feb56ed9
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/common/lib/__mocks__/telemetry.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const trackOnboardingLinkClick = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/onboarding/common/lib/telemetry.ts b/x-pack/plugins/security_solution/public/onboarding/common/lib/telemetry.ts
new file mode 100644
index 0000000000000..a88ae651ae600
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/common/lib/telemetry.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../common/lib/telemetry';
+
+export const trackOnboardingLinkClick = (linkId: string) => {
+ track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.ONBOARDING}_${linkId}`);
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx
index dbd0c105d27a1..53e8b6c34e8f2 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx
@@ -14,8 +14,10 @@ import React from 'react';
import { render } from '@testing-library/react';
import { AgentRequiredCallout } from './agent_required_callout';
import { TestProviders } from '../../../../../../common/mock/test_providers';
+import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry';
jest.mock('../../../../../../common/lib/kibana');
+jest.mock('../../../../../common/lib/telemetry');
describe('AgentRequiredCallout', () => {
beforeEach(() => {
@@ -30,4 +32,12 @@ describe('AgentRequiredCallout', () => {
).toBeInTheDocument();
expect(getByTestId('agentLink')).toBeInTheDocument();
});
+
+ it('should track the agent link click', () => {
+ const { getByTestId } = render(, { wrapper: TestProviders });
+
+ getByTestId('agentLink').click();
+
+ expect(trackOnboardingLinkClick).toHaveBeenCalledWith('agent_required');
+ });
});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx
index aad22c959bc65..b1d18b138487b 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx
@@ -11,16 +11,22 @@ import { EuiIcon } from '@elastic/eui';
import { LinkAnchor } from '../../../../../../common/components/links';
import { CardCallOut } from '../../common/card_callout';
import { useNavigation } from '../../../../../../common/lib/kibana';
-import { FLEET_APP_ID, ADD_AGENT_PATH } from '../constants';
+import { FLEET_APP_ID, ADD_AGENT_PATH, TELEMETRY_AGENT_REQUIRED } from '../constants';
+import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry';
const fleetAgentLinkProps = { appId: FLEET_APP_ID, path: ADD_AGENT_PATH };
export const AgentRequiredCallout = React.memo(() => {
const { getAppUrl, navigateTo } = useNavigation();
const addAgentLink = getAppUrl(fleetAgentLinkProps);
- const onAddAgentClick = useCallback(() => {
- navigateTo(fleetAgentLinkProps);
- }, [navigateTo]);
+ const onAddAgentClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ trackOnboardingLinkClick(TELEMETRY_AGENT_REQUIRED);
+ navigateTo(fleetAgentLinkProps);
+ },
+ [navigateTo]
+ );
return (
({
- useKibana: jest.fn(),
-}));
+jest.mock('../../../../../../common/lib/kibana');
+jest.mock('../../../../../common/lib/telemetry');
describe('AgentlessAvailableCallout', () => {
const mockUseKibana = useKibana as jest.Mock;
@@ -62,4 +62,14 @@ describe('AgentlessAvailableCallout', () => {
).toBeInTheDocument();
expect(getByTestId('agentlessLearnMoreLink')).toBeInTheDocument();
});
+
+ it('should track the agentless learn more link click', () => {
+ const { getByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+
+ getByTestId('agentlessLearnMoreLink').click();
+
+ expect(trackOnboardingLinkClick).toHaveBeenCalledWith('agentless_learn_more');
+ });
});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx
index f802f83efb7e5..eaf8cbaa3b287 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx
@@ -5,19 +5,25 @@
* 2.0.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiIcon, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { useKibana } from '../../../../../../common/lib/kibana';
import { LinkAnchor } from '../../../../../../common/components/links';
+import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry';
import { CardCallOut } from '../../common/card_callout';
+import { TELEMETRY_AGENTLESS_LEARN_MORE } from '../constants';
export const AgentlessAvailableCallout = React.memo(() => {
const { euiTheme } = useEuiTheme();
const { docLinks } = useKibana().services;
+ const onClick = useCallback(() => {
+ trackOnboardingLinkClick(TELEMETRY_AGENTLESS_LEARN_MORE);
+ }, []);
+
/* @ts-expect-error: add the blog link to `packages/kbn-doc-links/src/get_doc_links.ts` when it is ready and remove this exit condition*/
if (!docLinks.links.fleet.agentlessBlog) {
return null;
@@ -54,6 +60,7 @@ export const AgentlessAvailableCallout = React.memo(() => {
{
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the callout', () => {
+ const { getByTestId, getByText } = render(, { wrapper: TestProviders });
+
+ expect(
+ getByText('Orchestrate response across endpoint vendors with bidirectional integrations')
+ ).toBeInTheDocument();
+ expect(getByTestId('endpointLearnMoreLink')).toBeInTheDocument();
+ });
+
+ it('should track the agent link click', () => {
+ const { getByTestId } = render(, { wrapper: TestProviders });
+
+ getByTestId('endpointLearnMoreLink').click();
+
+ expect(trackOnboardingLinkClick).toHaveBeenCalledWith('endpoint_learn_more');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx
index 2ff48a1992d1d..d5b0199c9f401 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiIcon, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
@@ -13,10 +13,15 @@ import { css } from '@emotion/react';
import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
import { LinkAnchor } from '../../../../../../common/components/links';
import { CardCallOut } from '../../common/card_callout';
+import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry';
+import { TELEMETRY_ENDPOINT_LEARN_MORE } from '../constants';
export const EndpointCallout = React.memo(() => {
const { euiTheme } = useEuiTheme();
const { docLinks } = useKibana().services;
+ const onClick = useCallback(() => {
+ trackOnboardingLinkClick(TELEMETRY_ENDPOINT_LEARN_MORE);
+ }, []);
return (
{
data-test-subj="endpointLearnMoreLink"
external={true}
target="_blank"
+ onClick={onClick}
>
{
const { href: integrationUrl, onClick: onAddIntegrationClicked } = useAddIntegrationsUrl();
+ const onClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ trackOnboardingLinkClick(TELEMETRY_MANAGE_INTEGRATIONS);
+ onAddIntegrationClicked(e);
+ },
+ [onAddIntegrationClicked]
+ );
+
if (!installedIntegrationsCount) {
return null;
}
@@ -41,7 +52,7 @@ export const ManageIntegrationsCallout = React.memo(
),
link: (
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts
index e245de6129478..c748f5205e7aa 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts
@@ -24,3 +24,9 @@ export const SCROLL_ELEMENT_ID = 'integrations-scroll-container';
export const SEARCH_FILTER_CATEGORIES: CategoryFacet[] = [];
export const WITH_SEARCH_BOX_HEIGHT = '568px';
export const WITHOUT_SEARCH_BOX_HEIGHT = '513px';
+export const TELEMETRY_MANAGE_INTEGRATIONS = `manage_integrations`;
+export const TELEMETRY_ENDPOINT_LEARN_MORE = `endpoint_learn_more`;
+export const TELEMETRY_AGENTLESS_LEARN_MORE = `agentless_learn_more`;
+export const TELEMETRY_AGENT_REQUIRED = `agent_required`;
+export const TELEMETRY_INTEGRATION_CARD = `card`;
+export const TELEMETRY_INTEGRATION_TAB = `tab`;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx
index f55cc8cd50b2d..c88ffb6a598b7 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx
@@ -15,9 +15,11 @@ import {
useStoredIntegrationTabId,
} from '../../../../hooks/use_stored_state';
import { DEFAULT_TAB } from './constants';
+import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry';
jest.mock('../../../onboarding_context');
jest.mock('../../../../hooks/use_stored_state');
+jest.mock('../../../../common/lib/telemetry');
jest.mock('../../../../../common/lib/kibana', () => ({
...jest.requireActual('../../../../../common/lib/kibana'),
@@ -118,6 +120,33 @@ describe('IntegrationsCardGridTabsComponent', () => {
expect(mockSetTabId).toHaveBeenCalledWith('user');
});
+ it('tracks the tab clicks', () => {
+ (useStoredIntegrationTabId as jest.Mock).mockReturnValue(['recommended', mockSetTabId]);
+
+ mockUseAvailablePackages.mockReturnValue({
+ isLoading: false,
+ filteredCards: [],
+ setCategory: mockSetCategory,
+ setSelectedSubCategory: mockSetSelectedSubCategory,
+ setSearchTerm: mockSetSearchTerm,
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ const tabButton = getByTestId('user');
+
+ act(() => {
+ fireEvent.click(tabButton);
+ });
+
+ expect(trackOnboardingLinkClick).toHaveBeenCalledWith('tab_user');
+ });
+
it('renders no search tools when showSearchTools is false', async () => {
mockUseAvailablePackages.mockReturnValue({
isLoading: false,
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx
index fc30fb0d6c617..e1ce7f5cdecf1 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx
@@ -21,6 +21,7 @@ import {
LOADING_SKELETON_TEXT_LINES,
SCROLL_ELEMENT_ID,
SEARCH_FILTER_CATEGORIES,
+ TELEMETRY_INTEGRATION_TAB,
WITHOUT_SEARCH_BOX_HEIGHT,
WITH_SEARCH_BOX_HEIGHT,
} from './constants';
@@ -28,6 +29,7 @@ import { INTEGRATION_TABS, INTEGRATION_TABS_BY_ID } from './integration_tabs_con
import { useIntegrationCardList } from './use_integration_card_list';
import { IntegrationTabId } from './types';
import { IntegrationCardTopCallout } from './callouts/integration_card_top_callout';
+import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry';
export interface IntegrationsCardGridTabsProps {
installedIntegrationsCount: number;
@@ -55,8 +57,10 @@ export const IntegrationsCardGridTabsComponent = React.memo {
const id = stringId as IntegrationTabId;
+ const trackId = `${TELEMETRY_INTEGRATION_TAB}_${id}`;
scrollElement.current?.scrollTo?.(0, 0);
setSelectedTabIdToStorage(id);
+ trackOnboardingLinkClick(trackId);
},
[setSelectedTabIdToStorage]
);
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts
index 9c4e1978f27b7..19ab340276b83 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts
@@ -6,12 +6,14 @@
*/
import { renderHook } from '@testing-library/react-hooks';
import { useIntegrationCardList } from './use_integration_card_list';
+import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry';
+jest.mock('../../../../common/lib/telemetry');
jest.mock('../../../../../common/lib/kibana', () => ({
...jest.requireActual('../../../../../common/lib/kibana'),
useNavigation: jest.fn().mockReturnValue({
navigateTo: jest.fn(),
- getAppUrl: jest.fn(),
+ getAppUrl: jest.fn().mockReturnValue(''),
}),
}));
@@ -73,4 +75,17 @@ describe('useIntegrationCardList', () => {
expect(result.current).toEqual([mockFilteredCards.featuredCards['epr:endpoint']]);
});
+
+ it('tracks integration card click', () => {
+ const { result } = renderHook(() =>
+ useIntegrationCardList({
+ integrationsList: mockIntegrationsList,
+ })
+ );
+
+ const card = result.current[0];
+ card.onCardClick?.();
+
+ expect(trackOnboardingLinkClick).toHaveBeenCalledWith('card_epr:endpoint');
+ });
});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts
index 2a9675f91e9a8..ccea5299551c1 100644
--- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts
@@ -20,8 +20,10 @@ import {
MAX_CARD_HEIGHT_IN_PX,
ONBOARDING_APP_ID,
ONBOARDING_LINK,
+ TELEMETRY_INTEGRATION_CARD,
} from './constants';
import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana';
+import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry';
const addPathParamToUrl = (url: string, onboardingLink: string) => {
const encoded = encodeURIComponent(onboardingLink);
@@ -97,6 +99,8 @@ const addSecuritySpecificProps = ({
showInstallationStatus: true,
url,
onCardClick: () => {
+ const trackId = `${TELEMETRY_INTEGRATION_CARD}_${card.id}`;
+ trackOnboardingLinkClick(trackId);
if (url.startsWith(APP_INTEGRATIONS_PATH)) {
navigateTo({
appId: INTEGRATION_APP_ID,