Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SecuritySolution][Onboarding] Send Telemetry when header/footer cards are clicked #196495

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export enum TELEMETRY_EVENT {
// Landing page - dashboard
DASHBOARD = 'navigate_to_dashboard',
CREATE_DASHBOARD = 'create_dashboard',
ONBOARDING = 'onboarding_',
agusruidiazgd marked this conversation as resolved.
Show resolved Hide resolved
agusruidiazgd marked this conversation as resolved.
Show resolved Hide resolved

ONBOARDING = 'onboarding',

Expand Down
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