mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ML] Adding job import export usage telemetry collection (#107772)
* [ML] Adding job import export usage telemtry collection * removing accidental managemet type change * adding tests * changes based on review * adding constant for event ids * adding toHaveBeenCalledWith tests * renaming tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
91366f0221
commit
f074091ef1
11 changed files with 183 additions and 33 deletions
15
x-pack/plugins/ml/common/constants/usage_collection.ts
Normal file
15
x-pack/plugins/ml/common/constants/usage_collection.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ML_USAGE_EVENT = {
|
||||
IMPORTED_ANOMALY_DETECTOR_JOBS: 'imported_anomaly_detector_jobs',
|
||||
IMPORTED_DATA_FRAME_ANALYTICS_JOBS: 'imported_data_frame_analytics_jobs',
|
||||
EXPORTED_ANOMALY_DETECTOR_JOBS: 'exported_anomaly_detector_jobs',
|
||||
EXPORTED_DATA_FRAME_ANALYTICS_JOBS: 'exported_data_frame_analytics_jobs',
|
||||
} as const;
|
||||
|
||||
export type MlUsageEvent = typeof ML_USAGE_EVENT[keyof typeof ML_USAGE_EVENT];
|
|
@ -11,6 +11,7 @@ import ReactDOM from 'react-dom';
|
|||
|
||||
import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public';
|
||||
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
import {
|
||||
|
@ -19,7 +20,8 @@ import {
|
|||
} from '../../../../../src/plugins/kibana_react/public';
|
||||
import { setDependencyCache, clearCache } from './util/dependency_cache';
|
||||
import { setLicenseCache } from './license';
|
||||
import { MlSetupDependencies, MlStartDependencies } from '../plugin';
|
||||
import type { MlSetupDependencies, MlStartDependencies } from '../plugin';
|
||||
import { mlUsageCollectionProvider } from './services/usage_collection';
|
||||
|
||||
import { MlRouter } from './routing';
|
||||
import { mlApiServicesProvider } from './services/ml_api_service';
|
||||
|
@ -39,11 +41,12 @@ const localStorage = new Storage(window.localStorage);
|
|||
/**
|
||||
* Provides global services available across the entire ML app.
|
||||
*/
|
||||
export function getMlGlobalServices(httpStart: HttpStart) {
|
||||
export function getMlGlobalServices(httpStart: HttpStart, usageCollection?: UsageCollectionSetup) {
|
||||
const httpService = new HttpService(httpStart);
|
||||
return {
|
||||
httpService,
|
||||
mlApiServices: mlApiServicesProvider(httpService),
|
||||
mlUsageCollection: mlUsageCollectionProvider(usageCollection),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -68,8 +71,8 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
setBreadcrumbs: coreStart.chrome!.setBreadcrumbs,
|
||||
redirectToMlAccessDeniedPage,
|
||||
};
|
||||
|
||||
const services = {
|
||||
appName: 'ML',
|
||||
kibanaVersion: deps.kibanaVersion,
|
||||
share: deps.share,
|
||||
data: deps.data,
|
||||
|
@ -80,6 +83,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
maps: deps.maps,
|
||||
triggersActionsUi: deps.triggersActionsUi,
|
||||
dataVisualizer: deps.dataVisualizer,
|
||||
usageCollection: deps.usageCollection,
|
||||
...coreStart,
|
||||
};
|
||||
|
||||
|
@ -94,7 +98,10 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
<ApplicationUsageTrackingProvider>
|
||||
<I18nContext>
|
||||
<KibanaContextProvider
|
||||
services={{ ...services, mlServices: getMlGlobalServices(coreStart.http) }}
|
||||
services={{
|
||||
...services,
|
||||
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
|
||||
}}
|
||||
>
|
||||
<MlRouter pageDeps={pageDeps} />
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -46,6 +46,7 @@ export const ExportJobsFlyout: FC<Props> = ({ isDisabled, currentTab }) => {
|
|||
const {
|
||||
services: {
|
||||
notifications: { toasts },
|
||||
mlServices: { mlUsageCollection },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
|
@ -121,6 +122,13 @@ export const ExportJobsFlyout: FC<Props> = ({ isDisabled, currentTab }) => {
|
|||
await jobsExportService.exportDataframeAnalyticsJobs(selectedJobIds);
|
||||
}
|
||||
|
||||
mlUsageCollection.count(
|
||||
selectedJobType === 'anomaly-detector'
|
||||
? 'exported_anomaly_detector_jobs'
|
||||
: 'exported_data_frame_analytics_jobs',
|
||||
selectedJobIds.length
|
||||
);
|
||||
|
||||
setExporting(false);
|
||||
setShowFlyout(false);
|
||||
} catch (error) {
|
||||
|
|
|
@ -54,6 +54,7 @@ export const ImportJobsFlyout: FC<Props> = ({ isDisabled }) => {
|
|||
indexPatterns: { getTitles: getIndexPatternTitles },
|
||||
},
|
||||
notifications: { toasts },
|
||||
mlServices: { mlUsageCollection },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
|
@ -175,6 +176,7 @@ export const ImportJobsFlyout: FC<Props> = ({ isDisabled }) => {
|
|||
const renamedJobs = jobImportService.renameAdJobs(jobIdObjects, adJobs);
|
||||
try {
|
||||
await bulkCreateADJobs(renamedJobs);
|
||||
mlUsageCollection.count('imported_anomaly_detector_jobs', renamedJobs.length);
|
||||
} catch (error) {
|
||||
// display unexpected error
|
||||
displayErrorToast(error);
|
||||
|
@ -182,6 +184,7 @@ export const ImportJobsFlyout: FC<Props> = ({ isDisabled }) => {
|
|||
} else if (jobType === 'data-frame-analytics') {
|
||||
const renamedJobs = jobImportService.renameDfaJobs(jobIdObjects, dfaJobs);
|
||||
await bulkCreateDfaJobs(renamedJobs);
|
||||
mlUsageCollection.count('imported_data_frame_analytics_jobs', renamedJobs.length);
|
||||
}
|
||||
|
||||
setImporting(false);
|
||||
|
|
|
@ -5,21 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import type { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import type { CoreStart } from 'kibana/public';
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import {
|
||||
useKibana,
|
||||
KibanaReactContextValue,
|
||||
} from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { SecurityPluginSetup } from '../../../../../security/public';
|
||||
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
|
||||
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
|
||||
import { MlServicesContext } from '../../app';
|
||||
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import type { SecurityPluginSetup } from '../../../../../security/public';
|
||||
import type { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
|
||||
import type { SharePluginStart } from '../../../../../../../src/plugins/share/public';
|
||||
import type { MlServicesContext } from '../../app';
|
||||
import type { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public';
|
||||
import type { MapsStartApi } from '../../../../../maps/public';
|
||||
import type { DataVisualizerPluginStart } from '../../../../../data_visualizer/public';
|
||||
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
|
||||
import type { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
|
||||
|
||||
interface StartPlugins {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -30,10 +31,10 @@ interface StartPlugins {
|
|||
maps?: MapsStartApi;
|
||||
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
|
||||
dataVisualizer?: DataVisualizerPluginStart;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
export type StartServices = CoreStart &
|
||||
StartPlugins & {
|
||||
appName: string;
|
||||
kibanaVersion: string;
|
||||
storage: IStorageWrapper;
|
||||
} & MlServicesContext;
|
||||
|
|
|
@ -9,13 +9,15 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import type { CoreSetup } from 'kibana/public';
|
||||
import type { ManagementSetup } from 'src/plugins/management/public';
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import type { MlStartDependencies } from '../../plugin';
|
||||
|
||||
import type { ManagementAppMountParams } from '../../../../../../src/plugins/management/public';
|
||||
|
||||
export function registerManagementSection(
|
||||
management: ManagementSetup,
|
||||
core: CoreSetup<MlStartDependencies>
|
||||
core: CoreSetup<MlStartDependencies>,
|
||||
deps: { usageCollection?: UsageCollectionSetup }
|
||||
) {
|
||||
return management.sections.section.insightsAndAlerting.registerApp({
|
||||
id: 'jobsListLink',
|
||||
|
@ -25,7 +27,7 @@ export function registerManagementSection(
|
|||
order: 2,
|
||||
async mount(params: ManagementAppMountParams) {
|
||||
const { mountApp } = await import('./jobs_list');
|
||||
return mountApp(core, params);
|
||||
return mountApp(core, params, deps);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,9 +23,10 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import type { SpacesContextProps } from 'src/plugins/spaces_oss/public';
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import type { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { PLUGIN_ID } from '../../../../../../common/constants/app';
|
||||
import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/';
|
||||
import type { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public';
|
||||
|
||||
import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities';
|
||||
import {
|
||||
|
@ -39,7 +40,7 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi
|
|||
import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list';
|
||||
import { AccessDeniedPage } from '../access_denied_page';
|
||||
import { InsufficientLicensePage } from '../insufficient_license_page';
|
||||
import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public';
|
||||
import type { SharePluginStart } from '../../../../../../../../../src/plugins/share/public';
|
||||
import type { SpacesPluginStart } from '../../../../../../../spaces/public';
|
||||
import { JobSpacesSyncFlyout } from '../../../../components/job_spaces_sync';
|
||||
import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs';
|
||||
|
@ -47,7 +48,7 @@ import { getMlGlobalServices } from '../../../../app';
|
|||
import { ListingPageUrlState } from '../../../../../../common/types/common';
|
||||
import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page';
|
||||
import { ExportJobsFlyout, ImportJobsFlyout } from '../../../../components/import_export_jobs';
|
||||
import { JobType } from '../../../../../../common/types/saved_objects';
|
||||
import type { JobType } from '../../../../../../common/types/saved_objects';
|
||||
|
||||
interface Tab extends EuiTabbedContentTab {
|
||||
'data-test-subj': string;
|
||||
|
@ -128,7 +129,8 @@ export const JobsListPage: FC<{
|
|||
history: ManagementAppMountParams['history'];
|
||||
spacesApi?: SpacesPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
}> = ({ coreStart, share, history, spacesApi, data }) => {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}> = ({ coreStart, share, history, spacesApi, data, usageCollection }) => {
|
||||
const spacesEnabled = spacesApi !== undefined;
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [accessDenied, setAccessDenied] = useState(false);
|
||||
|
@ -219,7 +221,13 @@ export const JobsListPage: FC<{
|
|||
<RedirectAppLinks application={coreStart.application}>
|
||||
<I18nContext>
|
||||
<KibanaContextProvider
|
||||
services={{ ...coreStart, share, data, mlServices: getMlGlobalServices(coreStart.http) }}
|
||||
services={{
|
||||
...coreStart,
|
||||
share,
|
||||
data,
|
||||
usageCollection,
|
||||
mlServices: getMlGlobalServices(coreStart.http, usageCollection),
|
||||
}}
|
||||
>
|
||||
<ContextWrapper feature={PLUGIN_ID}>
|
||||
<Router history={history}>
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
|
||||
import ReactDOM, { unmountComponentAtNode } from 'react-dom';
|
||||
import React from 'react';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import type { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import type { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public/';
|
||||
import { MlStartDependencies } from '../../../plugin';
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import type { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public/';
|
||||
import type { MlStartDependencies } from '../../../plugin';
|
||||
import { JobsListPage } from './components';
|
||||
import { getJobsListBreadcrumbs } from '../breadcrumbs';
|
||||
import { setDependencyCache, clearCache } from '../../util/dependency_cache';
|
||||
import './_index.scss';
|
||||
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
|
||||
import { SpacesPluginStart } from '../../../../../spaces/public';
|
||||
import type { SharePluginStart } from '../../../../../../../src/plugins/share/public';
|
||||
import type { SpacesPluginStart } from '../../../../../spaces/public';
|
||||
|
||||
const renderApp = (
|
||||
element: HTMLElement,
|
||||
|
@ -24,10 +25,18 @@ const renderApp = (
|
|||
coreStart: CoreStart,
|
||||
share: SharePluginStart,
|
||||
data: DataPublicPluginStart,
|
||||
spacesApi?: SpacesPluginStart
|
||||
spacesApi?: SpacesPluginStart,
|
||||
usageCollection?: UsageCollectionSetup
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
React.createElement(JobsListPage, { coreStart, history, share, data, spacesApi }),
|
||||
React.createElement(JobsListPage, {
|
||||
coreStart,
|
||||
history,
|
||||
share,
|
||||
data,
|
||||
spacesApi,
|
||||
usageCollection,
|
||||
}),
|
||||
element
|
||||
);
|
||||
return () => {
|
||||
|
@ -38,7 +47,8 @@ const renderApp = (
|
|||
|
||||
export async function mountApp(
|
||||
core: CoreSetup<MlStartDependencies>,
|
||||
params: ManagementAppMountParams
|
||||
params: ManagementAppMountParams,
|
||||
deps: { usageCollection?: UsageCollectionSetup }
|
||||
) {
|
||||
const [coreStart, pluginsStart] = await core.getStartServices();
|
||||
|
||||
|
@ -56,6 +66,7 @@ export async function mountApp(
|
|||
coreStart,
|
||||
pluginsStart.share,
|
||||
pluginsStart.data,
|
||||
pluginsStart.spaces
|
||||
pluginsStart.spaces,
|
||||
deps.usageCollection
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
|
||||
import { mlUsageCollectionProvider } from './usage_collection';
|
||||
|
||||
describe('usage_collection', () => {
|
||||
let usageCollection: jest.Mocked<UsageCollectionSetup>;
|
||||
|
||||
beforeEach(() => {
|
||||
usageCollection = ({
|
||||
reportUiCounter: jest.fn(),
|
||||
} as unknown) as jest.Mocked<UsageCollectionSetup>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should use usageCollection for usage events', () => {
|
||||
const mlUsageCollection = mlUsageCollectionProvider(usageCollection);
|
||||
|
||||
mlUsageCollection.click('exported_anomaly_detector_jobs');
|
||||
mlUsageCollection.count('exported_data_frame_analytics_jobs');
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledTimes(2);
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'ml',
|
||||
'click',
|
||||
'exported_anomaly_detector_jobs',
|
||||
undefined
|
||||
);
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'ml',
|
||||
'count',
|
||||
'exported_data_frame_analytics_jobs',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test('should not use usageCollection if usageCollection is disabled', () => {
|
||||
const mlUsageCollection = mlUsageCollectionProvider(undefined);
|
||||
mlUsageCollection.click('imported_anomaly_detector_jobs', 1);
|
||||
mlUsageCollection.count('imported_data_frame_analytics_jobs', 2);
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(usageCollection.reportUiCounter).not.toHaveBeenCalledWith(
|
||||
'ml',
|
||||
'click',
|
||||
'imported_anomaly_detector_jobs',
|
||||
undefined
|
||||
);
|
||||
expect(usageCollection.reportUiCounter).not.toHaveBeenCalledWith(
|
||||
'ml',
|
||||
'count',
|
||||
'imported_data_frame_analytics_jobs',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { PLUGIN_ID } from '../../../common/constants/app';
|
||||
import { MlUsageEvent } from '../../../common/constants/usage_collection';
|
||||
|
||||
export function mlUsageCollectionProvider(usageCollection?: UsageCollectionSetup) {
|
||||
if (usageCollection === undefined) {
|
||||
// if usageCollection is disabled, swallow the clicks and counts
|
||||
const noop = (eventNames: string | string[], count?: number) => undefined;
|
||||
return {
|
||||
click: noop,
|
||||
count: noop,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
click: (eventNames: MlUsageEvent | MlUsageEvent[], count?: number) =>
|
||||
usageCollection.reportUiCounter(PLUGIN_ID, METRIC_TYPE.CLICK, eventNames, count),
|
||||
count: (eventNames: MlUsageEvent | MlUsageEvent[], count?: number) =>
|
||||
usageCollection.reportUiCounter(PLUGIN_ID, METRIC_TYPE.COUNT, eventNames, count),
|
||||
};
|
||||
}
|
|
@ -42,10 +42,10 @@ import {
|
|||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '../../triggers_actions_ui/public';
|
||||
import { DataVisualizerPluginStart } from '../../data_visualizer/public';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../alerting/public';
|
||||
import type { DataVisualizerPluginStart } from '../../data_visualizer/public';
|
||||
import type { PluginSetupContract as AlertingSetup } from '../../alerting/public';
|
||||
import { registerManagementSection } from './application/management';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
||||
import type { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
||||
|
||||
export interface MlStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -127,7 +127,9 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
|||
}
|
||||
|
||||
if (pluginsSetup.management) {
|
||||
registerManagementSection(pluginsSetup.management, core).enable();
|
||||
registerManagementSection(pluginsSetup.management, core, {
|
||||
usageCollection: pluginsSetup.usageCollection,
|
||||
}).enable();
|
||||
}
|
||||
|
||||
const licensing = pluginsSetup.licensing.license$.pipe(take(1));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue