[OneDiscover] Add EBT event to track field usage (#193996)

- Closes https://github.com/elastic/kibana/issues/186156
- Closes https://github.com/elastic/kibana/issues/189454

## Summary

This PR adds new EBT event type `discover_field_usage` which we use for
tracking adding and removing grid columns and adding filters via
+/-/exists buttons. Properties of the added events consist of:

`eventType`: `dataTableSelection`, `dataTableRemoval`, or
`filterAddition`
`fieldName`: name of the field if it's from ECS schema
`filterOperation`: `+`, `-`, or `_exists_`


<img width="1002" alt="Screenshot 2024-09-25 at 17 51 27"
src="https://github.com/user-attachments/assets/b3f3fb69-55e1-43b2-9683-a6d8884f56fe">

## Testing

Enable "Usage collection" global setting.

Navigate to Discover and observe `kibana-browser` requests in Network
tab.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Julia Rechkunova 2024-09-30 18:20:21 +02:00 committed by GitHub
parent 5def848d2c
commit 7aa64b6ed5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 946 additions and 337 deletions

View file

@ -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;
}

View file

@ -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;

View file

@ -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}

View file

@ -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);

View file

@ -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<BehaviorSubject<SidebarToggleState>>(
() => new BehaviorSubject<SidebarToggleState>({ isCollapsed: false, toggle: () => {} })
@ -396,10 +424,10 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
sidebarPanel={
<SidebarMemoized
documents$={stateContainer.dataState.data$.documents$}
onAddField={onAddColumn}
onAddField={onAddColumnWithTracking}
onRemoveField={onRemoveColumnWithTracking}
columns={currentColumns}
onAddFilter={onFilter}
onRemoveField={onRemoveColumn}
onChangeDataView={stateContainer.actions.onChangeDataView}
selectedDataView={dataView}
trackUiMetric={trackUiMetric}

View file

@ -64,7 +64,7 @@ import type { DiscoverContextAppLocator } from './application/context/services/l
import type { DiscoverSingleDocLocator } from './application/doc/locator';
import type { DiscoverAppLocator } from '../common';
import type { ProfilesManager } from './context_awareness';
import type { DiscoverEBTContextManager } from './services/discover_ebt_context_manager';
import type { DiscoverEBTManager } from './services/discover_ebt_manager';
/**
* Location state of internal Discover history instance
@ -132,7 +132,7 @@ export interface DiscoverServices {
noDataPage?: NoDataPagePluginStart;
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
profilesManager: ProfilesManager;
ebtContextManager: DiscoverEBTContextManager;
ebtManager: DiscoverEBTManager;
fieldsMetadata?: FieldsMetadataPublicStart;
logsDataAccess?: LogsDataAccessPluginStart;
}
@ -149,7 +149,7 @@ export const buildServices = memoize(
scopedHistory,
urlTracker,
profilesManager,
ebtContextManager,
ebtManager,
setHeaderActionMenu = noop,
}: {
core: CoreStart;
@ -162,7 +162,7 @@ export const buildServices = memoize(
scopedHistory?: ScopedHistory;
urlTracker: UrlTracker;
profilesManager: ProfilesManager;
ebtContextManager: DiscoverEBTContextManager;
ebtManager: DiscoverEBTManager;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
}): DiscoverServices => {
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,
};

View file

@ -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,
};
};

View file

@ -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',
]);

View file

@ -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<ContextWithProfileId<RootContext>>;
private readonly dataSourceContext$: BehaviorSubject<ContextWithProfileId<DataSourceContext>>;
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);
}
}

View file

@ -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<DiscoverStartPlugins>, 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<SavedSearchAttributes>({
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'),
]);

View file

@ -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);
});
});
});

View file

@ -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<DiscoverEBTContextProps>;
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<DiscoverEBTContextProps>({
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;
}
}

View file

@ -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_',
});
});
});
});

View file

@ -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<DiscoverEBTContextProps>;
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<DiscoverEBTContextProps>({
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,
});
}
}

View file

@ -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 () => {

View file

@ -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',
]);
});
});
});
}

View file

@ -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'));