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.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\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\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\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:
parent
6f333b8ae0
commit
512cabcb34
16 changed files with 549 additions and 21 deletions
|
@ -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();
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 [];
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue