Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Entity Inventory] Add basic telemetry #197055

Merged
merged 44 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0871ba8
Create new Inventory telemetry events
iblancof Oct 17, 2024
b5f8f05
Add new events to ITelemetryClient
iblancof Oct 17, 2024
c3c957c
Add tests for new events
iblancof Oct 17, 2024
f6dd580
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 17, 2024
d05d1a3
Rename Search Query Submitted to Entity Inventory Search Query Submitted
iblancof Oct 17, 2024
307fe2b
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 18, 2024
4fee26d
Add event on entity click
iblancof Oct 18, 2024
21bb570
Add event on inventory viewed
iblancof Oct 18, 2024
5e010a9
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 21, 2024
5b2a9aa
Fix eventType
iblancof Oct 21, 2024
ade80e2
Add event on search query submitted
iblancof Oct 21, 2024
2231a41
Remove timerange from search query submitted
iblancof Oct 21, 2024
1f7369d
Create registerEemEnabledContext
iblancof Oct 21, 2024
0873ae3
Update eemEnabled on enablement
iblancof Oct 21, 2024
7ae4e9b
Register eemEnabled o plugin setup
iblancof Oct 21, 2024
ab08ebe
Rename eemEnabled to eem_enabled for telemetry
iblancof Oct 21, 2024
f56717b
Add event on entity type filtered
iblancof Oct 21, 2024
0533fb3
Add entity_types param in search submitted event
iblancof Oct 21, 2024
2806374
Rename filter event fn params
iblancof Oct 21, 2024
12acf93
Rename eemEnabled to eem_enabled
iblancof Oct 21, 2024
383e424
Refactor registerEemEnabledContext
iblancof Oct 21, 2024
bf12013
Create useIsLoadingComplete
iblancof Oct 21, 2024
91aa42a
Use isLoadingComplete hook in Inventory
iblancof Oct 21, 2024
d7d2aee
Use isLoadingComplete hook in Inventory
iblancof Oct 21, 2024
22b34d9
Add test for reportEntityInventoryEntityTypeFiltered
iblancof Oct 21, 2024
8a8568b
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 22, 2024
fd32a16
Use EntityType in telemetry events
iblancof Oct 22, 2024
925350f
Refactor search bar component to use optional parameters
iblancof Oct 22, 2024
a5926c0
Refactor entities_grid component to use constant for entity type
iblancof Oct 22, 2024
0851b1c
Refactor search bar component to use getKqlFieldsWithFallback
iblancof Oct 22, 2024
52f94cd
Move entity click telemetry from EntitiesGrid to EntityName
iblancof Oct 22, 2024
baf1270
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 23, 2024
a4bdbb2
Refactor telemetry initialization and update in inventory plugin
iblancof Oct 23, 2024
af1acc5
Delete registerEemEnabledContext
iblancof Oct 23, 2024
20776d3
Fix telemetry service tests
iblancof Oct 23, 2024
f7a6bee
Fix type in getMockInventoryContext
iblancof Oct 23, 2024
8233181
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 24, 2024
9fe837f
Remove unnecessary type annotation
iblancof Oct 24, 2024
d64b3c3
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 24, 2024
7bc59bd
Remove registerContextProvider in Inventory plugin
iblancof Oct 24, 2024
6b7be82
Merge branch 'main' into 195608-eco-inventory-telemetry
iblancof Oct 25, 2024
3a6dbfa
Fix tests
iblancof Oct 25, 2024
7a98daa
Fix types
iblancof Oct 25, 2024
5f5dba0
Remove EntityType usage
iblancof Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { HttpStart } from '@kbn/core-http-browser';
import { action } from '@storybook/addon-actions';
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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">
Expand All @@ -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;
Expand All @@ -62,6 +63,23 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode
[inventoryAPIClient]
);

const isLoadingComplete = useIsLoadingComplete({
iblancof marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ 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 { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider';
import { useAdHocInventoryDataView } from '../../hooks/use_adhoc_inventory_data_view';
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();
Expand All @@ -24,6 +26,7 @@ export function SearchBar() {
data: {
query: { queryString: queryStringService },
},
telemetry,
},
} = useKibana();

Expand All @@ -50,11 +53,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: string[]) => {
searchBarContentSubject$.next({ kuery, entityTypes: nextEntityTypes, refresh: false });
registerEntityTypeFilteredEvent({ filterEntityTypes: nextEntityTypes, filterKuery: kuery });
},
[kuery, searchBarContentSubject$]
[kuery, registerEntityTypeFilteredEvent, searchBarContentSubject$]
);

const handleQuerySubmit = useCallback<NonNullable<SearchBarOwnProps['onQuerySubmit']>>(
Expand All @@ -64,8 +97,14 @@ export function SearchBar() {
entityTypes,
refresh: !isUpdate,
});

registerSearchSubmittedEvent({
searchQuery: query,
searchEntityTypes: entityTypes,
searchIsUpdate: isUpdate,
});
},
[entityTypes, searchBarContentSubject$]
[entityTypes, registerSearchSubmittedEvent, searchBarContentSubject$]
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
11 changes: 8 additions & 3 deletions x-pack/plugins/observability_solution/inventory/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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;

Expand Down
Loading