mirror of
https://github.com/elastic/kibana.git
synced 2025-04-19 15:35:00 -04:00
[Security Solution][Onboarding] Adding telemetry to video selectors (#217280)
## Summary New event created for the video selectors inside rules, dashboards and alerts cards. ``` export interface OnboardingHubSelectorCardClickedParams { originStepId: string; selectorId: string; } ``` To verify: Add these lines to kibana.dev.yml ``` logging.browser.root.level: debug telemetry.optIn: true ``` 1. In the onboarding hub, expand the rules card 2. It should log `Report event "Onboarding Hub Step Selector Clicked"`. https://github.com/user-attachments/assets/c1b1084e-4917-4412-93ed-984a74b6b6b4 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] [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: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bf7389f515
commit
f00f83715c
11 changed files with 139 additions and 20 deletions
|
@ -75,8 +75,29 @@ export const onboardingHubStepFinishedEvent: OnboardingHubTelemetryEvent = {
|
|||
},
|
||||
};
|
||||
|
||||
export const onboardingHubStepSelectorClickedEvent: OnboardingHubTelemetryEvent = {
|
||||
eventType: OnboardingHubEventTypes.OnboardingHubStepSelectorClicked,
|
||||
schema: {
|
||||
originStepId: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'Active step ID',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
selectorId: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'Clicked Selector ID',
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const onboardingHubTelemetryEvents = [
|
||||
onboardingHubStepOpenEvent,
|
||||
onboardingHubStepLinkClickedEvent,
|
||||
onboardingHubStepFinishedEvent,
|
||||
onboardingHubStepSelectorClickedEvent,
|
||||
];
|
||||
|
|
|
@ -10,6 +10,7 @@ export enum OnboardingHubEventTypes {
|
|||
OnboardingHubStepOpen = 'Onboarding Hub Step Open',
|
||||
OnboardingHubStepFinished = 'Onboarding Hub Step Finished',
|
||||
OnboardingHubStepLinkClicked = 'Onboarding Hub Step Link Clicked',
|
||||
OnboardingHubStepSelectorClicked = 'Onboarding Hub Step Selector Clicked',
|
||||
}
|
||||
|
||||
type OnboardingHubStepOpenTrigger = 'navigation' | 'click';
|
||||
|
@ -24,6 +25,11 @@ export interface OnboardingHubStepLinkClickedParams {
|
|||
stepLinkId: string;
|
||||
}
|
||||
|
||||
export interface OnboardingHubSelectorCardClickedParams {
|
||||
originStepId: string;
|
||||
selectorId: string;
|
||||
}
|
||||
|
||||
export type OnboardingHubStepFinishedTrigger = 'auto_check' | 'click';
|
||||
|
||||
export interface OnboardingHubStepFinishedParams {
|
||||
|
@ -36,6 +42,7 @@ export interface OnboardingHubTelemetryEventsMap {
|
|||
[OnboardingHubEventTypes.OnboardingHubStepOpen]: OnboardingHubStepOpenParams;
|
||||
[OnboardingHubEventTypes.OnboardingHubStepFinished]: OnboardingHubStepFinishedParams;
|
||||
[OnboardingHubEventTypes.OnboardingHubStepLinkClicked]: OnboardingHubStepLinkClickedParams;
|
||||
[OnboardingHubEventTypes.OnboardingHubStepSelectorClicked]: OnboardingHubSelectorCardClickedParams;
|
||||
}
|
||||
|
||||
export interface OnboardingHubTelemetryEvent {
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
export const mockReportCardOpen = jest.fn();
|
||||
export const mockReportCardComplete = jest.fn();
|
||||
export const mockReportCardLinkClicked = jest.fn();
|
||||
export const mockReportCardSelectorClicked = jest.fn();
|
||||
|
||||
export const telemetry = {
|
||||
reportCardOpen: mockReportCardOpen,
|
||||
reportCardComplete: mockReportCardComplete,
|
||||
reportCardLinkClicked: mockReportCardLinkClicked,
|
||||
reportCardSelectorClicked: mockReportCardSelectorClicked,
|
||||
};
|
||||
export const mockTelemetry = jest.fn(() => telemetry);
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ export const AlertsCard: OnboardingCardComponent = ({
|
|||
items={ALERTS_CARD_ITEMS}
|
||||
onSelect={onSelectCard}
|
||||
selectedItem={selectedCardItem}
|
||||
cardId={OnboardingCardId.alerts}
|
||||
/>
|
||||
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
|
||||
<>
|
||||
|
|
|
@ -6,13 +6,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { CardSelectorList } from './card_selector_list';
|
||||
import type { CardSelectorListItem } from './card_selector_list';
|
||||
import { RulesCardItemId } from '../rules/types';
|
||||
import { OnboardingCardId } from '../../../../constants';
|
||||
import { OnboardingContextProvider } from '../../../onboarding_context';
|
||||
import { ExperimentalFeaturesService } from '../../../../../common/experimental_features_service';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
jest.mock('../../../../../common/experimental_features_service', () => ({
|
||||
ExperimentalFeaturesService: { get: jest.fn() },
|
||||
}));
|
||||
const mockExperimentalFeatures = ExperimentalFeaturesService.get as jest.Mock;
|
||||
|
||||
const items: CardSelectorListItem[] = [
|
||||
{
|
||||
id: RulesCardItemId.install,
|
||||
|
@ -31,6 +41,7 @@ const defaultProps = {
|
|||
onSelect: mockOnSelect,
|
||||
selectedItem: items[0],
|
||||
title: 'Select a Rule',
|
||||
cardId: OnboardingCardId.rules,
|
||||
};
|
||||
|
||||
describe('CardSelectorList', () => {
|
||||
|
@ -40,6 +51,7 @@ describe('CardSelectorList', () => {
|
|||
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockExperimentalFeatures.mockReturnValue({});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
afterAll(() => {
|
||||
|
@ -47,46 +59,81 @@ describe('CardSelectorList', () => {
|
|||
});
|
||||
|
||||
it('renders the component with the correct title', () => {
|
||||
const { getByText } = render(<CardSelectorList {...defaultProps} />);
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByText('Select a Rule')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all card items', () => {
|
||||
const { getByTestId } = render(<CardSelectorList {...defaultProps} />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
items.forEach((item) => {
|
||||
expect(getByTestId(`cardSelectorItem-${item.id}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies the "selected" class to the selected item', () => {
|
||||
const { getByTestId } = render(<CardSelectorList {...defaultProps} />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
const selectedItem = getByTestId(`cardSelectorItem-${RulesCardItemId.install}`);
|
||||
expect(selectedItem).toHaveClass('selectedCardPanelItem');
|
||||
});
|
||||
|
||||
it('does not apply the "selected" class to unselected items', () => {
|
||||
const { getByTestId } = render(<CardSelectorList {...defaultProps} />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
const unselectedItem = getByTestId(`cardSelectorItem-${RulesCardItemId.create}`);
|
||||
expect(unselectedItem).not.toHaveClass('selectedCardPanelItem');
|
||||
});
|
||||
|
||||
it('calls onSelect with the correct item when an item is clicked', () => {
|
||||
const { getByTestId } = render(<CardSelectorList {...defaultProps} />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
const unselectedItem = getByTestId(`cardSelectorItem-${RulesCardItemId.create}`);
|
||||
fireEvent.click(unselectedItem);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(items[1]);
|
||||
});
|
||||
|
||||
it('scrolls to the selected item on initial render', () => {
|
||||
jest.useFakeTimers();
|
||||
render(<CardSelectorList {...defaultProps} />);
|
||||
jest.runAllTimers();
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates the selected item visually when onSelect is called', () => {
|
||||
const { rerender, getByTestId } = render(<CardSelectorList {...defaultProps} />);
|
||||
rerender(<CardSelectorList {...defaultProps} selectedItem={items[1]} />);
|
||||
const { rerender, getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
rerender(
|
||||
<TestProviders>
|
||||
<OnboardingContextProvider spaceId="default">
|
||||
<CardSelectorList {...defaultProps} selectedItem={items[1]} />
|
||||
</OnboardingContextProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const newlySelectedItem = getByTestId(`cardSelectorItem-${RulesCardItemId.create}`);
|
||||
const previouslySelectedItem = getByTestId(`cardSelectorItem-${RulesCardItemId.install}`);
|
||||
|
|
|
@ -4,13 +4,15 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import type { OnboardingCardId } from '../../../../constants';
|
||||
import type { RulesCardItemId } from '../rules/types';
|
||||
import type { AlertsCardItemId } from '../alerts/types';
|
||||
import type { DashboardsCardItemId } from '../dashboards/types';
|
||||
import { useCardSelectorListStyles } from './card_selector_list.styles';
|
||||
import { HEIGHT_ANIMATION_DURATION } from '../../onboarding_card_panel.styles';
|
||||
import { useOnboardingContext } from '../../../onboarding_context';
|
||||
|
||||
export interface CardSelectorListItem {
|
||||
id: RulesCardItemId | AlertsCardItemId | DashboardsCardItemId;
|
||||
|
@ -23,6 +25,7 @@ export interface CardSelectorListProps {
|
|||
onSelect: (item: CardSelectorListItem) => void;
|
||||
selectedItem: CardSelectorListItem;
|
||||
title?: string;
|
||||
cardId: OnboardingCardId;
|
||||
}
|
||||
|
||||
const scrollToSelectedItem = (cardId: string) => {
|
||||
|
@ -35,7 +38,8 @@ const scrollToSelectedItem = (cardId: string) => {
|
|||
};
|
||||
|
||||
export const CardSelectorList = React.memo<CardSelectorListProps>(
|
||||
({ items, onSelect, selectedItem, title }) => {
|
||||
({ items, onSelect, selectedItem, title, cardId }) => {
|
||||
const { telemetry } = useOnboardingContext();
|
||||
const styles = useCardSelectorListStyles();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -43,6 +47,14 @@ export const CardSelectorList = React.memo<CardSelectorListProps>(
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(item: CardSelectorListItem) => {
|
||||
onSelect(item);
|
||||
telemetry.reportCardSelectorClicked(cardId, item.id);
|
||||
},
|
||||
[cardId, onSelect, telemetry]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
data-test-subj="cardSelectorList"
|
||||
|
@ -72,9 +84,7 @@ export const CardSelectorList = React.memo<CardSelectorListProps>(
|
|||
className={selectedItem.id === item.id ? 'selectedCardPanelItem' : ''}
|
||||
color={selectedItem.id === item.id ? 'subdued' : 'plain'}
|
||||
element="button"
|
||||
onClick={() => {
|
||||
onSelect(item);
|
||||
}}
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>{item.title}</h5>
|
||||
|
|
|
@ -80,6 +80,7 @@ export const DashboardsCard: OnboardingCardComponent = ({
|
|||
items={DASHBOARDS_CARD_ITEMS}
|
||||
onSelect={onSelectCard}
|
||||
selectedItem={selectedCardItem}
|
||||
cardId={OnboardingCardId.dashboards}
|
||||
/>
|
||||
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
|
||||
<>
|
||||
|
|
|
@ -80,6 +80,7 @@ export const RulesCard: OnboardingCardComponent = ({
|
|||
items={RULES_CARD_ITEMS}
|
||||
onSelect={onSelectCard}
|
||||
selectedItem={selectedCardItem}
|
||||
cardId={OnboardingCardId.rules}
|
||||
/>
|
||||
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
|
||||
<>
|
||||
|
|
|
@ -112,4 +112,26 @@ describe('useOnboardingTelemetry', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clicking a card selector', () => {
|
||||
it('should report card selector clicked event on the default topic', () => {
|
||||
const { result } = renderHook(useOnboardingTelemetry);
|
||||
result.current.reportCardSelectorClicked('testCard' as OnboardingCardId, 'selector1');
|
||||
|
||||
expect(telemetryMock.reportEvent).toHaveBeenCalledWith(
|
||||
OnboardingHubEventTypes.OnboardingHubStepSelectorClicked,
|
||||
{ originStepId: 'testCard', selectorId: 'selector1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should report card selector clicked event on another topic', () => {
|
||||
const { result } = renderHook(useOnboardingTelemetry);
|
||||
result.current.reportCardSelectorClicked('testCard2' as OnboardingCardId, 'selector2');
|
||||
|
||||
expect(telemetryMock.reportEvent).toHaveBeenCalledWith(
|
||||
OnboardingHubEventTypes.OnboardingHubStepSelectorClicked,
|
||||
{ originStepId: 'testTopic#testCard2', selectorId: 'selector2' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface OnboardingTelemetry {
|
|||
reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void;
|
||||
reportCardComplete: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void;
|
||||
reportCardLinkClicked: (cardId: OnboardingCardId, linkId: string) => void;
|
||||
reportCardSelectorClicked: (cardId: OnboardingCardId, selectorId: string) => void;
|
||||
}
|
||||
|
||||
export const useOnboardingTelemetry = (): OnboardingTelemetry => {
|
||||
|
@ -40,6 +41,12 @@ export const useOnboardingTelemetry = (): OnboardingTelemetry => {
|
|||
stepLinkId: linkId,
|
||||
});
|
||||
},
|
||||
reportCardSelectorClicked: (cardId, selectorId: string) => {
|
||||
telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepSelectorClicked, {
|
||||
originStepId: getStepId(cardId),
|
||||
selectorId,
|
||||
});
|
||||
},
|
||||
}),
|
||||
[telemetry]
|
||||
);
|
||||
|
|
|
@ -246,6 +246,6 @@
|
|||
"@kbn/custom-icons",
|
||||
"@kbn/security-plugin-types-common",
|
||||
"@kbn/management-settings-ids",
|
||||
"@kbn/inference-endpoint-ui-common"
|
||||
"@kbn/inference-endpoint-ui-common",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue