[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:
James Gowdy 2021-08-09 21:04:18 +01:00 committed by GitHub
parent 91366f0221
commit f074091ef1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 183 additions and 33 deletions

View 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];

View file

@ -11,6 +11,7 @@ import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public'; 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 { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { import {
@ -19,7 +20,8 @@ import {
} from '../../../../../src/plugins/kibana_react/public'; } from '../../../../../src/plugins/kibana_react/public';
import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setDependencyCache, clearCache } from './util/dependency_cache';
import { setLicenseCache } from './license'; 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 { MlRouter } from './routing';
import { mlApiServicesProvider } from './services/ml_api_service'; 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. * 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); const httpService = new HttpService(httpStart);
return { return {
httpService, httpService,
mlApiServices: mlApiServicesProvider(httpService), mlApiServices: mlApiServicesProvider(httpService),
mlUsageCollection: mlUsageCollectionProvider(usageCollection),
}; };
} }
@ -68,8 +71,8 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs,
redirectToMlAccessDeniedPage, redirectToMlAccessDeniedPage,
}; };
const services = { const services = {
appName: 'ML',
kibanaVersion: deps.kibanaVersion, kibanaVersion: deps.kibanaVersion,
share: deps.share, share: deps.share,
data: deps.data, data: deps.data,
@ -80,6 +83,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
maps: deps.maps, maps: deps.maps,
triggersActionsUi: deps.triggersActionsUi, triggersActionsUi: deps.triggersActionsUi,
dataVisualizer: deps.dataVisualizer, dataVisualizer: deps.dataVisualizer,
usageCollection: deps.usageCollection,
...coreStart, ...coreStart,
}; };
@ -94,7 +98,10 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
<ApplicationUsageTrackingProvider> <ApplicationUsageTrackingProvider>
<I18nContext> <I18nContext>
<KibanaContextProvider <KibanaContextProvider
services={{ ...services, mlServices: getMlGlobalServices(coreStart.http) }} services={{
...services,
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
}}
> >
<MlRouter pageDeps={pageDeps} /> <MlRouter pageDeps={pageDeps} />
</KibanaContextProvider> </KibanaContextProvider>

View file

@ -46,6 +46,7 @@ export const ExportJobsFlyout: FC<Props> = ({ isDisabled, currentTab }) => {
const { const {
services: { services: {
notifications: { toasts }, notifications: { toasts },
mlServices: { mlUsageCollection },
}, },
} = useMlKibana(); } = useMlKibana();
@ -121,6 +122,13 @@ export const ExportJobsFlyout: FC<Props> = ({ isDisabled, currentTab }) => {
await jobsExportService.exportDataframeAnalyticsJobs(selectedJobIds); await jobsExportService.exportDataframeAnalyticsJobs(selectedJobIds);
} }
mlUsageCollection.count(
selectedJobType === 'anomaly-detector'
? 'exported_anomaly_detector_jobs'
: 'exported_data_frame_analytics_jobs',
selectedJobIds.length
);
setExporting(false); setExporting(false);
setShowFlyout(false); setShowFlyout(false);
} catch (error) { } catch (error) {

View file

@ -54,6 +54,7 @@ export const ImportJobsFlyout: FC<Props> = ({ isDisabled }) => {
indexPatterns: { getTitles: getIndexPatternTitles }, indexPatterns: { getTitles: getIndexPatternTitles },
}, },
notifications: { toasts }, notifications: { toasts },
mlServices: { mlUsageCollection },
}, },
} = useMlKibana(); } = useMlKibana();
@ -175,6 +176,7 @@ export const ImportJobsFlyout: FC<Props> = ({ isDisabled }) => {
const renamedJobs = jobImportService.renameAdJobs(jobIdObjects, adJobs); const renamedJobs = jobImportService.renameAdJobs(jobIdObjects, adJobs);
try { try {
await bulkCreateADJobs(renamedJobs); await bulkCreateADJobs(renamedJobs);
mlUsageCollection.count('imported_anomaly_detector_jobs', renamedJobs.length);
} catch (error) { } catch (error) {
// display unexpected error // display unexpected error
displayErrorToast(error); displayErrorToast(error);
@ -182,6 +184,7 @@ export const ImportJobsFlyout: FC<Props> = ({ isDisabled }) => {
} else if (jobType === 'data-frame-analytics') { } else if (jobType === 'data-frame-analytics') {
const renamedJobs = jobImportService.renameDfaJobs(jobIdObjects, dfaJobs); const renamedJobs = jobImportService.renameDfaJobs(jobIdObjects, dfaJobs);
await bulkCreateDfaJobs(renamedJobs); await bulkCreateDfaJobs(renamedJobs);
mlUsageCollection.count('imported_data_frame_analytics_jobs', renamedJobs.length);
} }
setImporting(false); setImporting(false);

View file

@ -5,21 +5,22 @@
* 2.0. * 2.0.
*/ */
import { DataPublicPluginStart } from 'src/plugins/data/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public';
import { CoreStart } from 'kibana/public'; import type { CoreStart } from 'kibana/public';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
import { import {
useKibana, useKibana,
KibanaReactContextValue, KibanaReactContextValue,
} from '../../../../../../../src/plugins/kibana_react/public'; } from '../../../../../../../src/plugins/kibana_react/public';
import { SecurityPluginSetup } from '../../../../../security/public'; import type { SecurityPluginSetup } from '../../../../../security/public';
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public'; import type { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; import type { SharePluginStart } from '../../../../../../../src/plugins/share/public';
import { MlServicesContext } from '../../app'; import type { MlServicesContext } from '../../app';
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import type { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public';
import type { MapsStartApi } from '../../../../../maps/public'; import type { MapsStartApi } from '../../../../../maps/public';
import type { DataVisualizerPluginStart } from '../../../../../data_visualizer/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 { interface StartPlugins {
data: DataPublicPluginStart; data: DataPublicPluginStart;
@ -30,10 +31,10 @@ interface StartPlugins {
maps?: MapsStartApi; maps?: MapsStartApi;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
dataVisualizer?: DataVisualizerPluginStart; dataVisualizer?: DataVisualizerPluginStart;
usageCollection?: UsageCollectionSetup;
} }
export type StartServices = CoreStart & export type StartServices = CoreStart &
StartPlugins & { StartPlugins & {
appName: string;
kibanaVersion: string; kibanaVersion: string;
storage: IStorageWrapper; storage: IStorageWrapper;
} & MlServicesContext; } & MlServicesContext;

View file

@ -9,13 +9,15 @@ import { i18n } from '@kbn/i18n';
import type { CoreSetup } from 'kibana/public'; import type { CoreSetup } from 'kibana/public';
import type { ManagementSetup } from 'src/plugins/management/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 { MlStartDependencies } from '../../plugin';
import type { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; import type { ManagementAppMountParams } from '../../../../../../src/plugins/management/public';
export function registerManagementSection( export function registerManagementSection(
management: ManagementSetup, management: ManagementSetup,
core: CoreSetup<MlStartDependencies> core: CoreSetup<MlStartDependencies>,
deps: { usageCollection?: UsageCollectionSetup }
) { ) {
return management.sections.section.insightsAndAlerting.registerApp({ return management.sections.section.insightsAndAlerting.registerApp({
id: 'jobsListLink', id: 'jobsListLink',
@ -25,7 +27,7 @@ export function registerManagementSection(
order: 2, order: 2,
async mount(params: ManagementAppMountParams) { async mount(params: ManagementAppMountParams) {
const { mountApp } = await import('./jobs_list'); const { mountApp } = await import('./jobs_list');
return mountApp(core, params); return mountApp(core, params, deps);
}, },
}); });
} }

View file

@ -23,9 +23,10 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; 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 type { DataPublicPluginStart } from 'src/plugins/data/public';
import { PLUGIN_ID } from '../../../../../../common/constants/app'; 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 { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities';
import { 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 { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list';
import { AccessDeniedPage } from '../access_denied_page'; import { AccessDeniedPage } from '../access_denied_page';
import { InsufficientLicensePage } from '../insufficient_license_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 type { SpacesPluginStart } from '../../../../../../../spaces/public';
import { JobSpacesSyncFlyout } from '../../../../components/job_spaces_sync'; import { JobSpacesSyncFlyout } from '../../../../components/job_spaces_sync';
import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs';
@ -47,7 +48,7 @@ import { getMlGlobalServices } from '../../../../app';
import { ListingPageUrlState } from '../../../../../../common/types/common'; import { ListingPageUrlState } from '../../../../../../common/types/common';
import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page';
import { ExportJobsFlyout, ImportJobsFlyout } from '../../../../components/import_export_jobs'; 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 { interface Tab extends EuiTabbedContentTab {
'data-test-subj': string; 'data-test-subj': string;
@ -128,7 +129,8 @@ export const JobsListPage: FC<{
history: ManagementAppMountParams['history']; history: ManagementAppMountParams['history'];
spacesApi?: SpacesPluginStart; spacesApi?: SpacesPluginStart;
data: DataPublicPluginStart; data: DataPublicPluginStart;
}> = ({ coreStart, share, history, spacesApi, data }) => { usageCollection?: UsageCollectionSetup;
}> = ({ coreStart, share, history, spacesApi, data, usageCollection }) => {
const spacesEnabled = spacesApi !== undefined; const spacesEnabled = spacesApi !== undefined;
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const [accessDenied, setAccessDenied] = useState(false); const [accessDenied, setAccessDenied] = useState(false);
@ -219,7 +221,13 @@ export const JobsListPage: FC<{
<RedirectAppLinks application={coreStart.application}> <RedirectAppLinks application={coreStart.application}>
<I18nContext> <I18nContext>
<KibanaContextProvider <KibanaContextProvider
services={{ ...coreStart, share, data, mlServices: getMlGlobalServices(coreStart.http) }} services={{
...coreStart,
share,
data,
usageCollection,
mlServices: getMlGlobalServices(coreStart.http, usageCollection),
}}
> >
<ContextWrapper feature={PLUGIN_ID}> <ContextWrapper feature={PLUGIN_ID}>
<Router history={history}> <Router history={history}>

View file

@ -7,16 +7,17 @@
import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import ReactDOM, { unmountComponentAtNode } from 'react-dom';
import React from 'react'; 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 type { DataPublicPluginStart } from 'src/plugins/data/public';
import { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public/'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
import { MlStartDependencies } from '../../../plugin'; import type { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public/';
import type { MlStartDependencies } from '../../../plugin';
import { JobsListPage } from './components'; import { JobsListPage } from './components';
import { getJobsListBreadcrumbs } from '../breadcrumbs'; import { getJobsListBreadcrumbs } from '../breadcrumbs';
import { setDependencyCache, clearCache } from '../../util/dependency_cache'; import { setDependencyCache, clearCache } from '../../util/dependency_cache';
import './_index.scss'; import './_index.scss';
import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; import type { SharePluginStart } from '../../../../../../../src/plugins/share/public';
import { SpacesPluginStart } from '../../../../../spaces/public'; import type { SpacesPluginStart } from '../../../../../spaces/public';
const renderApp = ( const renderApp = (
element: HTMLElement, element: HTMLElement,
@ -24,10 +25,18 @@ const renderApp = (
coreStart: CoreStart, coreStart: CoreStart,
share: SharePluginStart, share: SharePluginStart,
data: DataPublicPluginStart, data: DataPublicPluginStart,
spacesApi?: SpacesPluginStart spacesApi?: SpacesPluginStart,
usageCollection?: UsageCollectionSetup
) => { ) => {
ReactDOM.render( ReactDOM.render(
React.createElement(JobsListPage, { coreStart, history, share, data, spacesApi }), React.createElement(JobsListPage, {
coreStart,
history,
share,
data,
spacesApi,
usageCollection,
}),
element element
); );
return () => { return () => {
@ -38,7 +47,8 @@ const renderApp = (
export async function mountApp( export async function mountApp(
core: CoreSetup<MlStartDependencies>, core: CoreSetup<MlStartDependencies>,
params: ManagementAppMountParams params: ManagementAppMountParams,
deps: { usageCollection?: UsageCollectionSetup }
) { ) {
const [coreStart, pluginsStart] = await core.getStartServices(); const [coreStart, pluginsStart] = await core.getStartServices();
@ -56,6 +66,7 @@ export async function mountApp(
coreStart, coreStart,
pluginsStart.share, pluginsStart.share,
pluginsStart.data, pluginsStart.data,
pluginsStart.spaces pluginsStart.spaces,
deps.usageCollection
); );
} }

View file

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

View file

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

View file

@ -42,10 +42,10 @@ import {
TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart, TriggersAndActionsUIPublicPluginStart,
} from '../../triggers_actions_ui/public'; } from '../../triggers_actions_ui/public';
import { DataVisualizerPluginStart } from '../../data_visualizer/public'; import type { DataVisualizerPluginStart } from '../../data_visualizer/public';
import { PluginSetupContract as AlertingSetup } from '../../alerting/public'; import type { PluginSetupContract as AlertingSetup } from '../../alerting/public';
import { registerManagementSection } from './application/management'; 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 { export interface MlStartDependencies {
data: DataPublicPluginStart; data: DataPublicPluginStart;
@ -127,7 +127,9 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
} }
if (pluginsSetup.management) { if (pluginsSetup.management) {
registerManagementSection(pluginsSetup.management, core).enable(); registerManagementSection(pluginsSetup.management, core, {
usageCollection: pluginsSetup.usageCollection,
}).enable();
} }
const licensing = pluginsSetup.licensing.license$.pipe(take(1)); const licensing = pluginsSetup.licensing.license$.pipe(take(1));