[Discover] Support EBT tracking across tabs (#224508)

## Summary

This PR splits out a separate class from `DiscoverEBTManager` called
`ScopedDiscoverEBTManager`, similar to #216488, in order to better
support EBT tracking across tabs.

The profiles tracking in EBT events is a bit convoluted, and ideally
we'd be able to fully isolate the scoped managers, but our use of the
global EBT context observable makes that infeasible since it's a
singleton. If we simply updated the profiles in the EBT context when
switching tabs, it could result in the wrong profiles being tracked for
events fired asynchronously, e.g.:
- Starting from tab A, create a new tab B.
- Switch to tab B (which updates the EBT context with tab B's profiles)
and trigger a long running search.
- While the search is still running, switch back to tab A (updating the
EBT context back to tab A's profiles).
- Tab B's search completes while tab A is active, and the EBT context
for tab B's `discoverFetchAll` event incorrectly contains tab A's
profiles, since they were set when switching back to tab A.

This is solved by keeping track of the active scoped manager in the root
EBT manager, and temporarily updating the EBT context profiles when
firing events from inactive tabs, which seems to be reliable to prevent
leaking across tabs from my testing.

Since I'm using the same "scoped" service approach used for context
awareness across tabs, I've removed the dedicated
`ScopedProfilesManagerProvider` and replaced it with a general purpose
`ScopedServicesProvider` that can be used for all of these types of
services.

Unfortunately while Git recognized that certain files were just moved
and modified (e.g. `discover_ebt_manager.test.ts`), GitHub is displaying
them as entirely new files. To make it easier to review the actual file
changes, open the "Changes from X commits" dropdown and select from the
first commit to "Update unit tests", which will correctly display the
changes before the files were moved (they weren't modified after this
commit).

Resolves #223943.

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Davis McPhee 2025-06-25 20:28:04 -03:00 committed by GitHub
parent 998a1a2a8e
commit 8d605fd48e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1232 additions and 668 deletions

View file

@ -30,8 +30,7 @@
"expressions",
"unifiedDocViewer",
"unifiedSearch",
"contentManagement",
"discoverShared"
"contentManagement"
],
"optionalPlugins": [
"dataVisualizer",

View file

@ -46,7 +46,7 @@ import type { SearchSourceDependencies } from '@kbn/data-plugin/common';
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { createElement } from 'react';
import { createContextAwarenessMocks } from '../context_awareness/__mocks__';
import { DiscoverEBTManager } from '../plugin_imports/discover_ebt_manager';
import { DiscoverEBTManager } from '../ebt_manager';
import { discoverSharedPluginMock } from '@kbn/discover-shared-plugin/public/mocks';
import { createUrlTrackerMock } from './url_tracker.mock';

View file

@ -20,13 +20,15 @@ import {
type DiscoverCustomizationService,
} from '../customizations';
import { DiscoverMainProvider } from '../application/main/state_management/discover_state_provider';
import { type ScopedProfilesManager, ScopedProfilesManagerProvider } from '../context_awareness';
import { type ScopedProfilesManager } from '../context_awareness';
import type { DiscoverServices } from '../build_services';
import { createDiscoverServicesMock } from './services';
import type { DiscoverStateContainer } from '../application/main/state_management/discover_state';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { ChartPortalsRenderer } from '../application/main/components/chart';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ScopedDiscoverEBTManager } from '../ebt_manager';
import { ScopedServicesProvider } from '../components/scoped_services_provider';
export const DiscoverTestProvider = ({
services: originalServices,
@ -34,6 +36,7 @@ export const DiscoverTestProvider = ({
customizationService,
runtimeState,
scopedProfilesManager: originalScopedProfilesManager,
scopedEbtManager: originalScopedEbtManager,
currentTabId: originalCurrentTabId,
usePortalsRenderer,
children,
@ -42,6 +45,7 @@ export const DiscoverTestProvider = ({
stateContainer?: DiscoverStateContainer;
customizationService?: DiscoverCustomizationService;
scopedProfilesManager?: ScopedProfilesManager;
scopedEbtManager?: ScopedDiscoverEBTManager;
runtimeState?: CombinedRuntimeState;
currentTabId?: string;
usePortalsRenderer?: boolean;
@ -51,16 +55,25 @@ export const DiscoverTestProvider = ({
() => originalServices ?? createDiscoverServicesMock(),
[originalServices]
);
const scopedEbtManager = useMemo(
() => originalScopedEbtManager ?? services.ebtManager.createScopedEBTManager(),
[originalScopedEbtManager, services.ebtManager]
);
const scopedProfilesManager = useMemo(
() => originalScopedProfilesManager ?? services.profilesManager.createScopedProfilesManager(),
[originalScopedProfilesManager, services.profilesManager]
() =>
originalScopedProfilesManager ??
services.profilesManager.createScopedProfilesManager({ scopedEbtManager }),
[originalScopedProfilesManager, scopedEbtManager, services.profilesManager]
);
const currentTabId = originalCurrentTabId ?? stateContainer?.getCurrentTab().id;
children = (
<ScopedProfilesManagerProvider scopedProfilesManager={scopedProfilesManager}>
<ScopedServicesProvider
scopedProfilesManager={scopedProfilesManager}
scopedEBTManager={scopedEbtManager}
>
{children}
</ScopedProfilesManagerProvider>
</ScopedServicesProvider>
);
if (runtimeState) {

View file

@ -24,7 +24,6 @@ import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { generateFilters } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import type { UseColumnsProps } from '@kbn/unified-data-table';
import { popularizeField, useColumns } from '@kbn/unified-data-table';
@ -41,6 +40,7 @@ import { ContextAppContent } from './context_app_content';
import { SurrDocType } from './services/context';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { setBreadcrumbs } from '../../utils/breadcrumbs';
import { useScopedServices } from '../../components/scoped_services_provider';
const ContextAppContentMemoized = memo(ContextAppContent);
@ -54,8 +54,8 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
const styles = useMemoCss(componentStyles);
const services = useDiscoverServices();
const { scopedEBTManager } = useScopedServices();
const {
analytics,
locator,
uiSettings,
capabilities,
@ -63,7 +63,6 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
navigation,
filterManager,
core,
ebtManager,
fieldsMetadata,
} = services;
@ -136,7 +135,10 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
*/
useEffect(() => {
const doFetch = async () => {
const startTime = window.performance.now();
const surroundingDocsFetchTracker = scopedEBTManager.trackPerformanceEvent(
'discoverSurroundingDocsFetch'
);
let fetchType = '';
if (!prevAppState.current) {
fetchType = 'all';
@ -155,13 +157,8 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
await fetchContextRows();
}
if (analytics && fetchType) {
const fetchDuration = window.performance.now() - startTime;
reportPerformanceMetricEvent(analytics, {
eventName: 'discoverSurroundingDocsFetch',
duration: fetchDuration,
meta: { fetchType },
});
if (fetchType) {
surroundingDocsFetchTracker.reportEvent({ meta: { fetchType } });
}
};
@ -170,7 +167,6 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
prevAppState.current = cloneDeep(appState);
prevGlobalState.current = cloneDeep(globalState);
}, [
analytics,
appState,
globalState,
anchorId,
@ -178,6 +174,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
fetchAllRows,
fetchSurroundingRows,
fetchedState.anchor.id,
scopedEBTManager,
]);
const rows = useMemo(
@ -209,30 +206,30 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
if (dataViews) {
const fieldName = typeof field === 'string' ? field : field.name;
await popularizeField(dataView, fieldName, dataViews, capabilities);
void ebtManager.trackFilterAddition({
void scopedEBTManager.trackFilterAddition({
fieldName: fieldName === '_exists_' ? String(values) : fieldName,
filterOperation: fieldName === '_exists_' ? '_exists_' : operation,
fieldsMetadata,
});
}
},
[filterManager, dataViews, dataView, capabilities, ebtManager, fieldsMetadata]
[filterManager, dataView, dataViews, capabilities, scopedEBTManager, fieldsMetadata]
);
const onAddColumnWithTracking = useCallback(
(columnName: string) => {
onAddColumn(columnName);
void ebtManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata });
void scopedEBTManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata });
},
[onAddColumn, ebtManager, fieldsMetadata]
[onAddColumn, scopedEBTManager, fieldsMetadata]
);
const onRemoveColumnWithTracking = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
void ebtManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata });
void scopedEBTManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata });
},
[onRemoveColumn, ebtManager, fieldsMetadata]
[onRemoveColumn, scopedEBTManager, fieldsMetadata]
);
const TopNavMenu = navigation.ui.AggregateQueryTopNavMenu;

View file

@ -16,7 +16,8 @@ import { LoadingIndicator } from '../../components/common/loading_indicator';
import { useDataView } from '../../hooks/use_data_view';
import type { ContextHistoryLocationState } from './services/locator';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { ScopedProfilesManagerProvider, useRootProfile } from '../../context_awareness';
import { useRootProfile } from '../../context_awareness';
import { ScopedServicesProvider } from '../../components/scoped_services_provider';
export interface ContextUrlParams {
dataViewId: string;
@ -24,7 +25,7 @@ export interface ContextUrlParams {
}
export function ContextAppRoute() {
const { profilesManager, getScopedHistory } = useDiscoverServices();
const { profilesManager, ebtManager, getScopedHistory } = useDiscoverServices();
const scopedHistory = getScopedHistory<ContextHistoryLocationState>();
const locationState = useMemo(
() => scopedHistory?.location.state as ContextHistoryLocationState | undefined,
@ -50,7 +51,10 @@ export function ContextAppRoute() {
const dataViewId = decodeURIComponent(encodedDataViewId);
const anchorId = decodeURIComponent(id);
const { dataView, error } = useDataView({ index: locationState?.dataViewSpec || dataViewId });
const [scopedProfilesManager] = useState(() => profilesManager.createScopedProfilesManager());
const [scopedEbtManager] = useState(() => ebtManager.createScopedEBTManager());
const [scopedProfilesManager] = useState(() =>
profilesManager.createScopedProfilesManager({ scopedEbtManager })
);
const rootProfileState = useRootProfile();
if (error) {
@ -80,10 +84,13 @@ export function ContextAppRoute() {
}
return (
<ScopedProfilesManagerProvider scopedProfilesManager={scopedProfilesManager}>
<ScopedServicesProvider
scopedProfilesManager={scopedProfilesManager}
scopedEBTManager={scopedEbtManager}
>
<rootProfileState.AppWrapper>
<ContextApp anchorId={anchorId} dataView={dataView} referrer={locationState?.referrer} />
</rootProfileState.AppWrapper>
</ScopedProfilesManagerProvider>
</ScopedServicesProvider>
);
}

View file

@ -28,7 +28,7 @@ import {
getTieBreakerFieldName,
getEsQuerySort,
} from '../../../../common/utils/sorting/get_es_query_sort';
import { useScopedProfilesManager } from '../../../context_awareness';
import { useScopedServices } from '../../../components/scoped_services_provider';
const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({
[statusKey]: { value: LoadingStatus.FAILED, error, reason },
@ -41,7 +41,7 @@ export interface ContextAppFetchProps {
}
export function useContextAppFetch({ anchorId, dataView, appState }: ContextAppFetchProps) {
const scopedProfilesManager = useScopedProfilesManager();
const { scopedProfilesManager } = useScopedServices();
const services = useDiscoverServices();
const { uiSettings: config, data, toastNotifications, filterManager } = services;

View file

@ -34,7 +34,9 @@ describe('context app', function () {
searchSourceStub,
[{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }],
discoverServices,
discoverServices.profilesManager.createScopedProfilesManager()
discoverServices.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServices.ebtManager.createScopedEBTManager(),
})
);
describe('function fetchAnchor', function () {

View file

@ -88,7 +88,9 @@ describe('context predecessors', function () {
[],
dataPluginMock,
discoverServiceMock,
discoverServiceMock.profilesManager.createScopedProfilesManager()
discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
})
);
};
});
@ -239,7 +241,9 @@ describe('context predecessors', function () {
[],
dataPluginMock,
discoverServiceMock,
discoverServiceMock.profilesManager.createScopedProfilesManager()
discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
})
);
};
});

View file

@ -88,7 +88,9 @@ describe('context successors', function () {
[],
dataPluginMock,
discoverServiceMock,
discoverServiceMock.profilesManager.createScopedProfilesManager()
discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
})
);
};
});
@ -241,7 +243,9 @@ describe('context successors', function () {
[],
dataPluginMock,
discoverServiceMock,
discoverServiceMock.profilesManager.createScopedProfilesManager()
discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
})
);
};
});
@ -316,7 +320,9 @@ describe('context successors', function () {
...discoverServiceMock,
data: dataPluginMock,
},
discoverServiceMock.profilesManager.createScopedProfilesManager()
discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
})
);
};
});

View file

@ -19,7 +19,7 @@ import { setBreadcrumbs } from '../../../utils/breadcrumbs';
import { useDiscoverServices } from '../../../hooks/use_discover_services';
import { SingleDocViewer } from './single_doc_viewer';
import { createDataViewDataSource } from '../../../../common/data_sources';
import { useScopedProfilesManager } from '../../../context_awareness';
import { useScopedServices } from '../../../components/scoped_services_provider';
export interface DocProps extends EsDocSearchProps {
/**
@ -30,7 +30,7 @@ export interface DocProps extends EsDocSearchProps {
export function Doc(props: DocProps) {
const { dataView } = props;
const scopedProfilesManager = useScopedProfilesManager();
const { scopedProfilesManager } = useScopedServices();
const services = useDiscoverServices();
const { locator, chrome, docLinks } = services;
const indexExistsLink = docLinks.links.apis.indexExists;

View file

@ -19,7 +19,8 @@ import { useDiscoverServices } from '../../hooks/use_discover_services';
import { DiscoverError } from '../../components/common/error_alert';
import { useDataView } from '../../hooks/use_data_view';
import type { DocHistoryLocationState } from './locator';
import { ScopedProfilesManagerProvider, useRootProfile } from '../../context_awareness';
import { useRootProfile } from '../../context_awareness';
import { ScopedServicesProvider } from '../../components/scoped_services_provider';
export interface DocUrlParams {
dataViewId: string;
@ -27,7 +28,7 @@ export interface DocUrlParams {
}
export const SingleDocRoute = () => {
const { timefilter, core, profilesManager, getScopedHistory } = useDiscoverServices();
const { timefilter, core, profilesManager, ebtManager, getScopedHistory } = useDiscoverServices();
const { search } = useLocation();
const { dataViewId, index } = useParams<DocUrlParams>();
@ -53,7 +54,10 @@ export const SingleDocRoute = () => {
const { dataView, error } = useDataView({
index: locationState?.dataViewSpec || decodeURIComponent(dataViewId),
});
const [scopedProfilesManager] = useState(() => profilesManager.createScopedProfilesManager());
const [scopedEbtManager] = useState(() => ebtManager.createScopedEBTManager());
const [scopedProfilesManager] = useState(() =>
profilesManager.createScopedProfilesManager({ scopedEbtManager })
);
const rootProfileState = useRootProfile();
if (error) {
@ -98,10 +102,13 @@ export const SingleDocRoute = () => {
}
return (
<ScopedProfilesManagerProvider scopedProfilesManager={scopedProfilesManager}>
<ScopedServicesProvider
scopedProfilesManager={scopedProfilesManager}
scopedEBTManager={scopedEbtManager}
>
<rootProfileState.AppWrapper>
<Doc id={id} index={index} dataView={dataView} referrer={locationState?.referrer} />
</rootProfileState.AppWrapper>
</ScopedProfilesManagerProvider>
</ScopedServicesProvider>
);
};

View file

@ -25,7 +25,7 @@ import { DiscoverMainProvider } from '../../state_management/discover_state_prov
import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useDiscoverHistogram } from './use_discover_histogram';
import { ScopedProfilesManagerProvider } from '../../../../context_awareness';
import { ScopedServicesProvider } from '../../../../components/scoped_services_provider';
export type ChartPortalNode = HtmlPortalNode;
export type ChartPortalNodes = Record<string, ChartPortalNode>;
@ -88,6 +88,7 @@ const UnifiedHistogramGuard = ({
const currentScopedProfilesManager = useRuntimeState(
currentTabRuntimeState.scopedProfilesManager$
);
const currentScopedEbtManager = useRuntimeState(currentTabRuntimeState.scopedEbtManager$);
const currentDataView = useRuntimeState(currentTabRuntimeState.currentDataView$);
const adHocDataViews = useRuntimeState(runtimeStateManager.adHocDataViews$);
const isInitialized = useRef(false);
@ -108,12 +109,15 @@ const UnifiedHistogramGuard = ({
<DiscoverCustomizationProvider value={currentCustomizationService}>
<DiscoverMainProvider value={currentStateContainer}>
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
<ScopedProfilesManagerProvider scopedProfilesManager={currentScopedProfilesManager}>
<ScopedServicesProvider
scopedProfilesManager={currentScopedProfilesManager}
scopedEBTManager={currentScopedEbtManager}
>
<UnifiedHistogramChartWrapper
stateContainer={currentStateContainer}
panelsToggle={panelsToggle}
/>
</ScopedProfilesManagerProvider>
</ScopedServicesProvider>
</RuntimeStateProvider>
</DiscoverMainProvider>
</DiscoverCustomizationProvider>

View file

@ -77,13 +77,15 @@ async function mountComponent(
};
profilesManager = profilesManager ?? services.profilesManager;
const scopedEbtManager = services.ebtManager.createScopedEBTManager();
const component = mountWithIntl(
<DiscoverTestProvider
services={{ ...services, profilesManager }}
stateContainer={stateContainer}
customizationService={customisationService}
scopedProfilesManager={profilesManager.createScopedProfilesManager()}
scopedProfilesManager={profilesManager.createScopedProfilesManager({ scopedEbtManager })}
scopedEbtManager={scopedEbtManager}
>
<DiscoverDocuments {...props} />
</DiscoverTestProvider>

View file

@ -83,6 +83,7 @@ import {
useInternalStateDispatch,
useInternalStateSelector,
} from '../../state_management/redux';
import { useScopedServices } from '../../../../components/scoped_services_provider';
const DiscoverGridMemoized = React.memo(DiscoverGrid);
@ -110,10 +111,11 @@ function DiscoverDocumentsComponent({
onFieldEdited?: () => void;
}) {
const services = useDiscoverServices();
const { scopedEBTManager } = useScopedServices();
const dispatch = useInternalStateDispatch();
const documents$ = stateContainer.dataState.data$.documents$;
const savedSearch = useSavedSearchInitial();
const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services;
const { dataViews, capabilities, uiSettings, uiActions, fieldsMetadata } = services;
const requestParams = useCurrentTabSelector((state) => state.dataRequestParams);
const [
dataSource,
@ -193,17 +195,17 @@ function DiscoverDocumentsComponent({
const onAddColumnWithTracking = useCallback(
(columnName: string) => {
onAddColumn(columnName);
void ebtManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata });
void scopedEBTManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata });
},
[onAddColumn, ebtManager, fieldsMetadata]
[onAddColumn, scopedEBTManager, fieldsMetadata]
);
const onRemoveColumnWithTracking = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
void ebtManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata });
void scopedEBTManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata });
},
[onRemoveColumn, ebtManager, fieldsMetadata]
[onRemoveColumn, scopedEBTManager, fieldsMetadata]
);
const docViewerRef = useRef<DocViewerApi>(null);

View file

@ -62,6 +62,7 @@ import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useCurrentDataView, useCurrentTabSelector } from '../../state_management/redux';
import { TABS_ENABLED } from '../../../../constants';
import { DiscoverHistogramLayout } from './discover_histogram_layout';
import { useScopedServices } from '../../../../components/scoped_services_provider';
const queryClient = new QueryClient();
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
@ -89,9 +90,9 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
spaces,
observabilityAIAssistant,
dataVisualizer: dataVisualizerService,
ebtManager,
fieldsMetadata,
} = useDiscoverServices();
const { scopedEBTManager } = useScopedServices();
const styles = useMemoCss(componentStyles);
const globalQueryState = data.query.getState();
@ -162,17 +163,17 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
const onAddColumnWithTracking = useCallback(
(columnName: string) => {
onAddColumn(columnName);
void ebtManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata });
void scopedEBTManager.trackDataTableSelection({ fieldName: columnName, fieldsMetadata });
},
[onAddColumn, ebtManager, fieldsMetadata]
[onAddColumn, scopedEBTManager, fieldsMetadata]
);
const onRemoveColumnWithTracking = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
void ebtManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata });
void scopedEBTManager.trackDataTableRemoval({ fieldName: columnName, fieldsMetadata });
},
[onRemoveColumn, ebtManager, fieldsMetadata]
[onRemoveColumn, scopedEBTManager, fieldsMetadata]
);
// The assistant is getting the state from the url correctly
@ -196,14 +197,22 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'filter_added');
}
void ebtManager.trackFilterAddition({
void scopedEBTManager.trackFilterAddition({
fieldName: fieldName === '_exists_' ? String(values) : fieldName,
filterOperation: fieldName === '_exists_' ? '_exists_' : operation,
fieldsMetadata,
});
return filterManager.addFilters(newFilters);
},
[filterManager, dataView, dataViews, trackUiMetric, capabilities, ebtManager, fieldsMetadata]
[
dataView,
dataViews,
capabilities,
filterManager,
trackUiMetric,
scopedEBTManager,
fieldsMetadata,
]
);
const onPopulateWhereClause = useCallback<DocViewFilterFn>(
@ -233,13 +242,13 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'esql_filter_added');
}
void ebtManager.trackFilterAddition({
void scopedEBTManager.trackFilterAddition({
fieldName: fieldName === '_exists_' ? String(values) : fieldName,
filterOperation: fieldName === '_exists_' ? '_exists_' : operation,
fieldsMetadata,
});
},
[data.query.queryString, query, trackUiMetric, ebtManager, fieldsMetadata]
[query, data.query.queryString, trackUiMetric, scopedEBTManager, fieldsMetadata]
);
const onFilter = isEsqlMode ? onPopulateWhereClause : onAddFilter;

View file

@ -49,7 +49,7 @@ import { BrandedLoadingIndicator } from './branded_loading_indicator';
import { RedirectWhenSavedObjectNotFound } from './redirect_not_found';
import { DiscoverMainApp } from './main_app';
import { useAsyncFunction } from '../../hooks/use_async_function';
import { ScopedProfilesManagerProvider } from '../../../../context_awareness';
import { ScopedServicesProvider } from '../../../../components/scoped_services_provider';
export interface DiscoverSessionViewProps {
customizationContext: DiscoverCustomizationContext;
@ -139,6 +139,10 @@ export const DiscoverSessionView = ({
runtimeStateManager,
(tab) => tab.scopedProfilesManager$
);
const scopedEbtManager = useCurrentTabRuntimeState(
runtimeStateManager,
(tab) => tab.scopedEbtManager$
);
const currentDataView = useCurrentTabRuntimeState(
runtimeStateManager,
(tab) => tab.currentDataView$
@ -214,12 +218,7 @@ export const DiscoverSessionView = ({
);
}
if (
!currentStateContainer ||
!currentCustomizationService ||
!scopedProfilesManager ||
!currentDataView
) {
if (!currentStateContainer || !currentCustomizationService || !currentDataView) {
return <BrandedLoadingIndicator />;
}
@ -227,9 +226,12 @@ export const DiscoverSessionView = ({
<DiscoverCustomizationProvider value={currentCustomizationService}>
<DiscoverMainProvider value={currentStateContainer}>
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
<ScopedProfilesManagerProvider scopedProfilesManager={scopedProfilesManager}>
<ScopedServicesProvider
scopedProfilesManager={scopedProfilesManager}
scopedEBTManager={scopedEbtManager}
>
<DiscoverMainApp stateContainer={currentStateContainer} />
</ScopedProfilesManagerProvider>
</ScopedServicesProvider>
</RuntimeStateProvider>
</DiscoverMainProvider>
</DiscoverCustomizationProvider>

View file

@ -69,7 +69,7 @@ describe('test fetchAll', () => {
const { appState, internalState, runtimeStateManager, getCurrentTab } = getDiscoverStateMock(
{}
);
const { scopedProfilesManager$ } = selectTabRuntimeState(
const { scopedProfilesManager$, scopedEbtManager$ } = selectTabRuntimeState(
runtimeStateManager,
getCurrentTab().id
);
@ -81,6 +81,7 @@ describe('test fetchAll', () => {
appStateContainer: appState,
internalState,
scopedProfilesManager: scopedProfilesManager$.getValue(),
scopedEbtManager: scopedEbtManager$.getValue(),
searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED,
savedSearch: {

View file

@ -11,7 +11,6 @@ import type { Adapters } from '@kbn/inspector-plugin/common';
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
import type { BehaviorSubject } from 'rxjs';
import { combineLatest, distinctUntilChanged, filter, firstValueFrom, race, switchMap } from 'rxjs';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { isEqual } from 'lodash';
import { isOfAggregateQueryType } from '@kbn/es-query';
import type { DiscoverAppStateContainer } from '../state_management/discover_app_state_container';
@ -37,6 +36,7 @@ import type { DiscoverServices } from '../../../build_services';
import { fetchEsql } from './fetch_esql';
import type { InternalStateStore, TabState } from '../state_management/redux';
import type { ScopedProfilesManager } from '../../../context_awareness';
import type { ScopedDiscoverEBTManager } from '../../../ebt_manager';
export interface CommonFetchParams {
dataSubjects: SavedSearchData;
@ -49,6 +49,7 @@ export interface CommonFetchParams {
searchSessionId: string;
services: DiscoverServices;
scopedProfilesManager: ScopedProfilesManager;
scopedEbtManager: ScopedDiscoverEBTManager;
}
/**
@ -72,6 +73,7 @@ export function fetchAll(
appStateContainer,
services,
scopedProfilesManager,
scopedEbtManager,
inspectorAdapters,
savedSearch,
abortController,
@ -123,19 +125,14 @@ export function fetchAll(
})
: fetchDocuments(searchSource, params);
const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';
const startTime = window.performance.now();
const fetchAllRequestOnlyTracker = scopedEbtManager.trackPerformanceEvent(
'discoverFetchAllRequestsOnly'
);
// Handle results of the individual queries and forward the results to the corresponding dataSubjects
response
.then(({ records, esqlQueryColumns, interceptedWarnings = [], esqlHeaderWarning }) => {
if (services.analytics) {
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(services.analytics, {
eventName: 'discoverFetchAllRequestsOnly',
duration,
meta: { fetchType },
});
}
fetchAllRequestOnlyTracker.reportEvent({ meta: { fetchType } });
if (isEsqlQuery) {
const fetchStatus =

View file

@ -25,7 +25,10 @@ import { selectTabRuntimeState } from '../state_management/redux';
const getDeps = (): CommonFetchParams => {
const { appState, internalState, dataState, runtimeStateManager, getCurrentTab } =
getDiscoverStateMock({});
const { scopedProfilesManager$ } = selectTabRuntimeState(runtimeStateManager, getCurrentTab().id);
const { scopedProfilesManager$, scopedEbtManager$ } = selectTabRuntimeState(
runtimeStateManager,
getCurrentTab().id
);
appState.update({ sampleSize: 100 });
return {
dataSubjects: dataState.data$,
@ -38,6 +41,7 @@ const getDeps = (): CommonFetchParams => {
internalState,
appStateContainer: appState,
scopedProfilesManager: scopedProfilesManager$.getValue(),
scopedEbtManager: scopedEbtManager$.getValue(),
};
};

View file

@ -21,7 +21,9 @@ describe('fetchEsql', () => {
jest.clearAllMocks();
});
const scopedProfilesManager = discoverServiceMock.profilesManager.createScopedProfilesManager();
const scopedProfilesManager = discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
});
const fetchEsqlMockProps = {
query: { esql: 'from *' },
dataView: dataViewWithTimefieldMock,

View file

@ -25,7 +25,6 @@ import { RequestAdapter } from '@kbn/inspector-plugin/common';
import type { AggregateQuery, Query } from '@kbn/es-query';
import { isOfAggregateQueryType } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type { SearchResponseWarning } from '@kbn/search-response-warnings';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { DEFAULT_COLUMNS_SETTING, SEARCH_ON_PAGE_LOAD_SETTING } from '@kbn/discover-utils';
@ -249,11 +248,10 @@ export function getDataStateContainer({
.pipe(
mergeMap(async ({ options }) => {
const { id: currentTabId, resetDefaultProfileState, dataRequestParams } = getCurrentTab();
const { scopedProfilesManager$, currentDataView$ } = selectTabRuntimeState(
runtimeStateManager,
currentTabId
);
const { scopedProfilesManager$, scopedEbtManager$, currentDataView$ } =
selectTabRuntimeState(runtimeStateManager, currentTabId);
const scopedProfilesManager = scopedProfilesManager$.getValue();
const scopedEbtManager = scopedEbtManager$.getValue();
const searchSessionId =
(options.fetchMore && dataRequestParams.searchSessionId) ||
@ -269,6 +267,7 @@ export function getDataStateContainer({
internalState,
savedSearch: savedSearchContainer.getState(),
scopedProfilesManager,
scopedEbtManager,
};
abortController?.abort();
@ -276,18 +275,14 @@ export function getDataStateContainer({
if (options.fetchMore) {
abortControllerFetchMore = new AbortController();
const fetchMoreStartTime = window.performance.now();
const fetchMoreTracker = scopedEbtManager.trackPerformanceEvent('discoverFetchMore');
await fetchMoreDocuments({
...commonFetchParams,
abortController: abortControllerFetchMore,
});
const fetchMoreDuration = window.performance.now() - fetchMoreStartTime;
reportPerformanceMetricEvent(services.analytics, {
eventName: 'discoverFetchMore',
duration: fetchMoreDuration,
});
fetchMoreTracker.reportEvent();
return;
}
@ -326,7 +321,7 @@ export function getDataStateContainer({
abortController = new AbortController();
const prevAutoRefreshDone = autoRefreshDone;
const fetchAllStartTime = window.performance.now();
const fetchAllTracker = scopedEbtManager.trackPerformanceEvent('discoverFetchAll');
await fetchAll({
...commonFetchParams,
@ -366,11 +361,7 @@ export function getDataStateContainer({
},
});
const fetchAllDuration = window.performance.now() - fetchAllStartTime;
reportPerformanceMetricEvent(services.analytics, {
eventName: 'discoverFetchAll',
duration: fetchAllDuration,
});
fetchAllTracker.reportEvent();
// If the autoRefreshCallback is still the same as when we started i.e. there was no newer call
// replacing this current one, call it to make sure we tell that the auto refresh is done

View file

@ -76,11 +76,13 @@ export const initializeSession: InternalStateThunkActionCreator<
dispatch(clearAllTabs());
}
const discoverSessionLoadTracker =
services.ebtManager.trackPerformanceEvent('discoverLoadSavedSearch');
const { currentDataView$, stateContainer$, customizationService$, scopedProfilesManager$ } =
selectTabRuntimeState(runtimeStateManager, tabId);
const {
currentDataView$,
stateContainer$,
customizationService$,
scopedProfilesManager$,
scopedEbtManager$,
} = selectTabRuntimeState(runtimeStateManager, tabId);
const tabState = selectTab(getState(), tabId);
let urlState = cleanupUrlState(
@ -100,7 +102,12 @@ export const initializeSession: InternalStateThunkActionCreator<
currentDataView$.next(undefined);
stateContainer$.next(undefined);
customizationService$.next(undefined);
scopedProfilesManager$.next(services.profilesManager.createScopedProfilesManager());
scopedEbtManager$.next(services.ebtManager.createScopedEBTManager());
scopedProfilesManager$.next(
services.profilesManager.createScopedProfilesManager({
scopedEbtManager: scopedEbtManager$.getValue(),
})
);
}
if (TABS_ENABLED && !wasTabInitialized) {
@ -124,6 +131,10 @@ export const initializeSession: InternalStateThunkActionCreator<
}
}
const discoverSessionLoadTracker = scopedEbtManager$
.getValue()
.trackPerformanceEvent('discoverLoadSavedSearch');
const persistedDiscoverSession = discoverSessionId
? await services.savedSearch.get(discoverSessionId)
: undefined;

View file

@ -35,7 +35,7 @@ export const setTabs: InternalStateThunkActionCreator<
(
dispatch,
getState,
{ runtimeStateManager, tabsStorageManager, services: { profilesManager } }
{ runtimeStateManager, tabsStorageManager, services: { profilesManager, ebtManager } }
) => {
const previousState = getState();
const previousTabs = selectAllTabs(previousState);
@ -48,7 +48,19 @@ export const setTabs: InternalStateThunkActionCreator<
}
for (const tab of addedTabs) {
runtimeStateManager.tabs.byId[tab.id] = createTabRuntimeState({ profilesManager });
runtimeStateManager.tabs.byId[tab.id] = createTabRuntimeState({
profilesManager,
ebtManager,
});
}
const selectedTabRuntimeState = selectTabRuntimeState(
runtimeStateManager,
params.selectedTabId
);
if (selectedTabRuntimeState) {
selectedTabRuntimeState.scopedEbtManager$.getValue().setAsActiveManager();
}
dispatch(

View file

@ -17,6 +17,7 @@ import type { DiscoverStateContainer } from '../discover_state';
import type { ConnectedCustomizationService } from '../../../../customizations';
import type { ProfilesManager, ScopedProfilesManager } from '../../../../context_awareness';
import type { TabState } from './types';
import type { DiscoverEBTManager, ScopedDiscoverEBTManager } from '../../../../ebt_manager';
interface DiscoverRuntimeState {
adHocDataViews: DataView[];
@ -27,6 +28,7 @@ interface TabRuntimeState {
customizationService?: ConnectedCustomizationService;
unifiedHistogramLayoutProps?: UnifiedHistogramPartialLayoutProps;
scopedProfilesManager: ScopedProfilesManager;
scopedEbtManager: ScopedDiscoverEBTManager;
currentDataView: DataView;
}
@ -49,19 +51,28 @@ export const createRuntimeStateManager = (): RuntimeStateManager => ({
export const createTabRuntimeState = ({
profilesManager,
ebtManager,
}: {
profilesManager: ProfilesManager;
}): ReactiveTabRuntimeState => ({
stateContainer$: new BehaviorSubject<DiscoverStateContainer | undefined>(undefined),
customizationService$: new BehaviorSubject<ConnectedCustomizationService | undefined>(undefined),
unifiedHistogramLayoutProps$: new BehaviorSubject<UnifiedHistogramPartialLayoutProps | undefined>(
undefined
),
scopedProfilesManager$: new BehaviorSubject<ScopedProfilesManager>(
profilesManager.createScopedProfilesManager()
),
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
});
ebtManager: DiscoverEBTManager;
}): ReactiveTabRuntimeState => {
const scopedEbtManager = ebtManager.createScopedEBTManager();
return {
stateContainer$: new BehaviorSubject<DiscoverStateContainer | undefined>(undefined),
customizationService$: new BehaviorSubject<ConnectedCustomizationService | undefined>(
undefined
),
unifiedHistogramLayoutProps$: new BehaviorSubject<
UnifiedHistogramPartialLayoutProps | undefined
>(undefined),
scopedProfilesManager$: new BehaviorSubject(
profilesManager.createScopedProfilesManager({ scopedEbtManager })
),
scopedEbtManager$: new BehaviorSubject(scopedEbtManager),
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
};
};
export const useRuntimeState = <T,>(stateSubject$: BehaviorSubject<T>) =>
useObservable(stateSubject$, stateSubject$.getValue());
@ -105,7 +116,7 @@ export const useCurrentTabRuntimeState = <T,>(
};
export type CombinedRuntimeState = DiscoverRuntimeState &
Omit<TabRuntimeState, 'scopedProfilesManager'>;
Omit<TabRuntimeState, 'scopedProfilesManager' | 'scopedEbtManager'>;
const runtimeStateContext = createContext<CombinedRuntimeState | undefined>(undefined);

View file

@ -17,8 +17,10 @@ const emptyDataView = buildDataViewMock({
name: 'emptyDataView',
fields: fieldList(),
});
const { profilesManagerMock } = createContextAwarenessMocks();
const scopedProfilesManager = profilesManagerMock.createScopedProfilesManager();
const { profilesManagerMock, scopedEbtManagerMock } = createContextAwarenessMocks();
const scopedProfilesManager = profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: scopedEbtManagerMock,
});
scopedProfilesManager.resolveDataSourceProfile({});

View file

@ -69,7 +69,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 { DiscoverEBTManager } from './plugin_imports/discover_ebt_manager';
import type { DiscoverEBTManager } from './ebt_manager';
/**
* Location state of internal Discover history instance

View file

@ -491,7 +491,9 @@ describe('Discover flyout', function () {
_source: { date: '2020-20-01T12:12:12.124', name: 'test2', extension: 'jpg' },
},
];
const scopedProfilesManager = services.profilesManager.createScopedProfilesManager();
const scopedProfilesManager = services.profilesManager.createScopedProfilesManager({
scopedEbtManager: services.ebtManager.createScopedEBTManager(),
});
const records = buildDataTableRecordList({
records: hits as EsHitRecord[],
dataView: dataViewMock,

View file

@ -0,0 +1,10 @@
/*
* 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".
*/
export { ScopedServicesProvider, useScopedServices } from './scoped_services_provider';

View file

@ -0,0 +1,46 @@
/*
* 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 React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react';
import type { ScopedProfilesManager } from '../../context_awareness';
import type { ScopedDiscoverEBTManager } from '../../ebt_manager';
interface ScopedServices {
scopedProfilesManager: ScopedProfilesManager;
scopedEBTManager: ScopedDiscoverEBTManager;
}
const scopedServicesContext = createContext<ScopedServices | undefined>(undefined);
export const ScopedServicesProvider = ({
scopedProfilesManager,
scopedEBTManager,
children,
}: PropsWithChildren<ScopedServices>) => {
const scopedServices = useMemo(
() => ({ scopedProfilesManager, scopedEBTManager }),
[scopedEBTManager, scopedProfilesManager]
);
return (
<scopedServicesContext.Provider value={scopedServices}>
{children}
</scopedServicesContext.Provider>
);
};
export const useScopedServices = () => {
const context = useContext(scopedServicesContext);
if (!context) {
throw new Error('useScopedServices must be used within a ScopedServicesProvider');
}
return context;
};

View file

@ -25,7 +25,7 @@ import {
} from '../profiles';
import type { ProfileProviderServices } from '../profile_providers/profile_provider_services';
import { ProfilesManager } from '../profiles_manager';
import { DiscoverEBTManager } from '../../plugin_imports/discover_ebt_manager';
import { DiscoverEBTManager } from '../../ebt_manager';
import {
createApmErrorsContextServiceMock,
createLogsContextServiceMock,
@ -169,12 +169,11 @@ export const createContextAwarenessMocks = ({
documentProfileServiceMock.registerProvider(documentProfileProviderMock);
}
const ebtManagerMock = new DiscoverEBTManager();
const scopedEbtManagerMock = new DiscoverEBTManager().createScopedEBTManager();
const profilesManagerMock = new ProfilesManager(
rootProfileServiceMock,
dataSourceProfileServiceMock,
documentProfileServiceMock,
ebtManagerMock
documentProfileServiceMock
);
const profileProviderServices = createProfileProviderServicesMock();
@ -190,7 +189,7 @@ export const createContextAwarenessMocks = ({
contextRecordMock2,
profilesManagerMock,
profileProviderServices,
ebtManagerMock,
scopedEbtManagerMock,
};
};

View file

@ -31,11 +31,14 @@ import {
import { createContextAwarenessMocks } from '../__mocks__';
import { type ScopedProfilesManager } from '../profiles_manager';
import { DiscoverTestProvider } from '../../__mocks__/test_provider';
import { v4 as uuidv4 } from 'uuid';
import type { ScopedDiscoverEBTManager } from '../../ebt_manager';
let mockScopedProfilesManager: ScopedProfilesManager;
let mockScopedEbtManager: ScopedDiscoverEBTManager;
let mockUuid = 0;
jest.mock('uuid', () => ({ ...jest.requireActual('uuid'), v4: () => (++mockUuid).toString() }));
jest.mock('uuid', () => ({ ...jest.requireActual('uuid'), v4: jest.fn() }));
const mockActions: Array<ActionDefinition<DiscoverCellActionExecutionContext>> = [];
const mockTriggerActions: Record<string, string[]> = { [DISCOVER_CELL_ACTIONS_TRIGGER.id]: [] };
@ -83,6 +86,7 @@ describe('useAdditionalCellActions', () => {
<DiscoverTestProvider
services={discoverServiceMock}
scopedProfilesManager={mockScopedProfilesManager}
scopedEbtManager={mockScopedEbtManager}
>
{children}
</DiscoverTestProvider>
@ -91,13 +95,19 @@ describe('useAdditionalCellActions', () => {
};
beforeEach(() => {
discoverServiceMock.profilesManager = createContextAwarenessMocks().profilesManagerMock;
mockScopedProfilesManager = discoverServiceMock.profilesManager.createScopedProfilesManager();
(uuidv4 as jest.Mock).mockImplementation(jest.requireActual('uuid').v4);
const { profilesManagerMock, scopedEbtManagerMock } = createContextAwarenessMocks();
discoverServiceMock.profilesManager = profilesManagerMock;
mockScopedEbtManager = scopedEbtManagerMock;
mockScopedProfilesManager = discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: mockScopedEbtManager,
});
(uuidv4 as jest.Mock).mockImplementation(() => (++mockUuid).toString());
});
afterEach(() => {
mockUuid = 0;
jest.clearAllMocks();
mockUuid = 0;
});
it('should return metadata', async () => {

View file

@ -28,6 +28,7 @@ const {
contextRecordMock,
contextRecordMock2,
profilesManagerMock,
scopedEbtManagerMock,
} = createContextAwarenessMocks({ shouldRegisterProviders: false });
rootProfileServiceMock.registerProvider({
@ -46,7 +47,9 @@ rootProfileServiceMock.registerProvider(rootProfileProviderMock);
dataSourceProfileServiceMock.registerProvider(dataSourceProfileProviderMock);
documentProfileServiceMock.registerProvider(documentProfileProviderMock);
const scopedProfilesManager = profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: scopedEbtManagerMock,
});
const record = scopedProfilesManager.resolveDocumentProfile({ record: contextRecordMock });
const record2 = scopedProfilesManager.resolveDocumentProfile({ record: contextRecordMock2 });
const services = createDiscoverServicesMock();

View file

@ -8,7 +8,8 @@
*/
import { useEffect, useMemo, useState } from 'react';
import { useScopedProfilesManager, type GetProfilesOptions } from '../profiles_manager';
import { type GetProfilesOptions } from '../profiles_manager';
import { useScopedServices } from '../../components/scoped_services_provider';
/**
* Hook to retreive the resolved profiles
@ -16,7 +17,7 @@ import { useScopedProfilesManager, type GetProfilesOptions } from '../profiles_m
* @returns The resolved profiles
*/
export const useProfiles = ({ record }: GetProfilesOptions = {}) => {
const scopedProfilesManager = useScopedProfilesManager();
const { scopedProfilesManager } = useScopedServices();
const [profiles, setProfiles] = useState(() => scopedProfilesManager.getProfiles({ record }));
const profiles$ = useMemo(
() => scopedProfilesManager.getProfiles$({ record }),

View file

@ -13,8 +13,6 @@ export { getMergedAccessor } from './composable_profile';
export {
ProfilesManager,
ScopedProfilesManager,
ScopedProfilesManagerProvider,
useScopedProfilesManager,
ContextualProfileLevel,
type GetProfilesOptions,
} from './profiles_manager';

View file

@ -9,5 +9,4 @@
export { ProfilesManager } from './profiles_manager';
export { ScopedProfilesManager, type GetProfilesOptions } from './scoped_profiles_manager';
export { ScopedProfilesManagerProvider, useScopedProfilesManager } from './provider';
export { ContextualProfileLevel } from './consts';

View file

@ -25,25 +25,31 @@ describe('ProfilesManager', () => {
beforeEach(() => {
jest.clearAllMocks();
mocks = createContextAwarenessMocks();
jest.spyOn(mocks.ebtManagerMock, 'updateProfilesContextWith');
jest.spyOn(mocks.ebtManagerMock, 'trackContextualProfileResolvedEvent');
jest.spyOn(mocks.scopedEbtManagerMock, 'updateProfilesContextWith');
jest.spyOn(mocks.scopedEbtManagerMock, 'trackContextualProfileResolvedEvent');
});
it('should return default profiles', () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
const profiles = scopedProfilesManager.getProfiles();
expect(profiles).toEqual([{}, {}, {}]);
});
it('should resolve root profile', async () => {
await mocks.profilesManagerMock.resolveRootProfile({});
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
const profiles = scopedProfilesManager.getProfiles();
expect(profiles).toEqual([toAppliedProfile(mocks.rootProfileProviderMock.profile), {}, {}]);
});
it('should resolve data source profile', async () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
await scopedProfilesManager.resolveDataSourceProfile({});
const profiles = scopedProfilesManager.getProfiles();
expect(profiles).toEqual([
@ -54,7 +60,9 @@ describe('ProfilesManager', () => {
});
it('should resolve document profile', async () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
const record = scopedProfilesManager.resolveDocumentProfile({
record: mocks.contextRecordMock,
});
@ -64,7 +72,9 @@ describe('ProfilesManager', () => {
it('should resolve multiple profiles', async () => {
await mocks.profilesManagerMock.resolveRootProfile({});
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
await scopedProfilesManager.resolveDataSourceProfile({});
const record = scopedProfilesManager.resolveDocumentProfile({
record: mocks.contextRecordMock,
@ -76,23 +86,31 @@ describe('ProfilesManager', () => {
toAppliedProfile(mocks.documentProfileProviderMock.profile),
]);
expect(mocks.ebtManagerMock.updateProfilesContextWith).toHaveBeenCalledWith([
expect(mocks.scopedEbtManagerMock.updateProfilesContextWith).toHaveBeenCalledWith([
'root-profile',
'data-source-profile',
]);
expect(mocks.ebtManagerMock.trackContextualProfileResolvedEvent).toHaveBeenNthCalledWith(1, {
profileId: 'root-profile',
contextLevel: 'rootLevel',
});
expect(mocks.ebtManagerMock.trackContextualProfileResolvedEvent).toHaveBeenNthCalledWith(2, {
profileId: 'data-source-profile',
contextLevel: 'dataSourceLevel',
});
expect(mocks.scopedEbtManagerMock.trackContextualProfileResolvedEvent).toHaveBeenNthCalledWith(
1,
{
profileId: 'root-profile',
contextLevel: 'rootLevel',
}
);
expect(mocks.scopedEbtManagerMock.trackContextualProfileResolvedEvent).toHaveBeenNthCalledWith(
2,
{
profileId: 'data-source-profile',
contextLevel: 'dataSourceLevel',
}
);
});
it('should expose profiles as an observable', async () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
const getProfilesSpy = jest.spyOn(scopedProfilesManager, 'getProfiles');
const record = scopedProfilesManager.resolveDocumentProfile({
record: mocks.contextRecordMock,
@ -135,7 +153,9 @@ describe('ProfilesManager', () => {
});
it('should not resolve data source profile again if params have not changed', async () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
await scopedProfilesManager.resolveDataSourceProfile({
dataSource: createEsqlDataSource(),
query: { esql: 'from *' },
@ -149,7 +169,9 @@ describe('ProfilesManager', () => {
});
it('should resolve data source profile again if params have changed', async () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
await scopedProfilesManager.resolveDataSourceProfile({
dataSource: createEsqlDataSource(),
query: { esql: 'from *' },
@ -164,7 +186,9 @@ describe('ProfilesManager', () => {
it('should log an error and fall back to the default profile if root profile resolution fails', async () => {
await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' });
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
let profiles = scopedProfilesManager.getProfiles();
expect(profiles).toEqual([toAppliedProfile(mocks.rootProfileProviderMock.profile), {}, {}]);
const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve');
@ -179,7 +203,9 @@ describe('ProfilesManager', () => {
});
it('should log an error and fall back to the default profile if data source profile resolution fails', async () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
await scopedProfilesManager.resolveDataSourceProfile({
dataSource: createEsqlDataSource(),
query: { esql: 'from *' },
@ -205,7 +231,9 @@ describe('ProfilesManager', () => {
});
it('should log an error and fall back to the default profile if document profile resolution fails', () => {
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
const record = scopedProfilesManager.resolveDocumentProfile({
record: mocks.contextRecordMock,
});
@ -224,7 +252,7 @@ describe('ProfilesManager', () => {
new Error('Failed to resolve')
);
expect(profiles).toEqual([{}, {}, {}]);
expect(mocks.ebtManagerMock.trackContextualProfileResolvedEvent).toHaveBeenCalledWith({
expect(mocks.scopedEbtManagerMock.trackContextualProfileResolvedEvent).toHaveBeenCalledWith({
profileId: 'document-profile',
contextLevel: 'documentLevel',
});
@ -245,7 +273,9 @@ describe('ProfilesManager', () => {
});
expect(resolveSpy).toHaveReturnedTimes(1);
expect(resolveSpy).toHaveLastReturnedWith(deferredResult);
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
expect(scopedProfilesManager.getProfiles()).toEqual([{}, {}, {}]);
const resolvedDeferredResult2$ = new Subject();
const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext);
@ -284,7 +314,9 @@ describe('ProfilesManager', () => {
const resolvedDeferredResult$ = new Subject();
const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context);
resolveSpy.mockResolvedValueOnce(deferredResult);
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager();
const scopedProfilesManager = mocks.profilesManagerMock.createScopedProfilesManager({
scopedEbtManager: mocks.scopedEbtManagerMock,
});
const promise1 = scopedProfilesManager.resolveDataSourceProfile({
dataSource: createEsqlDataSource(),
query: { esql: 'from *' },

View file

@ -17,7 +17,7 @@ import type {
RootContext,
} from '../profiles';
import type { ContextWithProfileId } from '../profile_service';
import type { DiscoverEBTManager } from '../../plugin_imports/discover_ebt_manager';
import type { ScopedDiscoverEBTManager } from '../../ebt_manager';
import type { AppliedProfile } from '../composable_profile';
import { logResolutionError } from './utils';
import { ScopedProfilesManager } from './scoped_profiles_manager';
@ -51,8 +51,7 @@ export class ProfilesManager {
constructor(
private readonly rootProfileService: RootProfileService,
private readonly dataSourceProfileService: DataSourceProfileService,
private readonly documentProfileService: DocumentProfileService,
private readonly ebtManager: DiscoverEBTManager
private readonly documentProfileService: DocumentProfileService
) {
this.rootContext$ = new BehaviorSubject(rootProfileService.defaultContext);
this.rootProfile = rootProfileService.getProfile({ context: this.rootContext$.getValue() });
@ -110,13 +109,17 @@ export class ProfilesManager {
* Creates a profiles manager instance scoped to a single tab with a shared root context
* @returns The scoped profiles manager
*/
public createScopedProfilesManager() {
public createScopedProfilesManager({
scopedEbtManager,
}: {
scopedEbtManager: ScopedDiscoverEBTManager;
}) {
return new ScopedProfilesManager(
this.rootContext$,
() => this.rootProfile,
this.dataSourceProfileService,
this.documentProfileService,
this.ebtManager
scopedEbtManager
);
}
}

View file

@ -1,34 +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 React, { type PropsWithChildren, createContext, useContext } from 'react';
import type { ScopedProfilesManager } from './scoped_profiles_manager';
const scopedProfilesManagerContext = createContext<ScopedProfilesManager | undefined>(undefined);
export const ScopedProfilesManagerProvider = ({
scopedProfilesManager,
children,
}: PropsWithChildren<{
scopedProfilesManager: ScopedProfilesManager;
}>) => (
<scopedProfilesManagerContext.Provider value={scopedProfilesManager}>
{children}
</scopedProfilesManagerContext.Provider>
);
export const useScopedProfilesManager = () => {
const context = useContext(scopedProfilesManagerContext);
if (!context) {
throw new Error('useScopedProfilesManager must be used within a ScopedProfilesManagerProvider');
}
return context;
};

View file

@ -24,7 +24,7 @@ import type {
DocumentProfileService,
} from '../profiles/document_profile';
import type { RootContext } from '../profiles/root_profile';
import type { DiscoverEBTManager } from '../../plugin_imports/discover_ebt_manager';
import type { ScopedDiscoverEBTManager } from '../../ebt_manager';
import { logResolutionError } from './utils';
import { DataSourceType, isDataSourceType } from '../../../common/data_sources';
import { ContextualProfileLevel } from './consts';
@ -60,7 +60,7 @@ export class ScopedProfilesManager {
private readonly getRootProfile: () => AppliedProfile,
private readonly dataSourceProfileService: DataSourceProfileService,
private readonly documentProfileService: DocumentProfileService,
private readonly ebtManager: DiscoverEBTManager
private readonly scopedEbtManager: ScopedDiscoverEBTManager
) {
this.dataSourceContext$ = new BehaviorSubject(dataSourceProfileService.defaultContext);
this.dataSourceProfile = dataSourceProfileService.getProfile({
@ -143,7 +143,7 @@ export class ScopedProfilesManager {
}
}
this.ebtManager.trackContextualProfileResolvedEvent({
this.scopedEbtManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.documentLevel,
profileId: context.profileId,
});
@ -187,16 +187,16 @@ export class ScopedProfilesManager {
private trackActiveProfiles(rootContextProfileId: string, dataSourceContextProfileId: string) {
const dscProfiles = [rootContextProfileId, dataSourceContextProfileId];
this.ebtManager.trackContextualProfileResolvedEvent({
this.scopedEbtManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: rootContextProfileId,
});
this.ebtManager.trackContextualProfileResolvedEvent({
this.scopedEbtManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.dataSourceLevel,
profileId: dataSourceContextProfileId,
});
this.ebtManager.updateProfilesContextWith(dscProfiles);
this.scopedEbtManager.updateProfilesContextWith(dscProfiles);
}
}

View file

@ -0,0 +1,635 @@
/*
* 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, skip } from 'rxjs';
import { coreMock } from '@kbn/core/public/mocks';
import { type DiscoverEBTContextProps, DiscoverEBTManager } from '.';
import { registerDiscoverEBTManagerAnalytics } from './discover_ebt_manager_registrations';
import { ContextualProfileLevel } from '../context_awareness/profiles_manager';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
jest.mock('@kbn/ebt-tools', () => ({
...jest.requireActual('@kbn/ebt-tools'),
reportPerformanceMetricEvent: jest.fn(),
}));
describe('DiscoverEBTManager', () => {
let discoverEBTContextManager: DiscoverEBTManager;
let discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>;
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();
discoverEbtContext$ = new BehaviorSubject<DiscoverEBTContextProps>({
discoverProfiles: [],
});
(coreSetupMock.analytics.reportEvent as jest.Mock).mockClear();
(reportPerformanceMetricEvent as jest.Mock).mockClear();
jest.spyOn(window.performance, 'now').mockRestore();
});
describe('register', () => {
it('should register the context provider and custom events', () => {
registerDiscoverEBTManagerAnalytics(coreSetupMock, discoverEbtContext$);
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
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,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
scopedManager.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,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
scopedManager.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,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.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,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
discoverEBTContextManager.onDiscoverAppUnmounted();
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
discoverEBTContextManager.onDiscoverAppMounted();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
});
it('should not update the profiles if there is no active scoped manager', () => {
const dscProfiles = ['profile1', 'profile2'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
});
it('should update the profiles when activating a scoped manager', () => {
const dscProfiles = ['profile1', 'profile2'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
scopedManager.setAsActiveManager();
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
});
it('should update the profiles when changing the active scoped manager', () => {
const dscProfiles = ['profile1', 'profile2'];
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
const anotherScopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
anotherScopedManager.updateProfilesContextWith(dscProfiles2);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
anotherScopedManager.setAsActiveManager();
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles2);
});
it('should not update the profiles for inactive scoped managers', () => {
const dscProfiles = ['profile1', 'profile2'];
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
const anotherScopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
anotherScopedManager.setAsActiveManager();
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
scopedManager.updateProfilesContextWith(dscProfiles2);
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
});
});
describe('onDiscoverAppMounted/onDiscoverAppUnmounted', () => {
it('should clear the active scoped manager after unmounting and remounting', () => {
const dscProfiles = ['profile1', 'profile2'];
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
discoverEBTContextManager.onDiscoverAppUnmounted();
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
discoverEBTContextManager.onDiscoverAppMounted();
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
scopedManager.updateProfilesContextWith(dscProfiles2);
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
scopedManager.setAsActiveManager();
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles2);
});
});
describe('trackFieldUsageEvent', () => {
it('should track the field usage when a field is added to the table', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
await scopedManager.trackDataTableSelection({
fieldName: 'test',
fieldsMetadata,
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledWith('discover_field_usage', {
eventName: 'dataTableSelection',
fieldName: 'test',
});
await scopedManager.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,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
await scopedManager.trackDataTableRemoval({
fieldName: 'test',
fieldsMetadata,
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledWith('discover_field_usage', {
eventName: 'dataTableRemoval',
fieldName: 'test',
});
await scopedManager.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,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
await scopedManager.trackFilterAddition({
fieldName: 'test',
fieldsMetadata,
filterOperation: '+',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledWith('discover_field_usage', {
eventName: 'filterAddition',
fieldName: 'test',
filterOperation: '+',
});
await scopedManager.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_',
});
});
it('should temporarily update the discoverEbtContext$ when tracking field usage in an inactive scoped manager', async () => {
const dscProfiles = ['profile1', 'profile2'];
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
const anotherScopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
anotherScopedManager.updateProfilesContextWith(dscProfiles2);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
const results: unknown[] = [];
discoverEbtContext$.pipe(skip(1)).subscribe(({ discoverProfiles }) => {
results.push(discoverProfiles);
});
jest
.spyOn(coreSetupMock.analytics, 'reportEvent')
.mockImplementation((eventType, eventData) => {
results.push({ eventType, eventData });
});
await anotherScopedManager.trackDataTableSelection({
fieldName: 'test',
fieldsMetadata,
});
await anotherScopedManager.trackDataTableRemoval({
fieldName: 'test',
fieldsMetadata,
});
await anotherScopedManager.trackFilterAddition({
fieldName: 'test',
fieldsMetadata,
filterOperation: '+',
});
expect(results).toEqual([
['profile21', 'profile22'],
{
eventType: 'discover_field_usage',
eventData: {
eventName: 'dataTableSelection',
fieldName: 'test',
},
},
['profile1', 'profile2'],
['profile21', 'profile22'],
{
eventType: 'discover_field_usage',
eventData: {
eventName: 'dataTableRemoval',
fieldName: 'test',
},
},
['profile1', 'profile2'],
['profile21', 'profile22'],
{
eventType: 'discover_field_usage',
eventData: {
eventName: 'filterAddition',
fieldName: 'test',
filterOperation: '+',
},
},
['profile1', 'profile2'],
]);
});
});
describe('trackContextualProfileResolvedEvent', () => {
it('should track the event when a next contextual profile is resolved', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenNthCalledWith(
1,
'discover_profile_resolved',
{
contextLevel: 'rootLevel',
profileId: 'test',
}
);
scopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.dataSourceLevel,
profileId: 'data-source-test',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenNthCalledWith(
2,
'discover_profile_resolved',
{
contextLevel: 'dataSourceLevel',
profileId: 'data-source-test',
}
);
scopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.documentLevel,
profileId: 'document-test',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenNthCalledWith(
3,
'discover_profile_resolved',
{
contextLevel: 'documentLevel',
profileId: 'document-test',
}
);
});
it('should not trigger duplicate requests', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(1);
scopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(1);
scopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test2',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(2);
});
it('should temporarily update the discoverEbtContext$ when a contextual profile is resolved in an inactive scoped manager', () => {
const dscProfiles = ['profile1', 'profile2'];
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
const anotherScopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
anotherScopedManager.updateProfilesContextWith(dscProfiles2);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
const results: unknown[] = [];
discoverEbtContext$.pipe(skip(1)).subscribe(({ discoverProfiles }) => {
results.push(discoverProfiles);
});
jest
.spyOn(coreSetupMock.analytics, 'reportEvent')
.mockImplementation((eventType, eventData) => {
results.push({ eventType, eventData });
});
anotherScopedManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test',
});
expect(results).toEqual([
['profile21', 'profile22'],
{
eventType: 'discover_profile_resolved',
eventData: {
contextLevel: 'rootLevel',
profileId: 'test',
},
},
['profile1', 'profile2'],
]);
});
});
describe('trackPerformanceEvent', () => {
it('should track performance events', () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
jest.spyOn(window.performance, 'now').mockReturnValueOnce(250).mockReturnValueOnce(1000);
const tracker = scopedManager.trackPerformanceEvent('testEvent');
tracker.reportEvent({ meta: { foo: 'bar' } });
expect(reportPerformanceMetricEvent).toHaveBeenCalledWith(coreSetupMock.analytics, {
eventName: 'testEvent',
duration: 750,
meta: { foo: 'bar' },
});
});
it('should temporarily update the discoverEbtContext$ when tracking performance events in an inactive scoped manager', () => {
const dscProfiles = ['profile1', 'profile2'];
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
const scopedManager = discoverEBTContextManager.createScopedEBTManager();
const anotherScopedManager = discoverEBTContextManager.createScopedEBTManager();
scopedManager.setAsActiveManager();
scopedManager.updateProfilesContextWith(dscProfiles);
anotherScopedManager.updateProfilesContextWith(dscProfiles2);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
const results: unknown[] = [];
discoverEbtContext$.pipe(skip(1)).subscribe(({ discoverProfiles }) => {
results.push(discoverProfiles);
});
(reportPerformanceMetricEvent as jest.Mock).mockImplementation((_, eventData) => {
results.push(eventData);
});
jest.spyOn(window.performance, 'now').mockReturnValueOnce(250).mockReturnValueOnce(1000);
const tracker = anotherScopedManager.trackPerformanceEvent('testEvent');
tracker.reportEvent({ meta: { foo: 'bar' } });
expect(results).toEqual([
['profile21', 'profile22'],
{
eventName: 'testEvent',
duration: 750,
meta: { foo: 'bar' },
},
['profile1', 'profile2'],
]);
});
});
});

View file

@ -0,0 +1,110 @@
/*
* 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 type { BehaviorSubject } from 'rxjs';
import { isEqual } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type {
DiscoverEBTContext,
DiscoverEBTContextProps,
ReportEvent,
ReportPerformanceEvent,
SetAsActiveManager,
UpdateProfilesContextWith,
} from './types';
import { ScopedDiscoverEBTManager } from './scoped_discover_ebt_manager';
export class DiscoverEBTManager {
private isCustomContextEnabled: boolean = false;
private customContext$: DiscoverEBTContext | undefined;
private activeScopedManagerId: string | undefined;
private reportEvent: ReportEvent | undefined;
private reportPerformanceEvent: ReportPerformanceEvent | undefined;
private updateProfilesContextWith: UpdateProfilesContextWith = (discoverProfiles) => {
if (
this.isCustomContextEnabled &&
this.customContext$ &&
!isEqual(this.customContext$.getValue().discoverProfiles, discoverProfiles)
) {
this.customContext$.next({
discoverProfiles,
});
}
};
// https://docs.elastic.dev/telemetry/collection/event-based-telemetry
public initialize({
core,
discoverEbtContext$,
}: {
core: CoreSetup;
discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>;
}) {
this.customContext$ = discoverEbtContext$;
this.reportEvent = core.analytics.reportEvent;
this.reportPerformanceEvent = (eventData) =>
reportPerformanceMetricEvent(core.analytics, eventData);
}
public onDiscoverAppMounted() {
this.isCustomContextEnabled = true;
}
public onDiscoverAppUnmounted() {
this.updateProfilesContextWith([]);
this.isCustomContextEnabled = false;
this.activeScopedManagerId = undefined;
}
public getProfilesContext() {
return this.customContext$?.getValue()?.discoverProfiles;
}
public createScopedEBTManager() {
const scopedManagerId = uuidv4();
let scopedDiscoverProfiles: string[] = [];
const withScopedContext =
<T extends (...params: Parameters<T>) => void>(callback: T) =>
(...params: Parameters<T>) => {
const currentDiscoverProfiles = this.customContext$?.getValue().discoverProfiles ?? [];
this.updateProfilesContextWith(scopedDiscoverProfiles);
callback(...params);
this.updateProfilesContextWith(currentDiscoverProfiles);
};
const scopedReportEvent = this.reportEvent ? withScopedContext(this.reportEvent) : undefined;
const scopedReportPerformanceEvent = this.reportPerformanceEvent
? withScopedContext(this.reportPerformanceEvent)
: undefined;
const scopedUpdateProfilesContextWith: UpdateProfilesContextWith = (discoverProfiles) => {
scopedDiscoverProfiles = discoverProfiles;
if (this.activeScopedManagerId === scopedManagerId) {
this.updateProfilesContextWith(discoverProfiles);
}
};
const scopedSetAsActiveManager: SetAsActiveManager = () => {
this.activeScopedManagerId = scopedManagerId;
this.updateProfilesContextWith(scopedDiscoverProfiles);
};
return new ScopedDiscoverEBTManager(
scopedReportEvent,
scopedReportPerformanceEvent,
scopedUpdateProfilesContextWith,
scopedSetAsActiveManager
);
}
}

View file

@ -10,7 +10,7 @@
import type { CoreSetup } from '@kbn/core/public';
import type { BehaviorSubject } from 'rxjs';
import type { DiscoverStartPlugins } from '../types';
import type { DiscoverEBTContextProps } from './discover_ebt_manager';
import type { DiscoverEBTContextProps } from './types';
/**
* Field usage events i.e. when a field is selected in the data table, removed from the data table, or a filter is added

View file

@ -0,0 +1,25 @@
/*
* 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".
*/
export {
registerDiscoverEBTManagerAnalytics,
FIELD_USAGE_EVENT_TYPE,
FIELD_USAGE_EVENT_NAME,
FIELD_USAGE_FIELD_NAME,
FIELD_USAGE_FILTER_OPERATION,
CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE,
CONTEXTUAL_PROFILE_LEVEL,
CONTEXTUAL_PROFILE_ID,
} from './discover_ebt_manager_registrations';
export { DiscoverEBTManager } from './discover_ebt_manager';
export type { ScopedDiscoverEBTManager } from './scoped_discover_ebt_manager';
export type { DiscoverEBTContextProps, DiscoverEBTContext } from './types';

View file

@ -7,12 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { BehaviorSubject } from 'rxjs';
import { isEqual } from 'lodash';
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { ContextualProfileLevel } from '../context_awareness/profiles_manager';
import type { PerformanceMetricEvent } from '@kbn/ebt-tools';
import {
CONTEXTUAL_PROFILE_ID,
CONTEXTUAL_PROFILE_LEVEL,
@ -22,10 +18,17 @@ import {
FIELD_USAGE_FIELD_NAME,
FIELD_USAGE_FILTER_OPERATION,
} from './discover_ebt_manager_registrations';
import { ContextualProfileLevel } from '../context_awareness';
import type {
ReportEvent,
ReportPerformanceEvent,
SetAsActiveManager,
UpdateProfilesContextWith,
} from './types';
type FilterOperation = '+' | '-' | '_exists_';
export enum FieldUsageEventName {
enum FieldUsageEventName {
dataTableSelection = 'dataTableSelection',
dataTableRemoval = 'dataTableRemoval',
filterAddition = 'filterAddition',
@ -41,90 +44,23 @@ interface ContextualProfileResolvedEventData {
[CONTEXTUAL_PROFILE_ID]: string;
}
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;
export class ScopedDiscoverEBTManager {
private lastResolvedContextProfiles: {
[ContextualProfileLevel.rootLevel]: string | undefined;
[ContextualProfileLevel.dataSourceLevel]: string | undefined;
[ContextualProfileLevel.documentLevel]: string | undefined;
} = {
[ContextualProfileLevel.rootLevel]: undefined,
[ContextualProfileLevel.dataSourceLevel]: undefined,
[ContextualProfileLevel.documentLevel]: undefined,
};
constructor() {
this.lastResolvedContextProfiles = {
[ContextualProfileLevel.rootLevel]: undefined,
[ContextualProfileLevel.dataSourceLevel]: undefined,
[ContextualProfileLevel.documentLevel]: undefined,
};
}
public trackPerformanceEvent(eventName: string) {
return { reportEvent: () => {} };
}
// https://docs.elastic.dev/telemetry/collection/event-based-telemetry
public initialize({
core,
discoverEbtContext$,
}: {
core: CoreSetup;
discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>;
}) {
this.customContext$ = discoverEbtContext$;
this.reportEvent = core.analytics.reportEvent;
this.trackPerformanceEvent = (eventName: string) => {
const startTime = window.performance.now();
let reported = false;
return {
reportEvent: () => {
if (reported) return;
reported = true;
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(core.analytics, {
eventName,
duration,
});
},
};
};
}
public onDiscoverAppMounted() {
this.isCustomContextEnabled = true;
}
public onDiscoverAppUnmounted() {
this.updateProfilesContextWith([]);
this.isCustomContextEnabled = false;
this.lastResolvedContextProfiles = {
[ContextualProfileLevel.rootLevel]: undefined,
[ContextualProfileLevel.dataSourceLevel]: undefined,
[ContextualProfileLevel.documentLevel]: undefined,
};
}
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;
}
constructor(
private readonly reportEvent: ReportEvent | undefined,
private readonly reportPerformanceEvent: ReportPerformanceEvent | undefined,
public readonly updateProfilesContextWith: UpdateProfilesContextWith,
public readonly setAsActiveManager: SetAsActiveManager
) {}
private async trackFieldUsageEvent({
eventName,
@ -235,4 +171,26 @@ export class DiscoverEBTManager {
this.reportEvent(CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE, eventData);
}
public trackPerformanceEvent(eventName: string) {
const startTime = window.performance.now();
let reported = false;
return {
reportEvent: (eventData?: Omit<PerformanceMetricEvent, 'eventName' | 'duration'>) => {
if (reported || !this.reportPerformanceEvent) {
return;
}
reported = true;
const duration = window.performance.now() - startTime;
this.reportPerformanceEvent({
...eventData,
eventName,
duration,
});
},
};
}
}

View file

@ -0,0 +1,27 @@
/*
* 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 type { CoreSetup } from '@kbn/core/public';
import type { PerformanceMetricEvent } from '@kbn/ebt-tools';
import type { BehaviorSubject } from 'rxjs';
export interface DiscoverEBTContextProps {
discoverProfiles: string[]; // Discover Context Awareness Profiles
}
export type DiscoverEBTContext = BehaviorSubject<DiscoverEBTContextProps>;
export type ReportEvent = CoreSetup['analytics']['reportEvent'];
export type ReportPerformanceEvent = (eventData: PerformanceMetricEvent) => void;
export type UpdateProfilesContextWith = (
discoverProfiles: DiscoverEBTContextProps['discoverProfiles']
) => void;
export type SetAsActiveManager = () => void;

View file

@ -255,8 +255,11 @@ describe('saved search embeddable', () => {
});
it('should resolve data source profile when fetching', async () => {
const scopedProfilesManager =
discoverServiceMock.profilesManager.createScopedProfilesManager();
const scopedProfilesManager = discoverServiceMock.profilesManager.createScopedProfilesManager(
{
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
}
);
const resolveDataSourceProfileSpy = jest.spyOn(
scopedProfilesManager,
'resolveDataSourceProfile'

View file

@ -40,7 +40,8 @@ import { initializeFetch, isEsqlMode } from './initialize_fetch';
import { initializeSearchEmbeddableApi } from './initialize_search_embeddable_api';
import type { SearchEmbeddableApi, SearchEmbeddableSerializedState } from './types';
import { deserializeState, serializeState } from './utils/serialization_utils';
import { BaseAppWrapper, ScopedProfilesManagerProvider } from '../context_awareness';
import { BaseAppWrapper } from '../context_awareness';
import { ScopedServicesProvider } from '../components/scoped_services_provider';
export const getSearchEmbeddableFactory = ({
startServices,
@ -73,7 +74,10 @@ export const getSearchEmbeddableFactory = ({
solutionNavId,
});
const AppWrapper = getRenderAppWrapper?.(BaseAppWrapper) ?? BaseAppWrapper;
const scopedProfilesManager = discoverServices.profilesManager.createScopedProfilesManager();
const scopedEbtManager = discoverServices.ebtManager.createScopedEBTManager();
const scopedProfilesManager = discoverServices.profilesManager.createScopedProfilesManager({
scopedEbtManager,
});
/** Specific by-reference state */
const savedObjectId$ = new BehaviorSubject<string | undefined>(runtimeState?.savedObjectId);
@ -315,7 +319,10 @@ export const getSearchEmbeddableFactory = ({
return (
<KibanaRenderContextProvider {...discoverServices.core}>
<KibanaContextProvider services={discoverServices}>
<ScopedProfilesManagerProvider scopedProfilesManager={scopedProfilesManager}>
<ScopedServicesProvider
scopedProfilesManager={scopedProfilesManager}
scopedEBTManager={scopedEbtManager}
>
<AppWrapper>
{renderAsFieldStatsTable ? (
<SearchEmbeddablFieldStatsTableComponent
@ -353,7 +360,7 @@ export const getSearchEmbeddableFactory = ({
</CellActionsProvider>
)}
</AppWrapper>
</ScopedProfilesManagerProvider>
</ScopedServicesProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>
);

View file

@ -42,7 +42,9 @@ describe('initialize fetch', () => {
api: mockedApi,
stateManager,
discoverServices: discoverServiceMock,
scopedProfilesManager: discoverServiceMock.profilesManager.createScopedProfilesManager(),
scopedProfilesManager: discoverServiceMock.profilesManager.createScopedProfilesManager({
scopedEbtManager: discoverServiceMock.ebtManager.createScopedEBTManager(),
}),
...setters,
});
await waitOneTick();

View file

@ -57,13 +57,10 @@ import type {
DiscoverStartPlugins,
} from './types';
import { DISCOVER_CELL_ACTIONS_TRIGGER } from './context_awareness/types';
import type {
DiscoverEBTContextProps,
DiscoverEBTManager,
} from './plugin_imports/discover_ebt_manager';
import type { DiscoverEBTContextProps, DiscoverEBTManager } from './ebt_manager';
import { registerDiscoverEBTManagerAnalytics } from './ebt_manager/discover_ebt_manager_registrations';
import type { ProfilesManager } from './context_awareness';
import { forwardLegacyUrls } from './plugin_imports/forward_legacy_urls';
import { registerDiscoverEBTManagerAnalytics } from './plugin_imports/discover_ebt_manager_registrations';
/**
* Contains Discover, one of the oldest parts of Kibana
@ -262,7 +259,7 @@ export class DiscoverPlugin
const getDiscoverServicesInternal = async () => {
const ebtManager = await getEmptyEbtManager();
const { profilesManager } = await this.createProfileServices(ebtManager);
const { profilesManager } = await this.createProfileServices();
return this.getDiscoverServices({ core, plugins, profilesManager, ebtManager });
};
@ -280,7 +277,7 @@ export class DiscoverPlugin
}
}
private async createProfileServices(ebtManager: DiscoverEBTManager) {
private async createProfileServices() {
const {
RootProfileService,
DataSourceProfileService,
@ -294,8 +291,7 @@ export class DiscoverPlugin
const profilesManager = new ProfilesManager(
rootProfileService,
dataSourceProfileService,
documentProfileService,
ebtManager
documentProfileService
);
return {
@ -324,7 +320,7 @@ export class DiscoverPlugin
dataSourceProfileService,
documentProfileService,
profilesManager,
} = await this.createProfileServices(ebtManager);
} = await this.createProfileServices();
const services = await this.getDiscoverServices({
core,
plugins,

View file

@ -1,353 +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 { type DiscoverEBTContextProps, DiscoverEBTManager } from './discover_ebt_manager';
import { registerDiscoverEBTManagerAnalytics } from './discover_ebt_manager_registrations';
import { ContextualProfileLevel } from '../context_awareness/profiles_manager';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
describe('DiscoverEBTManager', () => {
let discoverEBTContextManager: DiscoverEBTManager;
let discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>;
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();
discoverEbtContext$ = new BehaviorSubject<DiscoverEBTContextProps>({
discoverProfiles: [],
});
(coreSetupMock.analytics.reportEvent as jest.Mock).mockClear();
});
describe('register', () => {
it('should register the context provider and custom events', () => {
registerDiscoverEBTManagerAnalytics(coreSetupMock, discoverEbtContext$);
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
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,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
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,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
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,
discoverEbtContext$,
});
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,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
discoverEBTContextManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toBe(dscProfiles);
discoverEBTContextManager.onDiscoverAppUnmounted();
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
discoverEBTContextManager.updateProfilesContextWith(dscProfiles);
expect(discoverEBTContextManager.getProfilesContext()).toEqual([]);
discoverEBTContextManager.onDiscoverAppMounted();
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,
discoverEbtContext$,
});
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,
discoverEbtContext$,
});
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,
discoverEbtContext$,
});
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_',
});
});
});
describe('trackContextualProfileResolvedEvent', () => {
it('should track the event when a next contextual profile is resolved', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenNthCalledWith(
1,
'discover_profile_resolved',
{
contextLevel: 'rootLevel',
profileId: 'test',
}
);
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.dataSourceLevel,
profileId: 'data-source-test',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenNthCalledWith(
2,
'discover_profile_resolved',
{
contextLevel: 'dataSourceLevel',
profileId: 'data-source-test',
}
);
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.documentLevel,
profileId: 'document-test',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenNthCalledWith(
3,
'discover_profile_resolved',
{
contextLevel: 'documentLevel',
profileId: 'document-test',
}
);
});
it('should not trigger duplicate requests', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(1);
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(1);
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test2',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(2);
});
it('should trigger similar requests after remount', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
discoverEbtContext$,
});
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(1);
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(1);
discoverEBTContextManager.onDiscoverAppUnmounted();
discoverEBTContextManager.onDiscoverAppMounted();
discoverEBTContextManager.trackContextualProfileResolvedEvent({
contextLevel: ContextualProfileLevel.rootLevel,
profileId: 'test1',
});
expect(coreSetupMock.analytics.reportEvent).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -8,7 +8,7 @@
*/
export { HistoryService } from './history_service';
export { DiscoverEBTManager } from './discover_ebt_manager';
export { DiscoverEBTManager } from '../ebt_manager/discover_ebt_manager';
export { RootProfileService } from '../context_awareness/profiles/root_profile';
export { DataSourceProfileService } from '../context_awareness/profiles/data_source_profile';
export { DocumentProfileService } from '../context_awareness/profiles/document_profile';