[8.16] [Entity Inventory] Add basic telemetry (#197055) (#198047)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Entity Inventory] Add basic telemetry
(#197055)](https://github.com/elastic/kibana/pull/197055)

<!--- Backport version: 8.9.8 -->

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

<!--BACKPORT [{"author":{"name":"Irene
Blanco","email":"irene.blanco@elastic.co"},"sourceCommit":{"committedDate":"2024-10-25T14:49:49Z","message":"[Entity
Inventory] Add basic telemetry (#197055)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/195608.\r\n\r\nIn this PR, we
introduce basic telemetry tracking for the new
Inventory\r\nplugin.\r\nThese events will help us gain insight into how
users are interacting\r\nwith the Inventory feature, including the state
of the views, search\r\nbehaviors, and entity type
filtering.\r\n\r\n\r\n**New events**\r\n- Entity Inventory Viewed\r\n-
Entity Inventory Search Query Submitted\r\n- Entity Inventory Entity
Type Filtered\r\n- Entity View
Clicked\r\n\r\n\r\n![Untitled-2024-07-24-1420](https://github.com/user-attachments/assets/6e85ea00-c626-4bc1-a4f8-9907674eb264)\r\n\r\n\r\n~**New
attribute added to global context**~\r\n- ~eem_enabled~\r\n\r\n~It will
only be populated if the Inventory plugin is accessible to\r\nusers and
after they access the Observability solution.\r\nIf EEM is not enabled
and the user enables it, the property will be\r\nupdated
accordingly.~\r\n\r\nDetails about not implementing `eem_enabled` can be
found in
[this\r\ncomment](https://github.com/elastic/kibana/pull/197055#issuecomment-2432123047).","sha":"7d673b84c3ecec2f6da81b57196301c6e7fe384a","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","v9.0.0","telemetry","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services","v8.16.0"],"number":197055,"url":"https://github.com/elastic/kibana/pull/197055","mergeCommit":{"message":"[Entity
Inventory] Add basic telemetry (#197055)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/195608.\r\n\r\nIn this PR, we
introduce basic telemetry tracking for the new
Inventory\r\nplugin.\r\nThese events will help us gain insight into how
users are interacting\r\nwith the Inventory feature, including the state
of the views, search\r\nbehaviors, and entity type
filtering.\r\n\r\n\r\n**New events**\r\n- Entity Inventory Viewed\r\n-
Entity Inventory Search Query Submitted\r\n- Entity Inventory Entity
Type Filtered\r\n- Entity View
Clicked\r\n\r\n\r\n![Untitled-2024-07-24-1420](https://github.com/user-attachments/assets/6e85ea00-c626-4bc1-a4f8-9907674eb264)\r\n\r\n\r\n~**New
attribute added to global context**~\r\n- ~eem_enabled~\r\n\r\n~It will
only be populated if the Inventory plugin is accessible to\r\nusers and
after they access the Observability solution.\r\nIf EEM is not enabled
and the user enables it, the property will be\r\nupdated
accordingly.~\r\n\r\nDetails about not implementing `eem_enabled` can be
found in
[this\r\ncomment](https://github.com/elastic/kibana/pull/197055#issuecomment-2432123047).","sha":"7d673b84c3ecec2f6da81b57196301c6e7fe384a"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/197055","number":197055,"mergeCommit":{"message":"[Entity
Inventory] Add basic telemetry (#197055)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/195608.\r\n\r\nIn this PR, we
introduce basic telemetry tracking for the new
Inventory\r\nplugin.\r\nThese events will help us gain insight into how
users are interacting\r\nwith the Inventory feature, including the state
of the views, search\r\nbehaviors, and entity type
filtering.\r\n\r\n\r\n**New events**\r\n- Entity Inventory Viewed\r\n-
Entity Inventory Search Query Submitted\r\n- Entity Inventory Entity
Type Filtered\r\n- Entity View
Clicked\r\n\r\n\r\n![Untitled-2024-07-24-1420](https://github.com/user-attachments/assets/6e85ea00-c626-4bc1-a4f8-9907674eb264)\r\n\r\n\r\n~**New
attribute added to global context**~\r\n- ~eem_enabled~\r\n\r\n~It will
only be populated if the Inventory plugin is accessible to\r\nusers and
after they access the Observability solution.\r\nIf EEM is not enabled
and the user enables it, the property will be\r\nupdated
accordingly.~\r\n\r\nDetails about not implementing `eem_enabled` can be
found in
[this\r\ncomment](https://github.com/elastic/kibana/pull/197055#issuecomment-2432123047).","sha":"7d673b84c3ecec2f6da81b57196301c6e7fe384a"}},{"branch":"8.16","label":"v8.16.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: Irene Blanco <irene.blanco@elastic.co>
This commit is contained in:
Carlos Crespo 2024-10-29 10:36:49 +01:00 committed by GitHub
parent 6f333b8ae0
commit 512cabcb34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 549 additions and 21 deletions

View file

@ -15,7 +15,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { InventoryKibanaContext } from '../public/hooks/use_kibana';
import type { ITelemetryClient } from '../public/services/telemetry/types';
import { ITelemetryClient } from '../public/services/telemetry/types';
export function getMockInventoryContext(): InventoryKibanaContext {
const coreStart = coreMock.createStart();

View file

@ -25,13 +25,22 @@ interface EntityNameProps {
}
export function EntityName({ entity }: EntityNameProps) {
const { services } = useKibana();
const {
services: { telemetry, share },
} = useKibana();
const assetDetailsLocator =
services.share?.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
share?.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
const serviceOverviewLocator =
services.share?.url.locators.get<ServiceOverviewParams>('serviceOverviewLocator');
share?.url.locators.get<ServiceOverviewParams>('serviceOverviewLocator');
const handleLinkClick = useCallback(() => {
telemetry.reportEntityViewClicked({
view_type: 'detail',
entity_type: entity['entity.type'],
});
}, [entity, telemetry]);
const getEntityRedirectUrl = useCallback(() => {
const type = entity[ENTITY_TYPE];
@ -58,7 +67,12 @@ export function EntityName({ entity }: EntityNameProps) {
}, [entity, assetDetailsLocator, serviceOverviewLocator]);
return (
<EuiLink data-test-subj="entityNameLink" href={getEntityRedirectUrl()}>
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink
data-test-subj="entityNameLink"
href={getEntityRedirectUrl()}
onClick={handleLinkClick}
>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={0}>
<EntityIcon entity={entity} />

View file

@ -85,12 +85,13 @@ export function EntitiesGrid({
}
const columnEntityTableId = columnId as EntityColumnIds;
const entityType = entity[ENTITY_TYPE];
switch (columnEntityTableId) {
case 'alertsCount':
return entity?.alertsCount ? <AlertsBadge entity={entity} /> : null;
case ENTITY_TYPE:
const entityType = entity[columnEntityTableId];
return (
<BadgeFilterWithPopover
field={ENTITY_TYPE}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import React, { useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
import {
FeatureFeedbackButton,
@ -18,6 +18,7 @@ import { useEntityManager } from '../../hooks/use_entity_manager';
import { Welcome } from '../entity_enablement/welcome_modal';
import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async';
import { EmptyState } from '../empty_states/empty_state';
import { useIsLoadingComplete } from '../../hooks/use_is_loading_complete';
const pageTitle = (
<EuiFlexGroup gutterSize="s">
@ -36,7 +37,7 @@ const INVENTORY_FEEDBACK_LINK = 'https://ela.st/feedback-new-inventory';
export function InventoryPageTemplate({ children }: { children: React.ReactNode }) {
const {
services: { observabilityShared, inventoryAPIClient, kibanaEnvironment },
services: { observabilityShared, inventoryAPIClient, kibanaEnvironment, telemetry },
} = useKibana();
const { PageTemplate: ObservabilityPageTemplate } = observabilityShared.navigation;
@ -62,6 +63,23 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode
[inventoryAPIClient]
);
const isLoadingComplete = useIsLoadingComplete({
loadingStates: [isEnablementLoading, hasDataLoading],
});
useEffect(() => {
if (isLoadingComplete) {
const viewState = isEntityManagerEnabled
? value.hasData
? 'populated'
: 'empty'
: 'eem_disabled';
telemetry.reportEntityInventoryViewed({
view_state: viewState,
});
}
}, [isEntityManagerEnabled, value.hasData, telemetry, isLoadingComplete]);
if (isEnablementLoading || hasDataLoading) {
return (
<ObservabilityPageTemplate

View file

@ -9,6 +9,7 @@ import { SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar'
import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Query } from '@kbn/es-query';
import { EntityType } from '../../../common/entities';
import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider';
import { useAdHocInventoryDataView } from '../../hooks/use_adhoc_inventory_data_view';
@ -16,6 +17,7 @@ import { useInventoryParams } from '../../hooks/use_inventory_params';
import { useKibana } from '../../hooks/use_kibana';
import { EntityTypesControls } from './entity_types_controls';
import { DiscoverButton } from './discover_button';
import { getKqlFieldsWithFallback } from '../../utils/get_kql_field_names_with_fallback';
export function SearchBar() {
const { searchBarContentSubject$ } = useInventorySearchBarContext();
@ -25,6 +27,7 @@ export function SearchBar() {
data: {
query: { queryString: queryStringService },
},
telemetry,
},
} = useKibana();
@ -51,11 +54,41 @@ export function SearchBar() {
syncSearchBarWithUrl();
}, [syncSearchBarWithUrl]);
const registerSearchSubmittedEvent = useCallback(
({
searchQuery,
searchIsUpdate,
searchEntityTypes,
}: {
searchQuery?: Query;
searchEntityTypes?: string[];
searchIsUpdate?: boolean;
}) => {
telemetry.reportEntityInventorySearchQuerySubmitted({
kuery_fields: getKqlFieldsWithFallback(searchQuery?.query as string),
entity_types: searchEntityTypes || [],
action: searchIsUpdate ? 'submit' : 'refresh',
});
},
[telemetry]
);
const registerEntityTypeFilteredEvent = useCallback(
({ filterEntityTypes, filterKuery }: { filterEntityTypes: string[]; filterKuery?: string }) => {
telemetry.reportEntityInventoryEntityTypeFiltered({
entity_types: filterEntityTypes,
kuery_fields: filterKuery ? getKqlFieldsWithFallback(filterKuery) : [],
});
},
[telemetry]
);
const handleEntityTypesChange = useCallback(
(nextEntityTypes: EntityType[]) => {
searchBarContentSubject$.next({ kuery, entityTypes: nextEntityTypes, refresh: false });
registerEntityTypeFilteredEvent({ filterEntityTypes: nextEntityTypes, filterKuery: kuery });
},
[kuery, searchBarContentSubject$]
[kuery, registerEntityTypeFilteredEvent, searchBarContentSubject$]
);
const handleQuerySubmit = useCallback<NonNullable<SearchBarOwnProps['onQuerySubmit']>>(
@ -65,8 +98,14 @@ export function SearchBar() {
entityTypes,
refresh: !isUpdate,
});
registerSearchSubmittedEvent({
searchQuery: query,
searchEntityTypes: entityTypes,
searchIsUpdate: isUpdate,
});
},
[entityTypes, searchBarContentSubject$]
[entityTypes, registerSearchSubmittedEvent, searchBarContentSubject$]
);
return (

View file

@ -0,0 +1,109 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useIsLoadingComplete } from './use_is_loading_complete';
describe('useIsLoadingComplete', () => {
describe('initialization', () => {
it('should initialize with undefined', () => {
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [false, false] }));
expect(result.current).toBeUndefined();
});
it('should handle an empty array of loadingStates', () => {
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [] }));
expect(result.current).toBeUndefined();
});
it('should handle a single loading state that is false', () => {
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [false] }));
expect(result.current).toBeUndefined();
});
});
describe('loading states', () => {
it('should set isLoadingComplete to false when some loadingStates are true', () => {
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [true, false] }));
expect(result.current).toBe(false);
});
it('should set isLoadingComplete to false when all loadingStates are true', () => {
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [true, true] }));
expect(result.current).toBe(false);
});
it('should handle a single loading state that is true', () => {
const { result } = renderHook(() => useIsLoadingComplete({ loadingStates: [true] }));
expect(result.current).toBe(false);
});
});
describe('loading completion', () => {
it('should set isLoadingComplete to true when all loadingStates are false after being true', () => {
const { result, rerender } = renderHook(
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
{
initialProps: { loadingStates: [true, false] },
}
);
expect(result.current).toBe(false);
rerender({ loadingStates: [false, false] });
expect(result.current).toBe(true);
});
it('should set isLoadingComplete to true when all loadingStates are false after being mixed', () => {
const { result, rerender } = renderHook(
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
{
initialProps: { loadingStates: [true, false] },
}
);
expect(result.current).toBe(false);
rerender({ loadingStates: [false, false] });
expect(result.current).toBe(true);
});
});
describe('mixed states', () => {
it('should not change isLoadingComplete if loadingStates are mixed', () => {
const { result, rerender } = renderHook(
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
{
initialProps: { loadingStates: [true, true] },
}
);
expect(result.current).toBe(false);
rerender({ loadingStates: [true, false] });
expect(result.current).toBe(false);
});
it('should not change isLoadingComplete if loadingStates change from all true to mixed', () => {
const { result, rerender } = renderHook(
({ loadingStates }) => useIsLoadingComplete({ loadingStates }),
{
initialProps: { loadingStates: [true, true] },
}
);
expect(result.current).toBe(false);
rerender({ loadingStates: [true, false] });
expect(result.current).toBe(false);
});
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 { useState, useEffect } from 'react';
interface UseIsLoadingCompleteProps {
loadingStates: boolean[];
}
export const useIsLoadingComplete = ({ loadingStates }: UseIsLoadingCompleteProps) => {
const [isLoadingComplete, setIsLoadingComplete] = useState<boolean | undefined>(undefined);
useEffect(() => {
const someLoading = loadingStates.some((loading) => loading);
const allLoaded = loadingStates.every((loading) => !loading);
if (isLoadingComplete === undefined && someLoading) {
setIsLoadingComplete(false);
} else if (isLoadingComplete === false && allLoaded) {
setIsLoadingComplete(true);
}
}, [isLoadingComplete, loadingStates]);
return isLoadingComplete;
};

View file

@ -49,6 +49,7 @@ export class InventoryPlugin
this.kibanaVersion = context.env.packageInfo.version;
this.isServerlessEnv = context.env.packageInfo.buildFlavor === 'serverless';
}
setup(
coreSetup: CoreSetup<InventoryStartDependencies, InventoryPublicStart>,
pluginsSetup: InventorySetupDependencies
@ -58,6 +59,13 @@ export class InventoryPlugin
'observability:entityCentricExperience',
true
);
this.telemetry.setup({
analytics: coreSetup.analytics,
});
const telemetry = this.telemetry.start();
const getStartServices = coreSetup.getStartServices();
const hideInventory$ = from(getStartServices).pipe(
@ -105,9 +113,6 @@ export class InventoryPlugin
pluginsSetup.observabilityShared.navigation.registerSections(sections$);
this.telemetry.setup({ analytics: coreSetup.analytics });
const telemetry = this.telemetry.start();
const isCloudEnv = !!pluginsSetup.cloud?.isCloudEnabled;
const isServerlessEnv = pluginsSetup.cloud?.isServerlessEnabled || this.isServerlessEnv;

View file

@ -6,7 +6,16 @@
*/
import { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import { type ITelemetryClient, TelemetryEventTypes, type InventoryAddDataParams } from './types';
import {
type ITelemetryClient,
TelemetryEventTypes,
type InventoryAddDataParams,
type EntityInventoryViewedParams,
type EntityInventorySearchQuerySubmittedParams,
type EntityViewClickedParams,
type EntityInventoryEntityTypeFilteredParams,
} from './types';
export class TelemetryClient implements ITelemetryClient {
constructor(private analytics: AnalyticsServiceSetup) {}
@ -14,4 +23,24 @@ export class TelemetryClient implements ITelemetryClient {
public reportInventoryAddData = (params: InventoryAddDataParams) => {
this.analytics.reportEvent(TelemetryEventTypes.INVENTORY_ADD_DATA_CLICKED, params);
};
public reportEntityInventoryViewed = (params: EntityInventoryViewedParams) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_INVENTORY_VIEWED, params);
};
public reportEntityInventorySearchQuerySubmitted = (
params: EntityInventorySearchQuerySubmittedParams
) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_INVENTORY_SEARCH_QUERY_SUBMITTED, params);
};
public reportEntityInventoryEntityTypeFiltered = (
params: EntityInventoryEntityTypeFilteredParams
) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_INVENTORY_ENTITY_TYPE_FILTERED, params);
};
public reportEntityViewClicked = (params: EntityViewClickedParams) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_VIEW_CLICKED, params);
};
}

View file

@ -25,4 +25,94 @@ const inventoryAddDataEventType: TelemetryEvent = {
},
};
export const inventoryTelemetryEventBasedTypes = [inventoryAddDataEventType];
const entityInventoryViewedEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_INVENTORY_VIEWED,
schema: {
view_state: {
type: 'keyword',
_meta: {
description: 'State of the view: empty, populated or eem_disabled.',
},
},
},
};
const searchQuerySubmittedEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_INVENTORY_SEARCH_QUERY_SUBMITTED,
schema: {
kuery_fields: {
type: 'array',
items: {
type: 'text',
_meta: {
description: 'Kuery fields used in the search.',
},
},
},
entity_types: {
type: 'array',
items: {
type: 'keyword',
_meta: {
description: 'Entity types used in the search.',
},
},
},
action: {
type: 'keyword',
_meta: {
description: 'Action performed: submit or refresh.',
},
},
},
};
const entityInventoryEntityTypeFilteredEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_INVENTORY_ENTITY_TYPE_FILTERED,
schema: {
entity_types: {
type: 'array',
items: {
type: 'keyword',
_meta: {
description: 'Entity types used in the filter.',
},
},
},
kuery_fields: {
type: 'array',
items: {
type: 'text',
_meta: {
description: 'Kuery fields used in the filter.',
},
},
},
},
};
const entityViewClickedEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_VIEW_CLICKED,
schema: {
entity_type: {
type: 'keyword',
_meta: {
description: 'Type of the entity: container, host or service.',
},
},
view_type: {
type: 'keyword',
_meta: {
description: 'Type of the view: detail or flyout.',
},
},
},
};
export const inventoryTelemetryEventBasedTypes = [
inventoryAddDataEventType,
entityInventoryViewedEventType,
searchQuerySubmittedEventType,
entityInventoryEntityTypeFilteredEventType,
entityViewClickedEventType,
];

View file

@ -8,7 +8,13 @@ import { coreMock } from '@kbn/core/server/mocks';
import { inventoryTelemetryEventBasedTypes } from './telemetry_events';
import { TelemetryService } from './telemetry_service';
import { TelemetryEventTypes } from './types';
import {
type EntityInventoryViewedParams,
type EntityViewClickedParams,
type EntityInventorySearchQuerySubmittedParams,
TelemetryEventTypes,
type EntityInventoryEntityTypeFilteredParams,
} from './types';
describe('TelemetryService', () => {
let service: TelemetryService;
@ -48,7 +54,15 @@ describe('TelemetryService', () => {
service.setup(setupParams);
const telemetry = service.start();
expect(telemetry).toHaveProperty('reportInventoryAddData');
const expectedProperties = [
'reportInventoryAddData',
'reportEntityInventoryViewed',
'reportEntityInventorySearchQuerySubmitted',
'reportEntityViewClicked',
];
expectedProperties.forEach((property) => {
expect(telemetry).toHaveProperty(property);
});
});
});
@ -73,4 +87,84 @@ describe('TelemetryService', () => {
);
});
});
describe('#reportEntityInventoryViewed', () => {
it('should report entity inventory viewed with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const params: EntityInventoryViewedParams = {
view_state: 'empty',
};
telemetry.reportEntityInventoryViewed(params);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
TelemetryEventTypes.ENTITY_INVENTORY_VIEWED,
params
);
});
});
describe('#reportEntityInventorySearchQuerySubmitted', () => {
it('should report search query submitted with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const params: EntityInventorySearchQuerySubmittedParams = {
kuery_fields: ['_index'],
action: 'submit',
entity_types: ['container'],
};
telemetry.reportEntityInventorySearchQuerySubmitted(params);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
TelemetryEventTypes.ENTITY_INVENTORY_SEARCH_QUERY_SUBMITTED,
params
);
});
});
describe('#reportEntityInventoryEntityTypeFiltered', () => {
it('should report entity type filtered with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const params: EntityInventoryEntityTypeFilteredParams = {
kuery_fields: ['_index'],
entity_types: ['container'],
};
telemetry.reportEntityInventoryEntityTypeFiltered(params);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
TelemetryEventTypes.ENTITY_INVENTORY_ENTITY_TYPE_FILTERED,
params
);
});
});
describe('#reportEntityViewClicked', () => {
it('should report entity view clicked with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const params: EntityViewClickedParams = {
entity_type: 'container',
view_type: 'detail',
};
telemetry.reportEntityViewClicked(params);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
TelemetryEventTypes.ENTITY_VIEW_CLICKED,
params
);
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import type { TelemetryServiceSetupParams, ITelemetryClient, TelemetryEventParams } from './types';
import type { TelemetryServiceSetupParams, TelemetryEventParams } from './types';
import { inventoryTelemetryEventBasedTypes } from './telemetry_events';
import { TelemetryClient } from './telemetry_client';
@ -23,7 +23,7 @@ export class TelemetryService {
);
}
public start(): ITelemetryClient {
public start(): TelemetryClient {
if (!this.analytics) {
throw new Error(
'The TelemetryService.setup() method has not been invoked, be sure to call it during the plugin setup.'

View file

@ -6,24 +6,64 @@
*/
import type { AnalyticsServiceSetup, RootSchema } from '@kbn/core/public';
import { EntityManagerPublicPluginSetup } from '@kbn/entityManager-plugin/public';
export interface TelemetryServiceSetupParams {
analytics: AnalyticsServiceSetup;
}
export interface TelemetryServiceStartParams {
entityManager: EntityManagerPublicPluginSetup;
}
export interface InventoryAddDataParams {
view: 'add_data_button' | 'empty_state';
journey?: 'add_data' | 'associate_existing_service_logs';
}
export type TelemetryEventParams = InventoryAddDataParams;
export interface EntityInventoryViewedParams {
view_state: 'empty' | 'populated' | 'eem_disabled';
}
export interface EntityInventorySearchQuerySubmittedParams {
kuery_fields: string[];
entity_types: string[];
action: 'submit' | 'refresh';
}
export interface EntityInventoryEntityTypeFilteredParams {
kuery_fields: string[];
entity_types: string[];
}
export interface EntityViewClickedParams {
entity_type: string;
view_type: 'detail' | 'flyout';
}
export type TelemetryEventParams =
| InventoryAddDataParams
| EntityInventoryViewedParams
| EntityInventorySearchQuerySubmittedParams
| EntityInventoryEntityTypeFilteredParams
| EntityViewClickedParams;
export interface ITelemetryClient {
reportInventoryAddData(params: InventoryAddDataParams): void;
reportEntityInventoryViewed(params: EntityInventoryViewedParams): void;
reportEntityInventorySearchQuerySubmitted(
params: EntityInventorySearchQuerySubmittedParams
): void;
reportEntityInventoryEntityTypeFiltered(params: EntityInventoryEntityTypeFilteredParams): void;
reportEntityViewClicked(params: EntityViewClickedParams): void;
}
export enum TelemetryEventTypes {
INVENTORY_ADD_DATA_CLICKED = 'inventory_add_data_clicked',
ENTITY_INVENTORY_VIEWED = 'Entity Inventory Viewed',
ENTITY_INVENTORY_SEARCH_QUERY_SUBMITTED = 'Entity Inventory Search Query Submitted',
ENTITY_INVENTORY_ENTITY_TYPE_FILTERED = 'Entity Inventory Entity Type Filtered',
ENTITY_VIEW_CLICKED = 'Entity View Clicked',
}
export interface TelemetryEvent {

View file

@ -6,7 +6,7 @@
*/
import type { InventoryAPIClient } from '../api';
import type { ITelemetryClient } from './telemetry/types';
import { ITelemetryClient } from './telemetry/types';
export interface InventoryServices {
inventoryAPIClient: InventoryAPIClient;

View file

@ -0,0 +1,44 @@
/*
* 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 { getKqlFieldsWithFallback } from './get_kql_field_names_with_fallback';
import { getKqlFieldNamesFromExpression } from '@kbn/es-query';
jest.mock('@kbn/es-query', () => ({
getKqlFieldNamesFromExpression: jest.fn(),
}));
describe('getKqlFieldsWithFallback', () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should return field names when getKqlFieldNamesFromExpression succeeds', () => {
const mockFieldNames = ['field1', 'field2'];
(getKqlFieldNamesFromExpression as jest.Mock).mockReturnValue(mockFieldNames);
const expectedArg = 'testKuery';
const result = getKqlFieldsWithFallback(expectedArg);
expect(result).toEqual(mockFieldNames);
expect(getKqlFieldNamesFromExpression).toHaveBeenCalledWith(expectedArg);
});
it('should return an empty array when getKqlFieldNamesFromExpression throws an error', () => {
(getKqlFieldNamesFromExpression as jest.Mock).mockImplementation(() => {
throw new Error('Test error');
});
const expectedArg = 'testKuery';
const result = getKqlFieldsWithFallback(expectedArg);
expect(result).toEqual([]);
expect(getKqlFieldNamesFromExpression).toHaveBeenCalledWith(expectedArg);
});
});

View file

@ -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.
*/
import { getKqlFieldNamesFromExpression } from '@kbn/es-query';
export function getKqlFieldsWithFallback(kuery: string): string[] {
try {
return getKqlFieldNamesFromExpression(kuery);
} catch (e) {
return [];
}
}