Skip to content

Commit

Permalink
[SecuritySolution][Onboarding] Send Telemetry when header/footer card…
Browse files Browse the repository at this point in the history
…s are clicked (#196495)

## Summary

#196145

To verify:


1. Add these lines to kibana.dev.yml
```
logging.browser.root.level: debug
telemetry.optIn: true
```
\
2. In the onboarding hub, click on header cards.
It should log `onboarding_card_${cardId}` on cards clicked.
<img width="1101" alt="Screenshot 2024-10-16 at 10 30 58"
src="https://github.com/user-attachments/assets/902848f2-fdc5-412d-bfe0-9ed51ba87c56">
<img width="1258" alt="Screenshot 2024-10-15 at 16 54 32"
src="https://github.com/user-attachments/assets/883a49a2-cd78-4438-91bb-21b2842b8893">

\
3. It should log `onboarding_footer_link_${footerLinkId}` on footer
links visited.
<img width="1019" alt="Screenshot 2024-10-16 at 10 31 26"
src="https://github.com/user-attachments/assets/a7ff80a7-a30d-42e9-84d3-5a14fd243022">

<img width="1200" alt="Screenshot 2024-10-15 at 17 29 59"
src="https://github.com/user-attachments/assets/3034ca61-425b-47f5-a415-8bf6065f2c6f">


### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
  • Loading branch information
agusruidiazgd authored Oct 23, 2024
1 parent d8149bf commit e38b4df
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -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.
*/

export const TELEMETRY_HEADER_CARD = `header_card`;

export enum OnboardingHeaderCardId {
video = 'video',
teammates = 'teammates',
demo = 'demo',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 TELEMETRY_FOOTER_LINK = `footer_link`;

export enum OnboardingFooterLinkItemId {
video = 'video',
documentation = 'documentation',
demo = 'demo',
forum = 'forum',
labs = 'labs',
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import documentation from './images/documentation.png';
import forum from './images/forum.png';
import demo from './images/demo.png';
import labs from './images/labs.png';
import { OnboardingFooterLinkItemId } from './constants';

export const footerItems = [
{
icon: documentation,
key: 'documentation',
id: OnboardingFooterLinkItemId.documentation,
title: i18n.translate('xpack.securitySolution.onboarding.footer.documentation.title', {
defaultMessage: 'Browse documentation',
}),
Expand All @@ -32,7 +33,7 @@ export const footerItems = [
},
{
icon: forum,
key: 'forum',
id: OnboardingFooterLinkItemId.forum,
title: i18n.translate('xpack.securitySolution.onboarding.footer.forum.title', {
defaultMessage: 'Explore forum',
}),
Expand All @@ -48,7 +49,7 @@ export const footerItems = [
},
{
icon: demo,
key: 'demo',
id: OnboardingFooterLinkItemId.demo,
title: i18n.translate('xpack.securitySolution.onboarding.footer.demo.title', {
defaultMessage: 'View demo project',
}),
Expand All @@ -64,7 +65,7 @@ export const footerItems = [
},
{
icon: labs,
key: 'labs',
id: OnboardingFooterLinkItemId.labs,
title: i18n.translate('xpack.securitySolution.onboarding.footer.labs.title', {
defaultMessage: 'Elastic Security Labs',
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { trackOnboardingLinkClick } from '../../common/lib/telemetry';
import { FooterLinkItem } from './onboarding_footer';
import { OnboardingFooterLinkItemId, TELEMETRY_FOOTER_LINK } from './constants';

jest.mock('../../common/lib/telemetry');

describe('OnboardingFooterComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('FooterLinkItems should render the title and description', () => {
const { getByText } = render(
<FooterLinkItem
id={OnboardingFooterLinkItemId.documentation}
icon={'mockIcon.png'}
title={'Mock Title'}
description={'Mock Description'}
link={{ title: 'test', href: 'www.mock.com' }}
/>
);

expect(getByText('Mock Title')).toBeInTheDocument();
expect(getByText('Mock Description')).toBeInTheDocument();
});

it('FooterLinkItems should track the link click', () => {
const { getByTestId } = render(
<FooterLinkItem
id={OnboardingFooterLinkItemId.documentation}
icon={'mockIcon.png'}
title={'Mock Title'}
description={'Mock Description'}
link={{ title: 'test', href: 'www.mock.com' }}
/>
);

getByTestId('footerLinkItem').click();
expect(trackOnboardingLinkClick).toHaveBeenCalledWith(
`${TELEMETRY_FOOTER_LINK}_${OnboardingFooterLinkItemId.documentation}`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,71 @@
* 2.0.
*/

import React from 'react';
import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { useFooterStyles } from './onboarding_footer.styles';
import { footerItems } from './footer_items';
import { trackOnboardingLinkClick } from '../../common/lib/telemetry';
import type { OnboardingFooterLinkItemId } from './constants';
import { TELEMETRY_FOOTER_LINK } from './constants';

export const OnboardingFooter = React.memo(() => {
const styles = useFooterStyles();
return (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" className={styles}>
{footerItems.map((item) => (
<EuiFlexItem key={`footer-${item.key}`}>
<img src={item.icon} alt={item.title} height="64" width="64" />
<EuiSpacer size="m" />
<EuiTitle size="xxs" className="itemTitle">
<h3>{item.title}</h3>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size="xs">{item.description}</EuiText>
<EuiSpacer size="m" />
<EuiText size="xs">
<EuiLink href={item.link.href} target="_blank">
{item.link.title}
</EuiLink>
</EuiText>
</EuiFlexItem>
{footerItems.map(({ id, title, icon, description, link }) => (
<FooterLinkItem
id={id}
key={`footer-${id}`}
title={title}
icon={icon}
description={description}
link={link}
/>
))}
</EuiFlexGroup>
);
});
OnboardingFooter.displayName = 'OnboardingFooter';

interface FooterLinkItemProps {
id: OnboardingFooterLinkItemId;
title: string;
icon: string;
description: string;
link: { href: string; title: string };
}

export const FooterLinkItem = React.memo<FooterLinkItemProps>(
({ id, title, icon, description, link }) => {
const onClickWithReport = useCallback<React.MouseEventHandler>(() => {
trackOnboardingLinkClick(`${TELEMETRY_FOOTER_LINK}_${id}`);
}, [id]);

return (
<EuiFlexItem>
<img src={icon} alt={title} height="64" width="64" />
<EuiSpacer size="m" />
<EuiTitle size="xxs" className="itemTitle">
<h3>{title}</h3>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size="xs">{description}</EuiText>
<EuiSpacer size="m" />
<EuiText size="xs">
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
data-test-subj="footerLinkItem"
onClick={onClickWithReport}
href={link.href}
target="_blank"
>
{link.title}
</EuiLink>
</EuiText>
</EuiFlexItem>
);
}
);

FooterLinkItem.displayName = 'FooterLinkItem';
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import { LinkCard } from './link_card';
import { OnboardingHeaderCardId, TELEMETRY_HEADER_CARD } from '../../../constants';
import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry';

jest.mock('../../../../common/lib/telemetry');

describe('DataIngestionHubHeaderCardComponent', () => {
beforeEach(() => {
Expand All @@ -17,6 +21,7 @@ describe('DataIngestionHubHeaderCardComponent', () => {
it('should render the title, description, and icon', () => {
const { getByTestId, getByText } = render(
<LinkCard
id={OnboardingHeaderCardId.demo}
icon={'mockIcon.png'}
title={'Mock Title'}
description={'Mock Description'}
Expand All @@ -29,9 +34,27 @@ describe('DataIngestionHubHeaderCardComponent', () => {
expect(getByTestId('data-ingestion-header-card-icon')).toHaveAttribute('src', 'mockIcon.png');
});

it('should track the link card click', () => {
const { getByTestId } = render(
<LinkCard
id={OnboardingHeaderCardId.demo}
icon={'mockIcon.png'}
title={'Mock Title'}
description={'Mock Description'}
linkText="test"
/>
);

getByTestId('headerCardLink').click();
expect(trackOnboardingLinkClick).toHaveBeenCalledWith(
`${TELEMETRY_HEADER_CARD}_${OnboardingHeaderCardId.demo}`
);
});

it('should apply dark mode styles when color mode is DARK', () => {
const { container } = render(
<LinkCard
id={OnboardingHeaderCardId.demo}
icon={'mockIcon.png'}
title={'Mock Title'}
description={'Mock Description'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
* 2.0.
*/

import React from 'react';
import React, { useCallback } from 'react';
import { EuiCard, EuiImage, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import classNames from 'classnames';
import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry';
import { useCardStyles } from './link_card.styles';
import type { OnboardingHeaderCardId } from '../../../constants';
import { TELEMETRY_HEADER_CARD } from '../../../constants';

interface LinkCardProps {
id: OnboardingHeaderCardId;
icon: string;
title: string;
description: string;
Expand All @@ -21,13 +25,19 @@ interface LinkCardProps {
}

export const LinkCard: React.FC<LinkCardProps> = React.memo(
({ icon, title, description, onClick, href, target, linkText }) => {
({ id, icon, title, description, onClick, href, target, linkText }) => {
const cardStyles = useCardStyles();
const cardClassName = classNames(cardStyles, 'headerCard');

const onClickWithReport = useCallback<React.MouseEventHandler>(() => {
trackOnboardingLinkClick(`${TELEMETRY_HEADER_CARD}_${id}`);
onClick?.();
}, [id, onClick]);

return (
<EuiCard
className={cardClassName}
onClick={onClick}
onClick={onClickWithReport}
href={href}
target={target}
data-test-subj="data-ingestion-header-card"
Expand All @@ -50,7 +60,7 @@ export const LinkCard: React.FC<LinkCardProps> = React.memo(
<EuiSpacer size="s" />
<EuiText size="xs" className="headerCardLink">
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink href={href} onClick={onClick} target={target}>
<EuiLink data-test-subj="headerCardLink" href={href} onClick={onClick} target={target}>
{linkText}
</EuiLink>
</EuiText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { LinkCard } from '../common/link_card';
import demoImage from './images/demo_card.png';
import darkDemoImage from './images/demo_card_dark.png';
import * as i18n from './translations';
import { OnboardingHeaderCardId } from '../../../constants';

export const DemoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => {
return (
<LinkCard
id={OnboardingHeaderCardId.demo}
icon={isDarkMode ? darkDemoImage : demoImage}
title={i18n.ONBOARDING_HEADER_DEMO_TITLE}
description={i18n.ONBOARDING_HEADER_DEMO_DESCRIPTION}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { OnboardingHeaderCardId } from '../../../constants';
import { useOnboardingService } from '../../../../hooks/use_onboarding_service';
import { LinkCard } from '../common/link_card';
import teammatesImage from './images/teammates_card.png';
Expand All @@ -18,6 +19,7 @@ export const TeammatesCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }
const usersUrl = useObservable(usersUrl$, undefined);
return (
<LinkCard
id={OnboardingHeaderCardId.teammates}
icon={isDarkMode ? darkTeammatesImage : teammatesImage}
title={i18n.ONBOARDING_HEADER_TEAMMATES_TITLE}
description={i18n.ONBOARDING_HEADER_TEAMMATES_DESCRIPTION}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import React, { useCallback, useState } from 'react';
import { OnboardingHeaderCardId } from '../../../constants';
import { OnboardingHeaderVideoModal } from './video_modal';
import * as i18n from './translations';
import videoImage from './images/video_card.png';
Expand All @@ -14,6 +15,7 @@ import { LinkCard } from '../common/link_card';

export const VideoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => {
const [isModalVisible, setIsModalVisible] = useState(false);

const closeVideoModal = useCallback(() => {
setIsModalVisible(false);
}, []);
Expand All @@ -24,6 +26,7 @@ export const VideoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) =>
return (
<>
<LinkCard
id={OnboardingHeaderCardId.video}
onClick={showVideoModal}
icon={isDarkMode ? darkVideoImage : videoImage}
title={i18n.ONBOARDING_HEADER_VIDEO_TITLE}
Expand Down

0 comments on commit e38b4df

Please sign in to comment.