[Data table] Add telemetry for table vis split mode (#88604)

* Add telemetry for table vis

* Update telemetry schema

* Add unit tests

* Update license

* Use soClient instead of esClient, update tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Daniil 2021-01-25 14:11:59 +03:00 committed by GitHub
parent 2ff523556d
commit 72ef3b105a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 398 additions and 56 deletions

View file

@ -5247,6 +5247,36 @@
}
}
},
"vis_type_table": {
"properties": {
"total": {
"type": "long"
},
"total_split": {
"type": "long"
},
"split_columns": {
"properties": {
"total": {
"type": "long"
},
"enabled": {
"type": "long"
}
}
},
"split_rows": {
"properties": {
"total": {
"type": "long"
},
"enabled": {
"type": "long"
}
}
}
}
},
"vis_type_vega": {
"properties": {
"vega_lib_specs_total": {

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
export * from './types';

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
export const VIS_TYPE_TABLE = 'table';
export enum AggTypes {
SUM = 'sum',
AVG = 'avg',
MIN = 'min',
MAX = 'max',
COUNT = 'count',
}
export interface TableVisParams {
perPage: number | '';
showPartialRows: boolean;
showMetricsAtAllLevels: boolean;
showToolbar: boolean;
showTotal: boolean;
totalFunc: AggTypes;
percentageCol: string;
row?: boolean;
}

View file

@ -11,4 +11,5 @@ module.exports = {
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/vis_type_table'],
testRunner: 'jasmine2',
collectCoverageFrom: ['<rootDir>/src/plugins/vis_type_table/**/*.{js,ts,tsx}'],
};

View file

@ -19,7 +19,7 @@ import {
NumberInputOption,
VisOptionsProps,
} from '../../../vis_default_editor/public';
import { TableVisParams } from '../types';
import { TableVisParams } from '../../common';
import { totalAggregations } from './utils';
const { tabifyGetColumns } = search;

View file

@ -9,7 +9,7 @@
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { VisOptionsProps } from 'src/plugins/vis_default_editor/public';
import { TableVisParams } from '../types';
import { TableVisParams } from '../../common';
const TableOptionsComponent = lazy(() => import('./table_vis_options'));

View file

@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import { AggTypes } from '../types';
import { AggTypes } from '../../common';
const totalAggregations = [
{

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public';
import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler';
import { TableVisConfig } from '../types';
import { VIS_TYPE_TABLE } from '../../common';
export type Input = Datatable;
@ -19,7 +20,7 @@ interface Arguments {
export interface TableVisRenderValue {
visData: TableContext;
visType: 'table';
visType: typeof VIS_TYPE_TABLE;
visConfig: TableVisConfig;
}
@ -53,7 +54,7 @@ export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => (
as: 'table_vis',
value: {
visData: convertedData,
visType: 'table',
visType: VIS_TYPE_TABLE,
visConfig,
},
};

View file

@ -12,11 +12,11 @@ import { BaseVisTypeOptions } from '../../../visualizations/public';
import { TableOptions } from '../components/table_vis_options_lazy';
import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public';
import { TableVisParams, VIS_TYPE_TABLE } from '../../common';
import { toExpressionAst } from '../to_ast';
import { TableVisParams } from '../types';
export const tableVisLegacyTypeDefinition: BaseVisTypeOptions<TableVisParams> = {
name: 'table',
name: VIS_TYPE_TABLE,
title: i18n.translate('visTypeTable.tableVisTitle', {
defaultMessage: 'Data table',
}),

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { tableVisResponseHandler, TableContext } from './table_vis_response_handler';
import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { TableVisConfig } from './types';
import { VIS_TYPE_TABLE } from '../common';
export type Input = Datatable;
@ -19,7 +20,7 @@ interface Arguments {
export interface TableVisRenderValue {
visData: TableContext;
visType: 'table';
visType: typeof VIS_TYPE_TABLE;
visConfig: TableVisConfig;
}
@ -56,7 +57,7 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({
as: 'table_vis',
value: {
visData: convertedData,
visType: 'table',
visType: VIS_TYPE_TABLE,
visConfig,
},
};

View file

@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n';
import { AggGroupNames } from '../../data/public';
import { BaseVisTypeOptions } from '../../visualizations/public';
import { TableVisParams, VIS_TYPE_TABLE } from '../common';
import { TableOptions } from './components/table_vis_options_lazy';
import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
import { toExpressionAst } from './to_ast';
import { TableVisParams } from './types';
export const tableVisTypeDefinition: BaseVisTypeOptions<TableVisParams> = {
name: 'table',
name: VIS_TYPE_TABLE,
title: i18n.translate('visTypeTable.tableVisTitle', {
defaultMessage: 'Data table',
}),

View file

@ -8,7 +8,7 @@
import { Vis } from 'src/plugins/visualizations/public';
import { toExpressionAst } from './to_ast';
import { AggTypes, TableVisParams } from './types';
import { AggTypes, TableVisParams } from '../common';
const mockSchemas = {
metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }],

View file

@ -12,8 +12,9 @@ import {
} from '../../data/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public';
import { TableVisParams } from '../common';
import { TableExpressionFunctionDefinition } from './table_vis_fn';
import { TableVisConfig, TableVisParams } from './types';
import { TableVisConfig } from './types';
const buildTableVisConfig = (
schemas: ReturnType<typeof getVisSchemas>,

View file

@ -8,14 +8,7 @@
import { IFieldFormat } from 'src/plugins/data/public';
import { SchemaConfig } from 'src/plugins/visualizations/public';
export enum AggTypes {
SUM = 'sum',
AVG = 'avg',
MIN = 'min',
MAX = 'max',
COUNT = 'count',
}
import { TableVisParams } from '../common';
export interface Dimensions {
buckets: SchemaConfig[];
@ -44,16 +37,6 @@ export interface TableVisUseUiStateProps {
setColumnsWidth: (column: ColumnWidthData) => void;
}
export interface TableVisParams {
perPage: number | '';
showPartialRows: boolean;
showMetricsAtAllLevels: boolean;
showToolbar: boolean;
showTotal: boolean;
totalFunc: AggTypes;
percentageCol: string;
}
export interface TableVisConfig extends TableVisParams {
title: string;
dimensions: Dimensions;

View file

@ -9,8 +9,9 @@
import { useMemo } from 'react';
import { chain, findIndex } from 'lodash';
import { AggTypes } from '../../../common';
import { Table } from '../../table_vis_response_handler';
import { FormattedColumn, TableVisConfig, AggTypes } from '../../types';
import { FormattedColumn, TableVisConfig } from '../../types';
import { getFormatService } from '../../services';
import { addPercentageColumn } from '../add_percentage_column';

View file

@ -7,7 +7,7 @@
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { TableVisParams } from '../../types';
import { TableVisParams } from '../../../common';
export const usePagination = (visParams: TableVisParams, rowCount: number) => {
const [pagination, setPagination] = useState({

View file

@ -6,9 +6,11 @@
* Public License, v 1.
*/
import { PluginConfigDescriptor } from 'kibana/server';
import { CoreSetup, PluginConfigDescriptor } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { configSchema, ConfigSchema } from '../config';
import { registerVisTypeTableUsageCollector } from './usage_collector';
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {
@ -21,6 +23,10 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
};
export const plugin = () => ({
setup() {},
setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) {
if (plugins.usageCollection) {
registerVisTypeTableUsageCollector(plugins.usageCollection);
}
},
start() {},
});

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { SavedObjectsClientContract } from 'kibana/server';
import { getStats } from './get_stats';
const mockVisualizations = {
saved_objects: [
{
attributes: {
visState:
'{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": true }], "params": { "row": true }}',
},
},
{
attributes: {
visState:
'{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": false }], "params": { "row": true }}',
},
},
{
attributes: {
visState:
'{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "split", "enabled": true }], "params": { "row": false }}',
},
},
{
attributes: {
visState: '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }]}',
},
},
{
attributes: { visState: '{"type": "histogram"}' },
},
],
};
describe('vis_type_table getStats', () => {
const mockSoClient = ({
find: jest.fn().mockResolvedValue(mockVisualizations),
} as unknown) as SavedObjectsClientContract;
test('Returns stats from saved objects for table vis only', async () => {
const result = await getStats(mockSoClient);
expect(mockSoClient.find).toHaveBeenCalledWith({
type: 'visualization',
perPage: 10000,
});
expect(result).toEqual({
total: 4,
total_split: 3,
split_columns: {
total: 1,
enabled: 1,
},
split_rows: {
total: 2,
enabled: 1,
},
});
});
});

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server';
import {
SavedVisState,
VisualizationSavedObjectAttributes,
} from 'src/plugins/visualizations/common';
import { TableVisParams, VIS_TYPE_TABLE } from '../../common';
export interface VisTypeTableUsage {
/**
* Total number of table type visualizations
*/
total: number;
/**
* Total number of table visualizations, using "Split table" agg
*/
total_split: number;
/**
* Split table by columns stats
*/
split_columns: {
total: number;
enabled: number;
};
/**
* Split table by rows stats
*/
split_rows: {
total: number;
enabled: number;
};
}
/*
* Parse the response data into telemetry payload
*/
export async function getStats(
soClient: SavedObjectsClientContract | ISavedObjectsRepository
): Promise<VisTypeTableUsage | undefined> {
const visualizations = await soClient.find<VisualizationSavedObjectAttributes>({
type: 'visualization',
perPage: 10000,
});
const tableVisualizations = visualizations.saved_objects
.map<SavedVisState<TableVisParams>>(({ attributes }) => JSON.parse(attributes.visState))
.filter(({ type }) => type === VIS_TYPE_TABLE);
const defaultStats = {
total: tableVisualizations.length,
total_split: 0,
split_columns: {
total: 0,
enabled: 0,
},
split_rows: {
total: 0,
enabled: 0,
},
};
return tableVisualizations.reduce((acc, { aggs, params }) => {
const hasSplitAgg = aggs.find((agg) => agg.schema === 'split');
if (hasSplitAgg) {
acc.total_split += 1;
const isSplitRow = params.row;
const isSplitEnabled = hasSplitAgg.enabled;
const container = isSplitRow ? acc.split_rows : acc.split_columns;
container.total += 1;
container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled;
}
return acc;
}, defaultStats);
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
export { registerVisTypeTableUsageCollector } from './register_usage_collector';

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
jest.mock('./get_stats', () => ({
getStats: jest.fn().mockResolvedValue({ somestat: 1 }),
}));
import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock';
import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks';
import { registerVisTypeTableUsageCollector } from './register_usage_collector';
import { getStats } from './get_stats';
describe('registerVisTypeTableUsageCollector', () => {
it('Usage collector configs fit the shape', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisTypeTableUsageCollector(mockCollectorSet);
expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
type: 'vis_type_table',
isReady: expect.any(Function),
fetch: expect.any(Function),
schema: {
total: { type: 'long' },
total_split: { type: 'long' },
split_columns: {
total: { type: 'long' },
enabled: { type: 'long' },
},
split_rows: {
total: { type: 'long' },
enabled: { type: 'long' },
},
},
});
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('Usage collector config.fetch calls getStats', async () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisTypeTableUsageCollector(mockCollectorSet);
const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value;
const mockCollectorFetchContext = createCollectorFetchContextMock();
const fetchResult = await usageCollector.fetch(mockCollectorFetchContext);
expect(getStats).toBeCalledTimes(1);
expect(getStats).toBeCalledWith(mockCollectorFetchContext.soClient);
expect(fetchResult).toEqual({ somestat: 1 });
});
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getStats, VisTypeTableUsage } from './get_stats';
export function registerVisTypeTableUsageCollector(collectorSet: UsageCollectionSetup) {
const collector = collectorSet.makeUsageCollector<VisTypeTableUsage | undefined>({
type: 'vis_type_table',
isReady: () => true,
schema: {
total: { type: 'long' },
total_split: { type: 'long' },
split_columns: {
total: { type: 'long' },
enabled: { type: 'long' },
},
split_rows: {
total: { type: 'long' },
enabled: { type: 'long' },
},
},
fetch: ({ soClient }) => getStats(soClient),
});
collectorSet.registerCollector(collector);
}

View file

@ -8,6 +8,7 @@
"declarationMap": true
},
"include": [
"common/**/*",
"public/**/*",
"server/**/*",
"*.ts"

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
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
/** @public types */
export * from './types';

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { SavedObjectAttributes } from 'kibana/server';
import { AggConfigOptions } from 'src/plugins/data/common';
export interface VisParams {
[key: string]: any;
}
export interface SavedVisState<TVisParams = VisParams> {
title: string;
type: string;
params: TVisParams;
aggs: AggConfigOptions[];
}
export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes {
description: string;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
title: string;
version: number;
visState: string;
uiStateJSON: string;
}

View file

@ -34,7 +34,7 @@ export type {
Schema,
ISchemas,
} from './vis_types';
export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis';
export { SerializedVis, SerializedVisData, VisData } from './vis';
export type VisualizeEmbeddableFactoryContract = PublicContract<VisualizeEmbeddableFactory>;
export type VisualizeEmbeddableContract = PublicContract<VisualizeEmbeddable>;
export { VisualizeInput } from './embeddable';
@ -46,12 +46,13 @@ export { PersistedState } from './persisted_state';
export {
VisualizationControllerConstructor,
VisualizationController,
SavedVisState,
ISavedVis,
VisSavedObject,
VisResponseValue,
VisToExpressionAst,
VisParams,
} from './types';
export { ExprVisAPIEvents } from './expressions/vis';
export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry';
export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
export { SavedVisState } from '../common';

View file

@ -6,7 +6,7 @@
* Public License, v 1.
*/
import { SavedVisState } from '../types';
import { SavedVisState } from '../../common';
declare function updateOldState(oldState: unknown): SavedVisState;

View file

@ -7,7 +7,8 @@
*/
import { extractReferences, injectReferences } from './saved_visualization_references';
import { VisSavedObject, SavedVisState } from '../types';
import { VisSavedObject } from '../types';
import { SavedVisState } from '../../common';
describe('extractReferences', () => {
test('extracts nothing if savedSearchId is empty', () => {

View file

@ -7,15 +7,12 @@
*/
import { SavedObject } from '../../../plugins/saved_objects/public';
import {
AggConfigOptions,
SearchSourceFields,
TimefilterContract,
} from '../../../plugins/data/public';
import { SearchSourceFields, TimefilterContract } from '../../../plugins/data/public';
import { ExpressionAstExpression } from '../../expressions/public';
import { SerializedVis, Vis, VisParams } from './vis';
import { SerializedVis, Vis } from './vis';
import { ExprVis } from './expressions/vis';
import { SavedVisState, VisParams } from '../common/types';
export { Vis, SerializedVis, VisParams };
@ -30,13 +27,6 @@ export type VisualizationControllerConstructor = new (
vis: ExprVis
) => VisualizationController;
export interface SavedVisState {
title: string;
type: string;
params: VisParams;
aggs: AggConfigOptions[];
}
export interface ISavedVis {
id?: string;
title: string;

View file

@ -30,6 +30,7 @@ import {
AggConfigOptions,
SearchSourceFields,
} from '../../../plugins/data/public';
import { VisParams } from '../common/types';
export interface SerializedVisData {
expression?: string;
@ -56,10 +57,6 @@ export interface VisData {
savedSearchId?: string;
}
export interface VisParams {
[key: string]: any;
}
const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => {
const searchSource = inputSearchSource.createCopy();
if (savedSearchId) {