From ec90e85b1a28b741ffa3f9322690d00f5440d82a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 1 Oct 2024 03:53:38 +1000 Subject: [PATCH] [8.x] [OneDiscover] Add EBT event to track field usage (#193996) (#194457) # Backport This will backport the following commits from `main` to `8.x`: - [[OneDiscover] Add EBT event to track field usage (#193996)](https://github.com/elastic/kibana/pull/193996) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Julia Rechkunova --- .../discover/public/__mocks__/services.ts | 2 + .../application/context/context_app.test.tsx | 1 + .../application/context/context_app.tsx | 31 +- .../components/layout/discover_documents.tsx | 32 +- .../components/layout/discover_layout.tsx | 40 ++- src/plugins/discover/public/build_services.ts | 10 +- .../context_awareness/__mocks__/index.tsx | 8 +- .../profiles_manager.test.ts | 4 +- .../context_awareness/profiles_manager.ts | 10 +- src/plugins/discover/public/plugin.tsx | 86 +++-- .../discover_ebt_context_manager.test.ts | 95 ----- .../services/discover_ebt_context_manager.ts | 75 ---- .../services/discover_ebt_manager.test.ts | 242 +++++++++++++ .../public/services/discover_ebt_manager.ts | 219 ++++++++++++ .../context_awareness/_data_source_profile.ts | 101 +----- .../discover/context_awareness/_telemetry.ts | 326 ++++++++++++++++++ .../apps/discover/context_awareness/index.ts | 1 + 17 files changed, 946 insertions(+), 337 deletions(-) delete mode 100644 src/plugins/discover/public/services/discover_ebt_context_manager.test.ts delete mode 100644 src/plugins/discover/public/services/discover_ebt_context_manager.ts create mode 100644 src/plugins/discover/public/services/discover_ebt_manager.test.ts create mode 100644 src/plugins/discover/public/services/discover_ebt_manager.ts create mode 100644 test/functional/apps/discover/context_awareness/_telemetry.ts diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 3d78239558f3e..f00d105444630 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -45,6 +45,7 @@ import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { urlTrackerMock } from './url_tracker.mock'; import { createElement } from 'react'; import { createContextAwarenessMocks } from '../context_awareness/__mocks__'; +import { DiscoverEBTManager } from '../services/discover_ebt_manager'; export function createDiscoverServicesMock(): DiscoverServices { const dataPlugin = dataPluginMock.createStartContract(); @@ -245,6 +246,7 @@ export function createDiscoverServicesMock(): DiscoverServices { singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, urlTracker: urlTrackerMock, profilesManager: profilesManagerMock, + ebtManager: new DiscoverEBTManager(), setHeaderActionMenu: jest.fn(), } as unknown as DiscoverServices; } diff --git a/src/plugins/discover/public/application/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx index 9c77d1e40bbb2..7a99194cad575 100644 --- a/src/plugins/discover/public/application/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/context/context_app.test.tsx @@ -72,6 +72,7 @@ describe('ContextApp test', () => { contextLocator: { getRedirectUrl: jest.fn(() => '') }, singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, profilesManager: discoverServices.profilesManager, + ebtManager: discoverServices.ebtManager, timefilter: discoverServices.timefilter, uiActions: discoverServices.uiActions, } as unknown as DiscoverServices; diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index e0dfa985b594e..b0fc1342a8f72 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -56,6 +56,8 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) => navigation, filterManager, core, + ebtManager, + fieldsMetadata, } = services; const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); @@ -199,15 +201,36 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) => ); const addFilter = useCallback( - async (field: DataViewField | string, values: unknown, operation: string) => { + async (field: DataViewField | string, values: unknown, operation: '+' | '-') => { const newFilters = generateFilters(filterManager, field, values, operation, dataView); filterManager.addFilters(newFilters); if (dataViews) { const fieldName = typeof field === 'string' ? field : field.name; await popularizeField(dataView, fieldName, dataViews, capabilities); + void ebtManager.trackFilterAddition({ + fieldName: fieldName === '_exists_' ? String(values) : fieldName, + filterOperation: fieldName === '_exists_' ? '_exists_' : operation, + fieldsMetadata, + }); } }, - [filterManager, dataViews, dataView, capabilities] + [filterManager, dataViews, dataView, capabilities, ebtManager, fieldsMetadata] + ); + + const onAddColumnWithTracking = useCallback( + (columnName: string) => { + onAddColumn(columnName); + void ebtManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata }); + }, + [onAddColumn, ebtManager, fieldsMetadata] + ); + + const onRemoveColumnWithTracking = useCallback( + (columnName: string) => { + onRemoveColumn(columnName); + void ebtManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata }); + }, + [onRemoveColumn, ebtManager, fieldsMetadata] ); const TopNavMenu = navigation.ui.AggregateQueryTopNavMenu; @@ -271,8 +294,8 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) => isLegacy={isLegacy} columns={columns} grid={appState.grid} - onAddColumn={onAddColumn} - onRemoveColumn={onRemoveColumn} + onAddColumn={onAddColumnWithTracking} + onRemoveColumn={onRemoveColumnWithTracking} onSetColumns={onSetColumns} predecessorCount={appState.predecessorCount} successorCount={appState.successorCount} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 2fe2a4f5a8f93..77befc4dc334f 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -117,7 +117,7 @@ function DiscoverDocumentsComponent({ const services = useDiscoverServices(); const documents$ = stateContainer.dataState.data$.documents$; const savedSearch = useSavedSearchInitial(); - const { dataViews, capabilities, uiSettings, uiActions } = services; + const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services; const [ dataSource, query, @@ -200,6 +200,22 @@ function DiscoverDocumentsComponent({ settings: grid, }); + const onAddColumnWithTracking = useCallback( + (columnName: string) => { + onAddColumn(columnName); + void ebtManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata }); + }, + [onAddColumn, ebtManager, fieldsMetadata] + ); + + const onRemoveColumnWithTracking = useCallback( + (columnName: string) => { + onRemoveColumn(columnName); + void ebtManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata }); + }, + [onRemoveColumn, ebtManager, fieldsMetadata] + ); + const setExpandedDoc = useCallback( (doc: DataTableRecord | undefined) => { stateContainer.internalState.transitions.setExpandedDoc(doc); @@ -299,14 +315,22 @@ function DiscoverDocumentsComponent({ columnsMeta={customColumnsMeta} savedSearchId={savedSearch.id} onFilter={onAddFilter} - onRemoveColumn={onRemoveColumn} - onAddColumn={onAddColumn} + onRemoveColumn={onRemoveColumnWithTracking} + onAddColumn={onAddColumnWithTracking} onClose={() => setExpandedDoc(undefined)} setExpandedDoc={setExpandedDoc} query={query} /> ), - [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] + [ + dataView, + onAddColumnWithTracking, + onAddFilter, + onRemoveColumnWithTracking, + query, + savedSearch.id, + setExpandedDoc, + ] ); const configRowHeight = uiSettings.get(ROW_HEIGHT_OPTION); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 49e645e3f2206..bc9cad72a5eb6 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -78,6 +78,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { spaces, observabilityAIAssistant, dataVisualizer: dataVisualizerService, + ebtManager, + fieldsMetadata, } = useDiscoverServices(); const pageBackgroundColor = useEuiBackgroundColor('plain'); const globalQueryState = data.query.getState(); @@ -154,6 +156,22 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { settings: grid, }); + const onAddColumnWithTracking = useCallback( + (columnName: string) => { + onAddColumn(columnName); + void ebtManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata }); + }, + [onAddColumn, ebtManager, fieldsMetadata] + ); + + const onRemoveColumnWithTracking = useCallback( + (columnName: string) => { + onRemoveColumn(columnName); + void ebtManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata }); + }, + [onRemoveColumn, ebtManager, fieldsMetadata] + ); + // The assistant is getting the state from the url correctly // expect from the index pattern where we have only the dataview id useEffect(() => { @@ -175,9 +193,14 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); } + void ebtManager.trackFilterAddition({ + fieldName: fieldName === '_exists_' ? String(values) : fieldName, + filterOperation: fieldName === '_exists_' ? '_exists_' : operation, + fieldsMetadata, + }); return filterManager.addFilters(newFilters); }, - [filterManager, dataView, dataViews, trackUiMetric, capabilities] + [filterManager, dataView, dataViews, trackUiMetric, capabilities, ebtManager, fieldsMetadata] ); const getOperator = (fieldName: string, values: unknown, operation: '+' | '-') => { @@ -222,8 +245,13 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, 'esql_filter_added'); } + void ebtManager.trackFilterAddition({ + fieldName: fieldName === '_exists_' ? String(values) : fieldName, + filterOperation: fieldName === '_exists_' ? '_exists_' : operation, + fieldsMetadata, + }); }, - [data.query.queryString, query, trackUiMetric] + [data.query.queryString, query, trackUiMetric, ebtManager, fieldsMetadata] ); const onFilter = isEsqlMode ? onPopulateWhereClause : onAddFilter; @@ -274,8 +302,8 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { return undefined; } - return () => onAddColumn(draggingFieldName); - }, [onAddColumn, draggingFieldName, currentColumns]); + return () => onAddColumnWithTracking(draggingFieldName); + }, [onAddColumnWithTracking, draggingFieldName, currentColumns]); const [sidebarToggleState$] = useState>( () => new BehaviorSubject({ isCollapsed: false, toggle: () => {} }) @@ -396,10 +424,10 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { sidebarPanel={ { const { usageCollection } = plugins; @@ -223,7 +223,7 @@ export const buildServices = memoize( noDataPage: plugins.noDataPage, observabilityAIAssistant: plugins.observabilityAIAssistant, profilesManager, - ebtContextManager, + ebtManager, fieldsMetadata: plugins.fieldsMetadata, logsDataAccess: plugins.logsDataAccess, }; diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.tsx b/src/plugins/discover/public/context_awareness/__mocks__/index.tsx index a15b7aa26a8a0..153d401cc980a 100644 --- a/src/plugins/discover/public/context_awareness/__mocks__/index.tsx +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.tsx @@ -23,7 +23,7 @@ import { } from '../profiles'; import { ProfileProviderServices } from '../profile_providers/profile_provider_services'; import { ProfilesManager } from '../profiles_manager'; -import { DiscoverEBTContextManager } from '../../services/discover_ebt_context_manager'; +import { DiscoverEBTManager } from '../../services/discover_ebt_manager'; import { createLogsContextServiceMock } from '@kbn/discover-utils/src/__mocks__'; export const createContextAwarenessMocks = ({ @@ -152,12 +152,12 @@ export const createContextAwarenessMocks = ({ documentProfileServiceMock.registerProvider(documentProfileProviderMock); } - const ebtContextManagerMock = new DiscoverEBTContextManager(); + const ebtManagerMock = new DiscoverEBTManager(); const profilesManagerMock = new ProfilesManager( rootProfileServiceMock, dataSourceProfileServiceMock, documentProfileServiceMock, - ebtContextManagerMock + ebtManagerMock ); const profileProviderServices = createProfileProviderServicesMock(); @@ -173,7 +173,7 @@ export const createContextAwarenessMocks = ({ contextRecordMock2, profilesManagerMock, profileProviderServices, - ebtContextManagerMock, + ebtManagerMock, }; }; diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts index 87965edbe7488..da5ad8b56dcf3 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts @@ -21,7 +21,7 @@ describe('ProfilesManager', () => { beforeEach(() => { jest.clearAllMocks(); mocks = createContextAwarenessMocks(); - jest.spyOn(mocks.ebtContextManagerMock, 'updateProfilesContextWith'); + jest.spyOn(mocks.ebtManagerMock, 'updateProfilesContextWith'); }); it('should return default profiles', () => { @@ -62,7 +62,7 @@ describe('ProfilesManager', () => { mocks.documentProfileProviderMock.profile, ]); - expect(mocks.ebtContextManagerMock.updateProfilesContextWith).toHaveBeenCalledWith([ + expect(mocks.ebtManagerMock.updateProfilesContextWith).toHaveBeenCalledWith([ 'root-profile', 'data-source-profile', ]); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index 2c8b1c7d16cb0..6b7bef5e02294 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -25,7 +25,7 @@ import type { DocumentContext, } from './profiles'; import type { ContextWithProfileId } from './profile_service'; -import { DiscoverEBTContextManager } from '../services/discover_ebt_context_manager'; +import { DiscoverEBTManager } from '../services/discover_ebt_manager'; interface SerializedRootProfileParams { solutionNavId: RootProfileProviderParams['solutionNavId']; @@ -53,7 +53,7 @@ export interface GetProfilesOptions { export class ProfilesManager { private readonly rootContext$: BehaviorSubject>; private readonly dataSourceContext$: BehaviorSubject>; - private readonly ebtContextManager: DiscoverEBTContextManager; + private readonly ebtManager: DiscoverEBTManager; private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; @@ -64,11 +64,11 @@ export class ProfilesManager { private readonly rootProfileService: RootProfileService, private readonly dataSourceProfileService: DataSourceProfileService, private readonly documentProfileService: DocumentProfileService, - ebtContextManager: DiscoverEBTContextManager + ebtManager: DiscoverEBTManager ) { this.rootContext$ = new BehaviorSubject(rootProfileService.defaultContext); this.dataSourceContext$ = new BehaviorSubject(dataSourceProfileService.defaultContext); - this.ebtContextManager = ebtContextManager; + this.ebtManager = ebtManager; } /** @@ -206,7 +206,7 @@ export class ProfilesManager { private trackActiveProfiles(rootContextProfileId: string, dataSourceContextProfileId: string) { const dscProfiles = [rootContextProfileId, dataSourceContextProfileId]; - this.ebtContextManager.updateProfilesContextWith(dscProfiles); + this.ebtManager.updateProfilesContextWith(dscProfiles); } } diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index e6430f82c62fe..dbbcc90a7d451 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -59,7 +59,7 @@ import { RootProfileService } from './context_awareness/profiles/root_profile'; import { DataSourceProfileService } from './context_awareness/profiles/data_source_profile'; import { DocumentProfileService } from './context_awareness/profiles/document_profile'; import { ProfilesManager } from './context_awareness/profiles_manager'; -import { DiscoverEBTContextManager } from './services/discover_ebt_context_manager'; +import { DiscoverEBTManager } from './services/discover_ebt_manager'; /** * Contains Discover, one of the oldest parts of Kibana @@ -149,8 +149,12 @@ export class DiscoverPlugin this.urlTracker = { setTrackedUrl, restorePreviousUrl, setTrackingEnabled }; this.stopUrlTracking = stopUrlTracker; - const ebtContextManager = new DiscoverEBTContextManager(); - ebtContextManager.initialize({ core }); + const ebtManager = new DiscoverEBTManager(); + ebtManager.initialize({ + core, + shouldInitializeCustomContext: true, + shouldInitializeCustomEvents: true, + }); core.application.register({ id: PLUGIN_ID, @@ -176,7 +180,7 @@ export class DiscoverPlugin window.dispatchEvent(new HashChangeEvent('hashchange')); }); - ebtContextManager.enable(); + ebtManager.enableContext(); const services = buildServices({ core: coreStart, @@ -188,12 +192,12 @@ export class DiscoverPlugin history: this.historyService.getHistory(), scopedHistory: this.scopedHistory, urlTracker: this.urlTracker!, - profilesManager: await this.createProfilesManager( - coreStart, - discoverStartPlugins, - ebtContextManager - ), - ebtContextManager, + profilesManager: await this.createProfilesManager({ + core: coreStart, + plugins: discoverStartPlugins, + ebtManager, + }), + ebtManager, setHeaderActionMenu: params.setHeaderActionMenu, }); @@ -226,7 +230,7 @@ export class DiscoverPlugin }); return () => { - ebtContextManager.disableAndReset(); + ebtManager.disableAndResetContext(); unlistenParentHistory(); unmount(); appUnMounted(); @@ -296,11 +300,12 @@ export class DiscoverPlugin } const getDiscoverServicesInternal = () => { + const ebtManager = new DiscoverEBTManager(); // It is not initialized outside of Discover return this.getDiscoverServices( core, plugins, - this.createEmptyProfilesManager(), - new DiscoverEBTContextManager() // it's not enabled outside of Discover + this.createEmptyProfilesManager({ ebtManager }), + ebtManager ); }; @@ -326,11 +331,15 @@ export class DiscoverPlugin return { rootProfileService, dataSourceProfileService, documentProfileService }; } - private createProfilesManager = async ( - core: CoreStart, - plugins: DiscoverStartPlugins, - ebtContextManager: DiscoverEBTContextManager - ) => { + private async createProfilesManager({ + core, + plugins, + ebtManager, + }: { + core: CoreStart; + plugins: DiscoverStartPlugins; + ebtManager: DiscoverEBTManager; + }) { const { registerProfileProviders } = await import('./context_awareness/profile_providers'); const { rootProfileService, dataSourceProfileService, documentProfileService } = this.createProfileServices(); @@ -341,7 +350,7 @@ export class DiscoverPlugin rootProfileService, dataSourceProfileService, documentProfileService, - ebtContextManager + ebtManager ); await registerProfileProviders({ @@ -349,21 +358,18 @@ export class DiscoverPlugin dataSourceProfileService, documentProfileService, enabledExperimentalProfileIds, - services: this.getDiscoverServices(core, plugins, profilesManager, ebtContextManager), + services: this.getDiscoverServices(core, plugins, profilesManager, ebtManager), }); return profilesManager; - }; - - private createEmptyProfilesManager() { - const { rootProfileService, dataSourceProfileService, documentProfileService } = - this.createProfileServices(); + } + private createEmptyProfilesManager({ ebtManager }: { ebtManager: DiscoverEBTManager }) { return new ProfilesManager( - rootProfileService, - dataSourceProfileService, - documentProfileService, - new DiscoverEBTContextManager() // it's not enabled outside of Discover + new RootProfileService(), + new DataSourceProfileService(), + new DocumentProfileService(), + ebtManager ); } @@ -371,7 +377,7 @@ export class DiscoverPlugin core: CoreStart, plugins: DiscoverStartPlugins, profilesManager: ProfilesManager, - ebtContextManager: DiscoverEBTContextManager + ebtManager: DiscoverEBTManager ) => { return buildServices({ core, @@ -383,11 +389,13 @@ export class DiscoverPlugin history: this.historyService.getHistory(), urlTracker: this.urlTracker!, profilesManager, - ebtContextManager, + ebtManager, }); }; private registerEmbeddable(core: CoreSetup, plugins: DiscoverSetupPlugins) { + const ebtManager = new DiscoverEBTManager(); // It is not initialized outside of Discover + const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); return { @@ -396,16 +404,20 @@ export class DiscoverPlugin }; }; - const getDiscoverServicesInternal = async () => { + const getDiscoverServicesForEmbeddable = async () => { const [coreStart, deps] = await core.getStartServices(); - const ebtContextManager = new DiscoverEBTContextManager(); // it's not enabled outside of Discover - const profilesManager = await this.createProfilesManager(coreStart, deps, ebtContextManager); - return this.getDiscoverServices(coreStart, deps, profilesManager, ebtContextManager); + + const profilesManager = await this.createProfilesManager({ + core: coreStart, + plugins: deps, + ebtManager, + }); + return this.getDiscoverServices(coreStart, deps, profilesManager, ebtManager); }; plugins.embeddable.registerReactEmbeddableSavedObject({ onAdd: async (container, savedObject) => { - const services = await getDiscoverServicesInternal(); + const services = await getDiscoverServicesForEmbeddable(); const initialState = await deserializeState({ serializedState: { rawState: { savedObjectId: savedObject.id }, @@ -429,7 +441,7 @@ export class DiscoverPlugin plugins.embeddable.registerReactEmbeddableFactory(SEARCH_EMBEDDABLE_TYPE, async () => { const [startServices, discoverServices, { getSearchEmbeddableFactory }] = await Promise.all([ getStartServices(), - getDiscoverServicesInternal(), + getDiscoverServicesForEmbeddable(), import('./embeddable/get_search_embeddable_factory'), ]); diff --git a/src/plugins/discover/public/services/discover_ebt_context_manager.test.ts b/src/plugins/discover/public/services/discover_ebt_context_manager.test.ts deleted file mode 100644 index 3b2836325b671..0000000000000 --- a/src/plugins/discover/public/services/discover_ebt_context_manager.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { DiscoverEBTContextManager } from './discover_ebt_context_manager'; - -const coreSetupMock = coreMock.createSetup(); - -describe('DiscoverEBTContextManager', () => { - let discoverEBTContextManager: DiscoverEBTContextManager; - - beforeEach(() => { - discoverEBTContextManager = new DiscoverEBTContextManager(); - }); - - describe('register', () => { - it('should register the context provider', () => { - discoverEBTContextManager.initialize({ core: coreSetupMock }); - - expect(coreSetupMock.analytics.registerContextProvider).toHaveBeenCalledWith({ - name: 'discover_context', - context$: expect.any(BehaviorSubject), - schema: { - discoverProfiles: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: 'List of active Discover context awareness profiles', - }, - }, - }, - }, - }); - }); - }); - - describe('updateProfilesWith', () => { - it('should update the profiles with the provided props', () => { - const dscProfiles = ['profile1', 'profile2']; - const dscProfiles2 = ['profile21', 'profile22']; - discoverEBTContextManager.initialize({ core: coreSetupMock }); - discoverEBTContextManager.enable(); - - discoverEBTContextManager.updateProfilesContextWith(dscProfiles); - expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); - - discoverEBTContextManager.updateProfilesContextWith(dscProfiles2); - expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles2); - }); - - it('should not update the profiles if profile list did not change', () => { - const dscProfiles = ['profile1', 'profile2']; - const dscProfiles2 = ['profile1', 'profile2']; - discoverEBTContextManager.initialize({ core: coreSetupMock }); - discoverEBTContextManager.enable(); - - discoverEBTContextManager.updateProfilesContextWith(dscProfiles); - expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); - - discoverEBTContextManager.updateProfilesContextWith(dscProfiles2); - expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); - }); - - it('should not update the profiles if not enabled yet', () => { - const dscProfiles = ['profile1', 'profile2']; - discoverEBTContextManager.initialize({ core: coreSetupMock }); - - discoverEBTContextManager.updateProfilesContextWith(dscProfiles); - expect(discoverEBTContextManager.getProfilesContext()).toEqual([]); - }); - - it('should not update the profiles after resetting unless enabled again', () => { - const dscProfiles = ['profile1', 'profile2']; - discoverEBTContextManager.initialize({ core: coreSetupMock }); - discoverEBTContextManager.enable(); - discoverEBTContextManager.updateProfilesContextWith(dscProfiles); - expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); - discoverEBTContextManager.disableAndReset(); - expect(discoverEBTContextManager.getProfilesContext()).toEqual([]); - discoverEBTContextManager.updateProfilesContextWith(dscProfiles); - expect(discoverEBTContextManager.getProfilesContext()).toEqual([]); - discoverEBTContextManager.enable(); - discoverEBTContextManager.updateProfilesContextWith(dscProfiles); - expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); - }); - }); -}); diff --git a/src/plugins/discover/public/services/discover_ebt_context_manager.ts b/src/plugins/discover/public/services/discover_ebt_context_manager.ts deleted file mode 100644 index 12ea918c495d9..0000000000000 --- a/src/plugins/discover/public/services/discover_ebt_context_manager.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { BehaviorSubject } from 'rxjs'; -import { isEqual } from 'lodash'; -import type { CoreSetup } from '@kbn/core-lifecycle-browser'; - -export interface DiscoverEBTContextProps { - discoverProfiles: string[]; // Discover Context Awareness Profiles -} -export type DiscoverEBTContext = BehaviorSubject; - -export class DiscoverEBTContextManager { - private isEnabled: boolean = false; - private ebtContext$: DiscoverEBTContext | undefined; - - constructor() {} - - // https://docs.elastic.dev/telemetry/collection/event-based-telemetry - public initialize({ core }: { core: CoreSetup }) { - const context$ = new BehaviorSubject({ - discoverProfiles: [], - }); - - core.analytics.registerContextProvider({ - name: 'discover_context', - context$, - schema: { - discoverProfiles: { - type: 'array', - items: { - type: 'keyword', - _meta: { - description: 'List of active Discover context awareness profiles', - }, - }, - }, - // If we decide to extend EBT context with more properties, we can do it here - }, - }); - - this.ebtContext$ = context$; - } - - public enable() { - this.isEnabled = true; - } - - public updateProfilesContextWith(discoverProfiles: DiscoverEBTContextProps['discoverProfiles']) { - if ( - this.isEnabled && - this.ebtContext$ && - !isEqual(this.ebtContext$.getValue().discoverProfiles, discoverProfiles) - ) { - this.ebtContext$.next({ - discoverProfiles, - }); - } - } - - public getProfilesContext() { - return this.ebtContext$?.getValue()?.discoverProfiles; - } - - public disableAndReset() { - this.updateProfilesContextWith([]); - this.isEnabled = false; - } -} diff --git a/src/plugins/discover/public/services/discover_ebt_manager.test.ts b/src/plugins/discover/public/services/discover_ebt_manager.test.ts new file mode 100644 index 0000000000000..0ed20dacdb0ce --- /dev/null +++ b/src/plugins/discover/public/services/discover_ebt_manager.test.ts @@ -0,0 +1,242 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; +import { coreMock } from '@kbn/core/public/mocks'; +import { DiscoverEBTManager } from './discover_ebt_manager'; +import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; + +describe('DiscoverEBTManager', () => { + let discoverEBTContextManager: DiscoverEBTManager; + + const coreSetupMock = coreMock.createSetup(); + + const fieldsMetadata = { + getClient: jest.fn().mockResolvedValue({ + find: jest.fn().mockResolvedValue({ + fields: { + test: { + short: 'test', + }, + }, + }), + }), + } as unknown as FieldsMetadataPublicStart; + + beforeEach(() => { + discoverEBTContextManager = new DiscoverEBTManager(); + }); + + describe('register', () => { + it('should register the context provider and custom events', () => { + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: true, + shouldInitializeCustomEvents: true, + }); + + expect(coreSetupMock.analytics.registerContextProvider).toHaveBeenCalledWith({ + name: 'discover_context', + context$: expect.any(BehaviorSubject), + schema: { + discoverProfiles: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: 'List of active Discover context awareness profiles', + }, + }, + }, + }, + }); + + expect(coreSetupMock.analytics.registerEventType).toHaveBeenCalledWith({ + eventType: 'discover_field_usage', + schema: { + eventName: { + type: 'keyword', + _meta: { + description: + 'The name of the event that is tracked in the metrics i.e. dataTableSelection, dataTableRemoval', + }, + }, + fieldName: { + type: 'keyword', + _meta: { + description: "Field name if it's a part of ECS schema", + optional: true, + }, + }, + filterOperation: { + type: 'keyword', + _meta: { + description: "Operation type when a filter is added i.e. '+', '-', '_exists_'", + optional: true, + }, + }, + }, + }); + }); + }); + + describe('updateProfilesWith', () => { + it('should update the profiles with the provided props', () => { + const dscProfiles = ['profile1', 'profile2']; + const dscProfiles2 = ['profile21', 'profile22']; + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: true, + shouldInitializeCustomEvents: false, + }); + discoverEBTContextManager.enableContext(); + + discoverEBTContextManager.updateProfilesContextWith(dscProfiles); + expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); + + discoverEBTContextManager.updateProfilesContextWith(dscProfiles2); + expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles2); + }); + + it('should not update the profiles if profile list did not change', () => { + const dscProfiles = ['profile1', 'profile2']; + const dscProfiles2 = ['profile1', 'profile2']; + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: true, + shouldInitializeCustomEvents: false, + }); + discoverEBTContextManager.enableContext(); + + discoverEBTContextManager.updateProfilesContextWith(dscProfiles); + expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); + + discoverEBTContextManager.updateProfilesContextWith(dscProfiles2); + expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); + }); + + it('should not update the profiles if not enabled yet', () => { + const dscProfiles = ['profile1', 'profile2']; + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: true, + shouldInitializeCustomEvents: false, + }); + + discoverEBTContextManager.updateProfilesContextWith(dscProfiles); + expect(discoverEBTContextManager.getProfilesContext()).toEqual([]); + }); + + it('should not update the profiles after resetting unless enabled again', () => { + const dscProfiles = ['profile1', 'profile2']; + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: true, + shouldInitializeCustomEvents: false, + }); + discoverEBTContextManager.enableContext(); + discoverEBTContextManager.updateProfilesContextWith(dscProfiles); + expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); + discoverEBTContextManager.disableAndResetContext(); + expect(discoverEBTContextManager.getProfilesContext()).toEqual([]); + discoverEBTContextManager.updateProfilesContextWith(dscProfiles); + expect(discoverEBTContextManager.getProfilesContext()).toEqual([]); + discoverEBTContextManager.enableContext(); + discoverEBTContextManager.updateProfilesContextWith(dscProfiles); + expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles); + }); + }); + + describe('trackFieldUsageEvent', () => { + it('should track the field usage when a field is added to the table', async () => { + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: false, + shouldInitializeCustomEvents: true, + }); + + await discoverEBTContextManager.trackDataTableSelection({ + fieldName: 'test', + fieldsMetadata, + }); + + expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledWith('discover_field_usage', { + eventName: 'dataTableSelection', + fieldName: 'test', + }); + + await discoverEBTContextManager.trackDataTableSelection({ + fieldName: 'test2', + fieldsMetadata, + }); + + expect(coreSetupMock.analytics.reportEvent).toHaveBeenLastCalledWith('discover_field_usage', { + eventName: 'dataTableSelection', // non-ECS fields would not be included in properties + }); + }); + + it('should track the field usage when a field is removed from the table', async () => { + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: false, + shouldInitializeCustomEvents: true, + }); + + await discoverEBTContextManager.trackDataTableRemoval({ + fieldName: 'test', + fieldsMetadata, + }); + + expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledWith('discover_field_usage', { + eventName: 'dataTableRemoval', + fieldName: 'test', + }); + + await discoverEBTContextManager.trackDataTableRemoval({ + fieldName: 'test2', + fieldsMetadata, + }); + + expect(coreSetupMock.analytics.reportEvent).toHaveBeenLastCalledWith('discover_field_usage', { + eventName: 'dataTableRemoval', // non-ECS fields would not be included in properties + }); + }); + + it('should track the field usage when a filter is created', async () => { + discoverEBTContextManager.initialize({ + core: coreSetupMock, + shouldInitializeCustomContext: false, + shouldInitializeCustomEvents: true, + }); + + await discoverEBTContextManager.trackFilterAddition({ + fieldName: 'test', + fieldsMetadata, + filterOperation: '+', + }); + + expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledWith('discover_field_usage', { + eventName: 'filterAddition', + fieldName: 'test', + filterOperation: '+', + }); + + await discoverEBTContextManager.trackFilterAddition({ + fieldName: 'test2', + fieldsMetadata, + filterOperation: '_exists_', + }); + + expect(coreSetupMock.analytics.reportEvent).toHaveBeenLastCalledWith('discover_field_usage', { + eventName: 'filterAddition', // non-ECS fields would not be included in properties + filterOperation: '_exists_', + }); + }); + }); +}); diff --git a/src/plugins/discover/public/services/discover_ebt_manager.ts b/src/plugins/discover/public/services/discover_ebt_manager.ts new file mode 100644 index 0000000000000..420eb6c244444 --- /dev/null +++ b/src/plugins/discover/public/services/discover_ebt_manager.ts @@ -0,0 +1,219 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; +import { isEqual } from 'lodash'; +import type { CoreSetup } from '@kbn/core-lifecycle-browser'; +import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; + +const FIELD_USAGE_EVENT_TYPE = 'discover_field_usage'; +const FIELD_USAGE_EVENT_NAME = 'eventName'; +const FIELD_USAGE_FIELD_NAME = 'fieldName'; +const FIELD_USAGE_FILTER_OPERATION = 'filterOperation'; + +type FilterOperation = '+' | '-' | '_exists_'; + +export enum FieldUsageEventName { + dataTableSelection = 'dataTableSelection', + dataTableRemoval = 'dataTableRemoval', + filterAddition = 'filterAddition', +} +interface FieldUsageEventData { + [FIELD_USAGE_EVENT_NAME]: FieldUsageEventName; + [FIELD_USAGE_FIELD_NAME]?: string; + [FIELD_USAGE_FILTER_OPERATION]?: FilterOperation; +} + +export interface DiscoverEBTContextProps { + discoverProfiles: string[]; // Discover Context Awareness Profiles +} +export type DiscoverEBTContext = BehaviorSubject; + +export class DiscoverEBTManager { + private isCustomContextEnabled: boolean = false; + private customContext$: DiscoverEBTContext | undefined; + private reportEvent: CoreSetup['analytics']['reportEvent'] | undefined; + + constructor() {} + + // https://docs.elastic.dev/telemetry/collection/event-based-telemetry + public initialize({ + core, + shouldInitializeCustomContext, + shouldInitializeCustomEvents, + }: { + core: CoreSetup; + shouldInitializeCustomContext: boolean; + shouldInitializeCustomEvents: boolean; + }) { + if (shouldInitializeCustomContext) { + // Register Discover specific context to be used in EBT + const context$ = new BehaviorSubject({ + discoverProfiles: [], + }); + core.analytics.registerContextProvider({ + name: 'discover_context', + context$, + schema: { + discoverProfiles: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: 'List of active Discover context awareness profiles', + }, + }, + }, + // If we decide to extend EBT context with more properties, we can do it here + }, + }); + this.customContext$ = context$; + } + + if (shouldInitializeCustomEvents) { + // Register Discover events to be used with EBT + core.analytics.registerEventType({ + eventType: FIELD_USAGE_EVENT_TYPE, + schema: { + [FIELD_USAGE_EVENT_NAME]: { + type: 'keyword', + _meta: { + description: + 'The name of the event that is tracked in the metrics i.e. dataTableSelection, dataTableRemoval', + }, + }, + [FIELD_USAGE_FIELD_NAME]: { + type: 'keyword', + _meta: { + description: "Field name if it's a part of ECS schema", + optional: true, + }, + }, + [FIELD_USAGE_FILTER_OPERATION]: { + type: 'keyword', + _meta: { + description: "Operation type when a filter is added i.e. '+', '-', '_exists_'", + optional: true, + }, + }, + }, + }); + this.reportEvent = core.analytics.reportEvent; + } + } + + public enableContext() { + this.isCustomContextEnabled = true; + } + + public disableAndResetContext() { + this.updateProfilesContextWith([]); + this.isCustomContextEnabled = false; + } + + public updateProfilesContextWith(discoverProfiles: DiscoverEBTContextProps['discoverProfiles']) { + if ( + this.isCustomContextEnabled && + this.customContext$ && + !isEqual(this.customContext$.getValue().discoverProfiles, discoverProfiles) + ) { + this.customContext$.next({ + discoverProfiles, + }); + } + } + + public getProfilesContext() { + return this.customContext$?.getValue()?.discoverProfiles; + } + + private async trackFieldUsageEvent({ + eventName, + fieldName, + filterOperation, + fieldsMetadata, + }: { + eventName: FieldUsageEventName; + fieldName: string; + filterOperation?: FilterOperation; + fieldsMetadata: FieldsMetadataPublicStart | undefined; + }) { + if (!this.reportEvent) { + return; + } + + const eventData: FieldUsageEventData = { + [FIELD_USAGE_EVENT_NAME]: eventName, + }; + + if (fieldsMetadata) { + const client = await fieldsMetadata.getClient(); + const { fields } = await client.find({ + attributes: ['short'], + fieldNames: [fieldName], + }); + + // excludes non ECS fields + if (fields[fieldName]?.short) { + eventData[FIELD_USAGE_FIELD_NAME] = fieldName; + } + } + + if (filterOperation) { + eventData[FIELD_USAGE_FILTER_OPERATION] = filterOperation; + } + + this.reportEvent(FIELD_USAGE_EVENT_TYPE, eventData); + } + + public async trackDataTableSelection({ + fieldName, + fieldsMetadata, + }: { + fieldName: string; + fieldsMetadata: FieldsMetadataPublicStart | undefined; + }) { + await this.trackFieldUsageEvent({ + eventName: FieldUsageEventName.dataTableSelection, + fieldName, + fieldsMetadata, + }); + } + + public async trackDataTableRemoval({ + fieldName, + fieldsMetadata, + }: { + fieldName: string; + fieldsMetadata: FieldsMetadataPublicStart | undefined; + }) { + await this.trackFieldUsageEvent({ + eventName: FieldUsageEventName.dataTableRemoval, + fieldName, + fieldsMetadata, + }); + } + + public async trackFilterAddition({ + fieldName, + fieldsMetadata, + filterOperation, + }: { + fieldName: string; + fieldsMetadata: FieldsMetadataPublicStart | undefined; + filterOperation: FilterOperation; + }) { + await this.trackFieldUsageEvent({ + eventName: FieldUsageEventName.filterAddition, + fieldName, + fieldsMetadata, + filterOperation, + }); + } +} diff --git a/test/functional/apps/discover/context_awareness/_data_source_profile.ts b/test/functional/apps/discover/context_awareness/_data_source_profile.ts index ecf4b2fb29c4c..35e3552afa655 100644 --- a/test/functional/apps/discover/context_awareness/_data_source_profile.ts +++ b/test/functional/apps/discover/context_awareness/_data_source_profile.ts @@ -12,115 +12,16 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const { common, discover, unifiedFieldList, dashboard, header, timePicker } = getPageObjects([ + const { common, discover, unifiedFieldList } = getPageObjects([ 'common', 'discover', 'unifiedFieldList', - 'dashboard', - 'header', - 'timePicker', ]); const testSubjects = getService('testSubjects'); const dataViews = getService('dataViews'); const dataGrid = getService('dataGrid'); - const monacoEditor = getService('monacoEditor'); - const ebtUIHelper = getService('kibana_ebt_ui'); - const retry = getService('retry'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const dashboardAddPanel = getService('dashboardAddPanel'); describe('data source profile', () => { - describe('telemetry', () => { - before(async () => { - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); - }); - - after(async () => { - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); - }); - - it('should set EBT context for telemetry events with default profile', async () => { - await common.navigateToApp('discover'); - await discover.selectTextBaseLang(); - await discover.waitUntilSearchingHasFinished(); - await monacoEditor.setCodeEditorValue('from my-example-* | sort @timestamp desc'); - await ebtUIHelper.setOptIn(true); - await testSubjects.click('querySubmitButton'); - await discover.waitUntilSearchingHasFinished(); - - const events = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { - eventTypes: ['performance_metric'], - withTimeoutMs: 500, - }); - - expect(events[events.length - 1].context.discoverProfiles).to.eql([ - 'example-root-profile', - 'default-data-source-profile', - ]); - }); - - it('should set EBT context for telemetry events when example profile and reset', async () => { - await common.navigateToApp('discover'); - await discover.selectTextBaseLang(); - await discover.waitUntilSearchingHasFinished(); - await monacoEditor.setCodeEditorValue('from my-example-logs | sort @timestamp desc'); - await ebtUIHelper.setOptIn(true); - await testSubjects.click('querySubmitButton'); - await discover.waitUntilSearchingHasFinished(); - - const events = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { - eventTypes: ['performance_metric'], - withTimeoutMs: 500, - }); - - expect(events[events.length - 1].context.discoverProfiles).to.eql([ - 'example-root-profile', - 'example-data-source-profile', - ]); - - // should reset the profiles when navigating away from Discover - await testSubjects.click('logo'); - await retry.waitFor('home page to open', async () => { - return (await testSubjects.getVisibleText('euiBreadcrumb')) === 'Home'; - }); - await testSubjects.click('addSampleData'); - - await retry.try(async () => { - const eventsAfter = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { - eventTypes: ['click'], - withTimeoutMs: 500, - }); - - expect(eventsAfter[eventsAfter.length - 1].context.discoverProfiles).to.eql([]); - }); - }); - - it('should not set EBT context for embeddables', async () => { - await dashboard.navigateToApp(); - await dashboard.gotoDashboardLandingPage(); - await dashboard.clickNewDashboard(); - await timePicker.setDefaultAbsoluteRange(); - await ebtUIHelper.setOptIn(true); - await dashboardAddPanel.addSavedSearch('A Saved Search'); - await header.waitUntilLoadingHasFinished(); - await dashboard.waitForRenderComplete(); - const rows = await dataGrid.getDocTableRows(); - expect(rows.length).to.be.above(0); - await testSubjects.click('dashboardEditorMenuButton'); - - const events = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { - eventTypes: ['click'], - withTimeoutMs: 500, - }); - - expect( - events.every((event) => !(event.context.discoverProfiles as string[])?.length) - ).to.be(true); - }); - }); - describe('ES|QL mode', () => { describe('cell renderers', () => { it('should render custom @timestamp but not custom log.level', async () => { diff --git a/test/functional/apps/discover/context_awareness/_telemetry.ts b/test/functional/apps/discover/context_awareness/_telemetry.ts new file mode 100644 index 0000000000000..587de698f9336 --- /dev/null +++ b/test/functional/apps/discover/context_awareness/_telemetry.ts @@ -0,0 +1,326 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common, discover, unifiedFieldList, dashboard, header, timePicker } = getPageObjects([ + 'common', + 'discover', + 'unifiedFieldList', + 'dashboard', + 'header', + 'timePicker', + ]); + const testSubjects = getService('testSubjects'); + const dataGrid = getService('dataGrid'); + const dataViews = getService('dataViews'); + const monacoEditor = getService('monacoEditor'); + const ebtUIHelper = getService('kibana_ebt_ui'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const dashboardAddPanel = getService('dashboardAddPanel'); + + describe('telemetry', () => { + describe('context', () => { + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + it('should set EBT context for telemetry events with default profile', async () => { + await common.navigateToApp('discover'); + await discover.selectTextBaseLang(); + await discover.waitUntilSearchingHasFinished(); + await monacoEditor.setCodeEditorValue('from my-example-* | sort @timestamp desc'); + await ebtUIHelper.setOptIn(true); + await testSubjects.click('querySubmitButton'); + await discover.waitUntilSearchingHasFinished(); + + const events = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['performance_metric'], + withTimeoutMs: 500, + }); + + expect(events[events.length - 1].context.discoverProfiles).to.eql([ + 'example-root-profile', + 'default-data-source-profile', + ]); + }); + + it('should set EBT context for telemetry events when example profile and reset', async () => { + await common.navigateToApp('discover'); + await discover.selectTextBaseLang(); + await discover.waitUntilSearchingHasFinished(); + await monacoEditor.setCodeEditorValue('from my-example-logs | sort @timestamp desc'); + await ebtUIHelper.setOptIn(true); + await testSubjects.click('querySubmitButton'); + await discover.waitUntilSearchingHasFinished(); + + const events = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['performance_metric'], + withTimeoutMs: 500, + }); + + expect(events[events.length - 1].context.discoverProfiles).to.eql([ + 'example-root-profile', + 'example-data-source-profile', + ]); + + // should reset the profiles when navigating away from Discover + await testSubjects.click('logo'); + await retry.waitFor('home page to open', async () => { + return (await testSubjects.getVisibleText('euiBreadcrumb')) === 'Home'; + }); + await testSubjects.click('addSampleData'); + + await retry.try(async () => { + const eventsAfter = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['click'], + withTimeoutMs: 500, + }); + + expect(eventsAfter[eventsAfter.length - 1].context.discoverProfiles).to.eql([]); + }); + }); + + it('should not set EBT context for embeddables', async () => { + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await timePicker.setDefaultAbsoluteRange(); + await ebtUIHelper.setOptIn(true); + await dashboardAddPanel.addSavedSearch('A Saved Search'); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be.above(0); + await testSubjects.click('dashboardEditorMenuButton'); + + const events = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['click'], + withTimeoutMs: 500, + }); + + expect( + events.length > 0 && + events.every((event) => !(event.context.discoverProfiles as string[])?.length) + ).to.be(true); + }); + }); + + describe('events', () => { + beforeEach(async () => { + await common.navigateToApp('discover'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + }); + + it('should track field usage when a field is added to the table', async () => { + await dataViews.switchToAndValidate('my-example-*'); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + await ebtUIHelper.setOptIn(true); + await unifiedFieldList.clickFieldListItemAdd('service.name'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + + const [event] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event.properties).to.eql({ + eventName: 'dataTableSelection', + fieldName: 'service.name', + }); + + await unifiedFieldList.clickFieldListItemAdd('_score'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + + const [_, event2] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event2.properties).to.eql({ + eventName: 'dataTableSelection', + }); + }); + + it('should track field usage when a field is removed from the table', async () => { + await dataViews.switchToAndValidate('my-example-logs'); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + await ebtUIHelper.setOptIn(true); + await unifiedFieldList.clickFieldListItemRemove('log.level'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + + const [event] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event.properties).to.eql({ + eventName: 'dataTableRemoval', + fieldName: 'log.level', + }); + }); + + it('should track field usage when a filter is added', async () => { + await dataViews.switchToAndValidate('my-example-logs'); + await discover.waitUntilSearchingHasFinished(); + await ebtUIHelper.setOptIn(true); + await dataGrid.clickCellFilterForButtonExcludingControlColumns(0, 0); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + + const [event] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event.properties).to.eql({ + eventName: 'filterAddition', + fieldName: '@timestamp', + filterOperation: '+', + }); + + await unifiedFieldList.clickFieldListExistsFilter('log.level'); + + const [_, event2] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event2.properties).to.eql({ + eventName: 'filterAddition', + fieldName: 'log.level', + filterOperation: '_exists_', + }); + }); + + it('should track field usage for doc viewer too', async () => { + await dataViews.switchToAndValidate('my-example-logs'); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + await ebtUIHelper.setOptIn(true); + + await dataGrid.clickRowToggle(); + await discover.isShowingDocViewer(); + + // event 1 + await dataGrid.clickFieldActionInFlyout('service.name', 'toggleColumnButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + // event 2 + await dataGrid.clickFieldActionInFlyout('log.level', 'toggleColumnButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + // event 3 + await dataGrid.clickFieldActionInFlyout('log.level', 'addFilterOutValueButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const [event1, event2, event3] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event1.properties).to.eql({ + eventName: 'dataTableSelection', + fieldName: 'service.name', + }); + + expect(event2.properties).to.eql({ + eventName: 'dataTableRemoval', + fieldName: 'log.level', + }); + + expect(event3.properties).to.eql({ + eventName: 'filterAddition', + fieldName: 'log.level', + filterOperation: '-', + }); + }); + + it('should track field usage on surrounding documents page', async () => { + await dataViews.switchToAndValidate('my-example-logs'); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.waitUntilSidebarHasLoaded(); + + await dataGrid.clickRowToggle({ rowIndex: 1 }); + await discover.isShowingDocViewer(); + + const [, surroundingActionEl] = await dataGrid.getRowActions(); + await surroundingActionEl.click(); + await header.waitUntilLoadingHasFinished(); + await ebtUIHelper.setOptIn(true); + + await dataGrid.clickRowToggle({ rowIndex: 0 }); + await discover.isShowingDocViewer(); + + // event 1 + await dataGrid.clickFieldActionInFlyout('service.name', 'toggleColumnButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + // event 2 + await dataGrid.clickFieldActionInFlyout('log.level', 'toggleColumnButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + // event 3 + await dataGrid.clickFieldActionInFlyout('log.level', 'addFilterOutValueButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const [event1, event2, event3] = await ebtUIHelper.getEvents(Number.MAX_SAFE_INTEGER, { + eventTypes: ['discover_field_usage'], + withTimeoutMs: 500, + }); + + expect(event1.properties).to.eql({ + eventName: 'dataTableSelection', + fieldName: 'service.name', + }); + + expect(event2.properties).to.eql({ + eventName: 'dataTableRemoval', + fieldName: 'log.level', + }); + + expect(event3.properties).to.eql({ + eventName: 'filterAddition', + fieldName: 'log.level', + filterOperation: '-', + }); + + expect(event3.context.discoverProfiles).to.eql([ + 'example-root-profile', + 'example-data-source-profile', + ]); + }); + }); + }); +} diff --git a/test/functional/apps/discover/context_awareness/index.ts b/test/functional/apps/discover/context_awareness/index.ts index 655f4460883d1..f937f38c741f9 100644 --- a/test/functional/apps/discover/context_awareness/index.ts +++ b/test/functional/apps/discover/context_awareness/index.ts @@ -38,6 +38,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./_root_profile')); loadTestFile(require.resolve('./_data_source_profile')); + loadTestFile(require.resolve('./_telemetry')); loadTestFile(require.resolve('./extensions/_get_row_indicator_provider')); loadTestFile(require.resolve('./extensions/_get_row_additional_leading_controls')); loadTestFile(require.resolve('./extensions/_get_doc_viewer'));