[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 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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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