mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
8634cd048d
commit
91c373eb49
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]
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue