From 2981333fecaae7352d5054b34e77c0f8c73df6a1 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 27 Apr 2020 14:24:34 -0700 Subject: [PATCH] [Reporting/telemetry] Add 'statuses' object to usage to show status counts by jobType & appType (#63922) (#64551) * [Reporting] Additional status by app data for usage * --wip-- [skip ci] * clean up types * add a prettier-ignore * fix types * --wip-- [skip ci] * fix typo * more tests * Tweak the data model * fix the comments and type keys to reflect the data model Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../reporting_usage_collector.test.ts.snap | 343 ++++++++++++++++++ .../server/usage/decorate_range_stats.ts | 4 +- .../server/usage/get_reporting_usage.ts | 75 +++- .../usage/reporting_usage_collector.test.ts | 318 +++++++--------- .../plugins/reporting/server/usage/types.d.ts | 75 +++- 5 files changed, 581 insertions(+), 234 deletions(-) create mode 100644 x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap diff --git a/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap new file mode 100644 index 0000000000000..aa22c3f66df18 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`data modeling with empty data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 0, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, +} +`; + +exports[`data modeling with normal looking usage data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 3, + }, + "_all": 12, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 0, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 1, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "completed_with_warnings": 1, + "failed": 0, + }, + "statuses": Object { + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 1, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "completed_with_warnings": 1, + "failed": 0, + }, + "statuses": Object { + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 6, + "dashboard": 0, + "visualization": 3, + }, + "available": true, + "layout": Object { + "preserve_layout": 9, + "print": 0, + }, + "total": 9, + }, + "status": Object { + "completed": 10, + "completed_with_warnings": 1, + "failed": 1, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "visualization": 1, + }, + "printable_pdf": Object { + "canvas workpad": 6, + "visualization": 3, + }, + }, + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + "failed": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, +} +`; + +exports[`data modeling with sparse data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 1, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "csv": Object { + "available": true, + "total": 1, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "csv": Object { + "available": true, + "total": 1, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, +} +`; diff --git a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts b/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts index 359bcc45230c3..ef985d2dd1cf3 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts @@ -6,7 +6,7 @@ import { uniq } from 'lodash'; import { CSV_JOB_TYPE, PDF_JOB_TYPE, PNG_JOB_TYPE } from '../../common/constants'; -import { AvailableTotal, FeatureAvailabilityMap, RangeStats, ExportType } from './types'; +import { AvailableTotal, ExportType, FeatureAvailabilityMap, RangeStats } from './types'; function getForFeature( range: Partial, @@ -47,6 +47,7 @@ export const decorateRangeStats = ( const { _all: rangeAll, status: rangeStatus, + statuses: rangeStatusByApp, [PDF_JOB_TYPE]: rangeStatsPdf, ...rangeStatsBasic } = rangeStats; @@ -73,6 +74,7 @@ export const decorateRangeStats = ( const resultStats = { _all: rangeAll || 0, status: { completed: 0, failed: 0, ...rangeStatus }, + statuses: rangeStatusByApp, ...rangePdf, ...rangeBasic, } as RangeStats; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index e9523d9e70202..2c3bb8f4bf71c 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -11,13 +11,13 @@ import { ReportingConfig } from '../types'; import { decorateRangeStats } from './decorate_range_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import { - AggregationBuckets, - AggregationResults, + AggregationResultBuckets, FeatureAvailabilityMap, JobTypes, KeyCountBucket, - RangeAggregationResults, RangeStats, + SearchResponse, + StatusByAppBucket, } from './types'; type XPackInfo = XPackMainPlugin['info']; @@ -29,6 +29,7 @@ const LAYOUT_TYPES_FIELD = 'meta.layout.keyword'; const OBJECT_TYPES_KEY = 'objectTypes'; const OBJECT_TYPES_FIELD = 'meta.objectType.keyword'; const STATUS_TYPES_KEY = 'statusTypes'; +const STATUS_BY_APP_KEY = 'statusByApp'; const STATUS_TYPES_FIELD = 'status'; const DEFAULT_TERMS_SIZE = 10; @@ -38,16 +39,30 @@ const PRINTABLE_PDF_JOBTYPE = 'printable_pdf'; const getKeyCount = (buckets: KeyCountBucket[]): { [key: string]: number } => buckets.reduce((accum, { key, doc_count: count }) => ({ ...accum, [key]: count }), {}); -function getAggStats(aggs: AggregationResults) { - const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY] as AggregationBuckets; - const jobTypes: JobTypes = jobBuckets.reduce( +// indexes some key/count buckets by statusType > jobType > appName: statusCount +const getAppStatuses = (buckets: StatusByAppBucket[]) => + buckets.reduce((statuses, statusBucket) => { + return { + ...statuses, + [statusBucket.key]: statusBucket.jobTypes.buckets.reduce((jobTypes, job) => { + return { + ...jobTypes, + [job.key]: job.appNames.buckets.reduce((apps, app) => { + return { + ...apps, + [app.key]: app.doc_count, + }; + }, {}), + }; + }, {}), + }; + }, {}); + +function getAggStats(aggs: AggregationResultBuckets): RangeStats { + const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY]; + const jobTypes = jobBuckets.reduce( (accum: JobTypes, { key, doc_count: count }: { key: string; doc_count: number }) => { - return { - ...accum, - [key]: { - total: count, - }, - }; + return { ...accum, [key]: { total: count } }; }, {} as JobTypes ); @@ -55,8 +70,8 @@ function getAggStats(aggs: AggregationResults) { // merge pdf stats into pdf jobtype key const pdfJobs = jobTypes[PRINTABLE_PDF_JOBTYPE]; if (pdfJobs) { - const pdfAppBuckets = get(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); - const pdfLayoutBuckets = get(aggs[LAYOUT_TYPES_KEY], '.pdf.buckets', []); + const pdfAppBuckets = get(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); + const pdfLayoutBuckets = get(aggs[LAYOUT_TYPES_KEY], '.pdf.buckets', []); pdfJobs.app = getKeyCount(pdfAppBuckets) as { visualization: number; dashboard: number; @@ -69,26 +84,35 @@ function getAggStats(aggs: AggregationResults) { const all = aggs.doc_count as number; let statusTypes = {}; - const statusBuckets = get(aggs[STATUS_TYPES_KEY], 'buckets', []); + const statusBuckets = get(aggs[STATUS_TYPES_KEY], 'buckets', []); if (statusBuckets) { statusTypes = getKeyCount(statusBuckets); } - return { _all: all, status: statusTypes, ...jobTypes }; + let statusByApp = {}; + const statusAppBuckets = get(aggs[STATUS_BY_APP_KEY], 'buckets', []); + if (statusAppBuckets) { + statusByApp = getAppStatuses(statusAppBuckets); + } + + return { _all: all, status: statusTypes, statuses: statusByApp, ...jobTypes }; } +type SearchAggregation = SearchResponse['aggregations']['ranges']['buckets']; + type RangeStatSets = Partial< RangeStats & { lastDay: RangeStats; last7Days: RangeStats; } >; -async function handleResponse(response: AggregationResults): Promise { - const buckets = get(response, 'aggregations.ranges.buckets'); + +async function handleResponse(response: SearchResponse): Promise { + const buckets = get(response, 'aggregations.ranges.buckets'); if (!buckets) { return {}; } - const { lastDay, last7Days, all } = buckets as RangeAggregationResults; + const { lastDay, last7Days, all } = buckets; const lastDayUsage = lastDay ? getAggStats(lastDay) : ({} as RangeStats); const last7DaysUsage = last7Days ? getAggStats(last7Days) : ({} as RangeStats); @@ -126,6 +150,17 @@ export async function getReportingUsage( aggs: { [JOB_TYPES_KEY]: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, [STATUS_TYPES_KEY]: { terms: { field: STATUS_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, + [STATUS_BY_APP_KEY]: { + terms: { field: 'status', size: DEFAULT_TERMS_SIZE }, + aggs: { + jobTypes: { + terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, + aggs: { + appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, // NOTE Discover/CSV export is missing the 'meta.objectType' field, so Discover/CSV results are missing for this agg + }, + }, + }, + }, [OBJECT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, aggs: { pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, @@ -141,7 +176,7 @@ export async function getReportingUsage( }; return callCluster('search', params) - .then((response: AggregationResults) => handleResponse(response)) + .then((response: SearchResponse) => handleResponse(response)) .then((usage: RangeStatSets) => { // Allow this to explicitly throw an exception if/when this config is deprecated, // because we shouldn't collect browserType in that case! diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts index dbc674ce36ec8..61b736a3e4d8c 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -12,6 +12,7 @@ import { getReportingUsageCollector, } from './reporting_usage_collector'; import { ReportingConfig } from '../types'; +import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -61,7 +62,7 @@ const getMockReportingConfig = () => ({ get: () => {}, kbnConfig: { get: () => '' }, }); -const getResponseMock = (customization = {}) => customization; +const getResponseMock = (base = {}) => base; describe('license checks', () => { let mockConfig: ReportingConfig; @@ -206,212 +207,143 @@ describe('data modeling', () => { ranges: { buckets: { all: { - doc_count: 54, - layoutTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'preserve_layout', doc_count: 13 }, - { key: 'print', doc_count: 10 }, - ], - }, - }, - objectTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 23 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'pending', doc_count: 33 }, - { key: 'completed', doc_count: 20 }, - { key: 'processing', doc_count: 1 }, - ], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 27 }, - { key: 'printable_pdf', doc_count: 23 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - lastDay: { - doc_count: 11, - layoutTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'print', doc_count: 2 }], - }, - }, - objectTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 2 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 11 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 5 }, - { key: 'PNG', doc_count: 4 }, - { key: 'printable_pdf', doc_count: 2 }, - ], - }, + doc_count: 12, + jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, + layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, + objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, + statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, + statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, }, last7Days: { - doc_count: 27, - layoutTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'print', doc_count: 10 }, - { key: 'preserve_layout', doc_count: 3 }, - ], - }, - }, - objectTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 13 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 27 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'printable_pdf', doc_count: 13 }, - { key: 'csv', doc_count: 10 }, - { key: 'PNG', doc_count: 4 }, - ], - }, + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + }, + lastDay: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, }, }, }, }, - }) + } as SearchResponse) // prettier-ignore ) ); const usageStats = await fetch(callClusterMock as any); - expect(usageStats).toMatchInlineSnapshot(` - Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 54, - "available": true, - "browser_type": undefined, - "csv": Object { - "available": true, - "total": 27, - }, - "enabled": true, - "last7Days": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 27, - "csv": Object { - "available": true, - "total": 10, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 13, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 3, - "print": 10, + expect(usageStats).toMatchSnapshot(); + }); + + test('with sparse data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + last7Days: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + lastDay: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + }, }, - "total": 13, }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 27, - }, - }, - "lastDay": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 11, - "csv": Object { - "available": true, - "total": 5, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 2, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 0, - "print": 2, + } as SearchResponse) // prettier-ignore + ) + ); + + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchSnapshot(); + }); + + test('with empty data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + last7Days: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + lastDay: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + }, }, - "total": 2, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 11, - }, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 23, - "visualization": 0, }, - "available": true, - "layout": Object { - "preserve_layout": 13, - "print": 10, - }, - "total": 23, - }, - "status": Object { - "completed": 20, - "failed": 0, - "pending": 33, - "processing": 1, - }, - } - `); + } as SearchResponse) + ) + ); + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts index 98e025ccf661e..83f1701863355 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts @@ -4,15 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AvailableTotal { - available: boolean; - total: number; -} - -interface StatusCounts { - [statusType: string]: number; -} - export interface KeyCountBucket { key: string; doc_count: number; @@ -20,22 +11,53 @@ export interface KeyCountBucket { export interface AggregationBuckets { buckets: KeyCountBucket[]; - pdf?: { - buckets: KeyCountBucket[]; - }; } -/* - * Mapped Types and Intersection Types - */ +export interface StatusByAppBucket { + key: string; + doc_count: number; + jobTypes: { + buckets: Array<{ + doc_count: number; + key: string; + appNames: AggregationBuckets; + }>; + }; +} -type AggregationKeys = 'jobTypes' | 'layoutTypes' | 'objectTypes' | 'statusTypes'; -export type AggregationResults = { [K in AggregationKeys]: AggregationBuckets } & { +export interface AggregationResultBuckets { + jobTypes: AggregationBuckets; + layoutTypes: { + doc_count: number; + pdf: AggregationBuckets; + }; + objectTypes: { + doc_count: number; + pdf: AggregationBuckets; + }; + statusTypes: AggregationBuckets; + statusByApp: { + buckets: StatusByAppBucket[]; + }; doc_count: number; -}; +} -type RangeAggregationKeys = 'all' | 'lastDay' | 'last7Days'; -export type RangeAggregationResults = { [K in RangeAggregationKeys]?: AggregationResults }; +export interface SearchResponse { + aggregations: { + ranges: { + buckets: { + all: AggregationResultBuckets; + last7Days: AggregationResultBuckets; + lastDay: AggregationResultBuckets; + }; + }; + }; +} + +export interface AvailableTotal { + available: boolean; + total: number; +} type BaseJobTypeKeys = 'csv' | 'PNG'; export type JobTypes = { [K in BaseJobTypeKeys]: AvailableTotal } & { @@ -51,9 +73,22 @@ export type JobTypes = { [K in BaseJobTypeKeys]: AvailableTotal } & { }; }; +interface StatusCounts { + [statusType: string]: number; +} + +interface StatusByAppCounts { + [statusType: string]: { + [jobType: string]: { + [appName: string]: number; + }; + }; +} + export type RangeStats = JobTypes & { _all: number; status: StatusCounts; + statuses: StatusByAppCounts; }; export type ExportType = 'csv' | 'printable_pdf' | 'PNG';