[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
This commit is contained in:
Angela Chuang 2024-10-22 16:52:43 +01:00 committed by GitHub
parent b562288289
commit 2b270897a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 185 additions and 12 deletions

View file

@ -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',

View file

@ -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();

View file

@ -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}`);
};

View file

@ -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(<AgentRequiredCallout />, { wrapper: TestProviders });
getByTestId('agentLink').click();
expect(trackOnboardingLinkClick).toHaveBeenCalledWith('agent_required');
});
});

View file

@ -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<HTMLAnchorElement>) => {
e.preventDefault();
trackOnboardingLinkClick(TELEMETRY_AGENT_REQUIRED);
navigateTo(fleetAgentLinkProps);
},
[navigateTo]
);
return (
<CardCallOut

View file

@ -10,10 +10,10 @@ import React from 'react';
import { TestProviders } from '../../../../../../common/mock/test_providers';
import { AgentlessAvailableCallout } from './agentless_available_callout';
import { useKibana } from '../../../../../../common/lib/kibana';
import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry';
jest.mock('../../../../../../common/lib/kibana', () => ({
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(<AgentlessAvailableCallout />, {
wrapper: TestProviders,
});
getByTestId('agentlessLearnMoreLink').click();
expect(trackOnboardingLinkClick).toHaveBeenCalledWith('agentless_learn_more');
});
});

View file

@ -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(() => {
<LinkAnchor
/* @ts-expect-error-next-line */
href={docLinks.links.fleet.agentlessBlog}
onClick={onClick}
data-test-subj="agentlessLearnMoreLink"
external={true}
target="_blank"

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { EndpointCallout } from './endpoint_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('EndpointCallout', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the callout', () => {
const { getByTestId, getByText } = render(<EndpointCallout />, { 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(<EndpointCallout />, { wrapper: TestProviders });
getByTestId('endpointLearnMoreLink').click();
expect(trackOnboardingLinkClick).toHaveBeenCalledWith('endpoint_learn_more');
});
});

View file

@ -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 (
<CardCallOut
@ -51,6 +56,7 @@ export const EndpointCallout = React.memo(() => {
data-test-subj="endpointLearnMoreLink"
external={true}
target="_blank"
onClick={onClick}
>
<FormattedMessage
id="xpack.securitySolution.onboarding.integrationsCard.callout.button.endpointLearnMoreLink"

View file

@ -4,18 +4,29 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiIcon } from '@elastic/eui';
import { LinkAnchor } from '../../../../../../common/components/links';
import { CardCallOut } from '../../common/card_callout';
import { useAddIntegrationsUrl } from '../../../../../../common/hooks/use_add_integrations_url';
import { trackOnboardingLinkClick } from '../../../../../common/lib/telemetry';
import { TELEMETRY_MANAGE_INTEGRATIONS } from '../constants';
export const ManageIntegrationsCallout = React.memo(
({ installedIntegrationsCount }: { installedIntegrationsCount: number }) => {
const { href: integrationUrl, onClick: onAddIntegrationClicked } = useAddIntegrationsUrl();
const onClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
trackOnboardingLinkClick(TELEMETRY_MANAGE_INTEGRATIONS);
onAddIntegrationClicked(e);
},
[onAddIntegrationClicked]
);
if (!installedIntegrationsCount) {
return null;
}
@ -41,7 +52,7 @@ export const ManageIntegrationsCallout = React.memo(
),
link: (
<LinkAnchor
onClick={onAddIntegrationClicked}
onClick={onClick}
href={integrationUrl}
data-test-subj="manageIntegrationsLink"
>

View file

@ -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`;

View file

@ -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(
<IntegrationsCardGridTabsComponent
{...props}
useAvailablePackages={mockUseAvailablePackages}
/>
);
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,

View file

@ -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<IntegrationsCardGrid
const onTabChange = useCallback(
(stringId: string) => {
const id = stringId as IntegrationTabId;
const trackId = `${TELEMETRY_INTEGRATION_TAB}_${id}`;
scrollElement.current?.scrollTo?.(0, 0);
setSelectedTabIdToStorage(id);
trackOnboardingLinkClick(trackId);
},
[setSelectedTabIdToStorage]
);

View file

@ -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');
});
});

View file

@ -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,