[8.18] [Security Solution][Onboarding] Adding telemetry to video selectors (#217280) (#218831)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[Security Solution][Onboarding] Adding telemetry to video selectors
(#217280)](https://github.com/elastic/kibana/pull/217280)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Agustina Nahir
Ruidiaz","email":"61565784+agusruidiazgd@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-04-16T13:42:30Z","message":"[Security
Solution][Onboarding] Adding telemetry to video selectors
(#217280)\n\n## Summary\n\nNew event created for the video selectors
inside rules, dashboards and\nalerts cards.\n\n```\nexport interface
OnboardingHubSelectorCardClickedParams {\n originStepId: string;\n
selectorId: string;\n}\n```\n\nTo verify:\n\nAdd these lines to
kibana.dev.yml\n\n```\nlogging.browser.root.level:
debug\ntelemetry.optIn: true\n```\n\n1. In the onboarding hub, expand
the rules card\n2. It should log `Report event \"Onboarding Hub Step
Selector
Clicked\"`.\n\n\nhttps://github.com/user-attachments/assets/c1b1084e-4917-4412-93ed-984a74b6b6b4\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f00f83715c8787033c7904c0794350f847900bfe","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","v9.0.0","Team:Threat
Hunting:Explore","ci:cloud-deploy","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[Security
Solution][Onboarding] Adding telemetry to video
selectors","number":217280,"url":"https://github.com/elastic/kibana/pull/217280","mergeCommit":{"message":"[Security
Solution][Onboarding] Adding telemetry to video selectors
(#217280)\n\n## Summary\n\nNew event created for the video selectors
inside rules, dashboards and\nalerts cards.\n\n```\nexport interface
OnboardingHubSelectorCardClickedParams {\n originStepId: string;\n
selectorId: string;\n}\n```\n\nTo verify:\n\nAdd these lines to
kibana.dev.yml\n\n```\nlogging.browser.root.level:
debug\ntelemetry.optIn: true\n```\n\n1. In the onboarding hub, expand
the rules card\n2. It should log `Report event \"Onboarding Hub Step
Selector
Clicked\"`.\n\n\nhttps://github.com/user-attachments/assets/c1b1084e-4917-4412-93ed-984a74b6b6b4\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f00f83715c8787033c7904c0794350f847900bfe"}},"sourceBranch":"main","suggestedTargetBranches":["8.18"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/218829","number":218829,"state":"OPEN"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217280","number":217280,"mergeCommit":{"message":"[Security
Solution][Onboarding] Adding telemetry to video selectors
(#217280)\n\n## Summary\n\nNew event created for the video selectors
inside rules, dashboards and\nalerts cards.\n\n```\nexport interface
OnboardingHubSelectorCardClickedParams {\n originStepId: string;\n
selectorId: string;\n}\n```\n\nTo verify:\n\nAdd these lines to
kibana.dev.yml\n\n```\nlogging.browser.root.level:
debug\ntelemetry.optIn: true\n```\n\n1. In the onboarding hub, expand
the rules card\n2. It should log `Report event \"Onboarding Hub Step
Selector
Clicked\"`.\n\n\nhttps://github.com/user-attachments/assets/c1b1084e-4917-4412-93ed-984a74b6b6b4\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f00f83715c8787033c7904c0794350f847900bfe"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"url":"https://github.com/elastic/kibana/pull/218830","number":218830,"branch":"8.19","state":"OPEN"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Agustina Nahir Ruidiaz 2025-04-23 10:43:11 +02:00 committed by GitHub
parent 8634cd048d
commit 91c373eb49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 139 additions and 20 deletions

View file

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

View file

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

View file

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

View file

@ -84,6 +84,7 @@ export const AlertsCard: OnboardingCardComponent = ({
items={ALERTS_CARD_ITEMS}
onSelect={onSelectCard}
selectedItem={selectedCardItem}
cardId={OnboardingCardId.alerts}
/>
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>

View file

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

View file

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

View file

@ -80,6 +80,7 @@ export const DashboardsCard: OnboardingCardComponent = ({
items={DASHBOARDS_CARD_ITEMS}
onSelect={onSelectCard}
selectedItem={selectedCardItem}
cardId={OnboardingCardId.dashboards}
/>
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>

View file

@ -80,6 +80,7 @@ export const RulesCard: OnboardingCardComponent = ({
items={RULES_CARD_ITEMS}
onSelect={onSelectCard}
selectedItem={selectedCardItem}
cardId={OnboardingCardId.rules}
/>
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>

View file

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

View file

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

View file

@ -239,6 +239,6 @@
"@kbn/product-doc-base-plugin",
"@kbn/shared-ux-error-boundary",
"@kbn/security-ai-prompts",
"@kbn/inference-endpoint-ui-common"
"@kbn/inference-endpoint-ui-common",
]
}