[Step 1 ] VisEditors Telemetry enhancements (remove legacy agg-based telemetries) (#135634)

* [Step 1 ] VisEditors Telemetry enhancements (remove legacy agg-based telemetries)

* remove legacy trackUiMetric telemetries

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2022-07-25 13:07:34 +03:00 committed by GitHub
parent e33f4b0f80
commit ded1fcb279
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 10 additions and 1886 deletions

View file

@ -10112,122 +10112,6 @@
}
}
}
},
"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_timeseries": {
"properties": {
"timeseries_use_last_value_mode_total": {
"type": "long",
"_meta": {
"description": "Number of TSVB visualizations using \"last value\" as a time range"
}
},
"timeseries_use_es_indices_total": {
"type": "long",
"_meta": {
"description": "Number of TSVB visualizations using elasticsearch indices"
}
},
"timeseries_table_use_aggregate_function": {
"type": "long",
"_meta": {
"description": "Number of TSVB table visualizations using aggregate function"
}
},
"timeseries_types": {
"properties": {
"table": {
"type": "long"
},
"gauge": {
"type": "long"
},
"markdown": {
"type": "long"
},
"top_n": {
"type": "long"
},
"timeseries": {
"type": "long"
},
"metric": {
"type": "long"
}
}
}
}
},
"vis_type_vega": {
"properties": {
"vega_lib_specs_total": {
"type": "long"
},
"vega_lite_lib_specs_total": {
"type": "long"
},
"vega_use_map_total": {
"type": "long"
}
}
},
"visualization_types": {
"properties": {
"DYNAMIC_KEY": {
"properties": {
"total": {
"type": "long"
},
"spaces_min": {
"type": "long"
},
"spaces_max": {
"type": "long"
},
"spaces_avg": {
"type": "long"
},
"saved_7_days_total": {
"type": "long"
},
"saved_30_days_total": {
"type": "long"
},
"saved_90_days_total": {
"type": "long"
}
}
}
}
}
}
}

View file

@ -13,13 +13,12 @@ import { HeatmapVisParams, HeatmapTypeProps } from '../../types';
const HeatmapOptionsLazy = lazy(() => import('./heatmap'));
export const getHeatmapOptions =
({ showElasticChartsOptions, palettes, trackUiMetric }: HeatmapTypeProps) =>
({ showElasticChartsOptions, palettes }: HeatmapTypeProps) =>
(props: VisEditorOptionsProps<HeatmapVisParams>) =>
(
<HeatmapOptionsLazy
{...props}
palettes={palettes}
showElasticChartsOptions={showElasticChartsOptions}
trackUiMetric={trackUiMetric}
/>
);

View file

@ -34,16 +34,10 @@ export class VisTypeHeatmapPlugin {
{ visualizations, charts, usageCollection }: VisTypeHeatmapSetupDependencies
) {
if (!core.uiSettings.get(LEGACY_HEATMAP_CHARTS_LIBRARY)) {
const trackUiMetric = usageCollection?.reportUiCounter.bind(
usageCollection,
'vis_type_heatmap'
);
visualizations.createBaseVisualization(
heatmapVisType({
showElasticChartsOptions: true,
palettes: charts.palettes,
trackUiMetric,
})
);
}

View file

@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { UiCounterMetricType } from '@kbn/analytics';
import type { Position } from '@elastic/charts';
import type { ChartsPluginSetup, Style, Labels, ColorSchemas } from '@kbn/charts-plugin/public';
import { Range } from '@kbn/expressions-plugin/public';
@ -14,7 +13,6 @@ import { LegendSize } from '@kbn/visualizations-plugin/public';
export interface HeatmapTypeProps {
showElasticChartsOptions?: boolean;
palettes?: ChartsPluginSetup['palettes'];
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
export interface HeatmapVisParams {

View file

@ -20,7 +20,6 @@ import { SplitTooltip } from './split_tooltip';
export const getHeatmapVisTypeDefinition = ({
showElasticChartsOptions = false,
palettes,
trackUiMetric,
}: HeatmapTypeProps): VisTypeDefinition<HeatmapVisParams> => ({
name: 'heatmap',
title: i18n.translate('visTypeHeatmap.heatmap.heatmapTitle', { defaultMessage: 'Heat map' }),
@ -68,7 +67,6 @@ export const getHeatmapVisTypeDefinition = ({
optionsTemplate: getHeatmapOptions({
showElasticChartsOptions,
palettes,
trackUiMetric,
}),
schemas: [
{

View file

@ -14,13 +14,12 @@ import { PieTypeProps } from '../../types';
const PieOptionsLazy = lazy(() => import('./pie'));
export const getPieOptions =
({ showElasticChartsOptions, palettes, trackUiMetric }: PieTypeProps) =>
({ showElasticChartsOptions, palettes }: PieTypeProps) =>
(props: VisEditorOptionsProps<PartitionVisParams>) =>
(
<PieOptionsLazy
{...props}
palettes={palettes}
showElasticChartsOptions={showElasticChartsOptions}
trackUiMetric={trackUiMetric}
/>
);

View file

@ -7,7 +7,6 @@
*/
import React, { useState, useEffect, useCallback } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import {
EuiPanel,
EuiTitle,
@ -223,9 +222,6 @@ const PieOptions = (props: PieOptionsProps) => {
value={stateParams.nestedLegend}
disabled={stateParams.legendDisplay === LegendDisplay.HIDE}
setValue={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'nested_legend_switched');
}
setValue(paramName, value);
}}
data-test-subj="visTypePieNestedLegendSwitch"
@ -253,9 +249,6 @@ const PieOptions = (props: PieOptionsProps) => {
activePalette={stateParams.palette}
paramName="palette"
setPalette={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'palette_selected');
}
setValue(paramName, value);
}}
/>
@ -296,9 +289,6 @@ const PieOptions = (props: PieOptionsProps) => {
: stateParams.labels.position || LabelPositions.DEFAULT
}
setValue={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'label_position_selected');
}
setLabels(paramName, value);
}}
data-test-subj="visTypePieLabelPositionSelect"
@ -338,9 +328,6 @@ const PieOptions = (props: PieOptionsProps) => {
paramName="valuesFormat"
value={stateParams.labels.valuesFormat || ValueFormats.PERCENT}
setValue={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'values_format_selected');
}
setLabels(paramName, value);
}}
data-test-subj="visTypePieValueFormatsSelect"

View file

@ -43,12 +43,10 @@ export class VisTypePiePlugin {
{ visualizations, charts, usageCollection }: VisTypePieSetupDependencies
) {
if (!core.uiSettings.get(LEGACY_PIE_CHARTS_LIBRARY, false)) {
const trackUiMetric = usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_pie');
visualizations.createBaseVisualization(
pieVisType({
showElasticChartsOptions: true,
palettes: charts.palettes,
trackUiMetric,
})
);
}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { UiCounterMetricType } from '@kbn/analytics';
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
@ -28,5 +27,4 @@ export interface Dimensions {
export interface PieTypeProps {
showElasticChartsOptions?: boolean;
palettes?: ChartsPluginSetup['palettes'];
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}

View file

@ -25,7 +25,6 @@ import { getPieOptions } from '../editor/components';
export const getPieVisTypeDefinition = ({
showElasticChartsOptions = false,
palettes,
trackUiMetric,
}: PieTypeProps): VisTypeDefinition<PartitionVisParams> => ({
name: 'pie',
title: i18n.translate('visTypePie.pie.pieTitle', { defaultMessage: 'Pie' }),
@ -68,7 +67,6 @@ export const getPieVisTypeDefinition = ({
optionsTemplate: getPieOptions({
showElasticChartsOptions,
palettes,
trackUiMetric,
}),
schemas: [
{

View file

@ -14,7 +14,6 @@
"share",
"visDefaultEditor"
],
"optionalPlugins": ["usageCollection"],
"owner": {
"name": "Vis Editors",
"githubTeam": "kibana-vis-editors"

View file

@ -6,21 +6,14 @@
* Side Public License, v 1.
*/
import { CoreSetup, PluginConfigDescriptor } from '@kbn/core/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { PluginConfigDescriptor } from '@kbn/core/server';
import { configSchema, ConfigSchema } from '../config';
import { registerVisTypeTableUsageCollector } from './usage_collector';
export const config: PluginConfigDescriptor<ConfigSchema> = {
schema: configSchema,
};
export const plugin = () => ({
setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) {
if (plugins.usageCollection) {
registerVisTypeTableUsageCollector(plugins.usageCollection);
}
},
setup() {},
start() {},
});

View file

@ -1,75 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getStats } from './get_stats';
import type { SavedObjectsClientContract } from '@kbn/core/server';
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 = {
createPointInTimeFinder: jest.fn().mockResolvedValue({
close: jest.fn(),
find: function* asyncGenerator() {
yield mockVisualizations;
},
}),
} as unknown as SavedObjectsClientContract;
test('Returns stats from saved objects for table vis only', async () => {
const result = await getStats(mockSoClient);
expect(mockSoClient.createPointInTimeFinder).toHaveBeenCalledWith({
type: 'visualization',
perPage: 1000,
namespaces: ['*'],
});
expect(result).toEqual({
total: 4,
total_split: 3,
split_columns: {
total: 1,
enabled: 1,
},
split_rows: {
total: 2,
enabled: 1,
},
});
});
});

View file

@ -1,102 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
ISavedObjectsRepository,
SavedObjectsClientContract,
SavedObjectsFindResult,
} from '@kbn/core/server';
import type { SavedVisState } from '@kbn/visualizations-plugin/common';
import { 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 finder = await soClient.createPointInTimeFinder({
type: 'visualization',
perPage: 1000,
namespaces: ['*'],
});
const stats: VisTypeTableUsage = {
total: 0,
total_split: 0,
split_columns: {
total: 0,
enabled: 0,
},
split_rows: {
total: 0,
enabled: 0,
},
};
const doTelemetry = ({ aggs, params }: SavedVisState) => {
stats.total += 1;
const hasSplitAgg = aggs.find((agg) => agg.schema === 'split');
if (hasSplitAgg) {
stats.total_split += 1;
const isSplitRow = params.row;
const isSplitEnabled = hasSplitAgg.enabled;
const container = isSplitRow ? stats.split_rows : stats.split_columns;
container.total += 1;
container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled;
}
};
for await (const response of finder.find()) {
(response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult<any>) => {
if (attributes?.visState) {
try {
const visState: SavedVisState = JSON.parse(attributes.visState);
if (visState.type === VIS_TYPE_TABLE) {
doTelemetry(visState);
}
} catch {
// nothing to be here, "so" not valid
}
}
});
}
await finder.close();
return stats;
}

View file

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

View file

@ -1,57 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
createUsageCollectionSetupMock,
createCollectorFetchContextMock,
} from '@kbn/usage-collection-plugin/server/mocks';
import { registerVisTypeTableUsageCollector } from './register_usage_collector';
import { getStats } from './get_stats';
jest.mock('./get_stats', () => ({
getStats: jest.fn().mockResolvedValue({ somestat: 1 }),
}));
describe('registerVisTypeTableUsageCollector', () => {
test('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);
});
test('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

@ -1,31 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/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

@ -17,7 +17,6 @@
{ "path": "../../data/tsconfig.json" },
{ "path": "../../visualizations/tsconfig.json" },
{ "path": "../../share/tsconfig.json" },
{ "path": "../../usage_collection/tsconfig.json" },
{ "path": "../../expressions/tsconfig.json" },
{ "path": "../../kibana_utils/tsconfig.json" },
{ "path": "../../kibana_react/tsconfig.json" },

View file

@ -5,7 +5,7 @@
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "inspector", "dataViews"],
"optionalPlugins": ["home","usageCollection"],
"optionalPlugins": ["home"],
"requiredBundles": ["unifiedSearch", "kibanaUtils", "kibanaReact", "fieldFormats"],
"owner": {
"name": "Vis Editors",

View file

@ -18,7 +18,6 @@ import {
import { firstValueFrom, Observable } from 'rxjs';
import { Server } from '@hapi/hapi';
import { map } from 'rxjs/operators';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import { PluginStart } from '@kbn/data-plugin/server';
import type { DataViewsService } from '@kbn/data-views-plugin/common';
@ -41,14 +40,11 @@ import {
} from './lib/search_strategies';
import type { TimeseriesVisData, VisPayload } from '../common/types';
import { registerTimeseriesUsageCollector } from './usage_collector';
export interface LegacySetup {
server: Server;
}
interface VisTypeTimeseriesPluginSetupDependencies {
usageCollection?: UsageCollectionSetup;
home?: HomeServerPluginSetup;
}
@ -128,10 +124,6 @@ export class VisTypeTimeseriesPlugin implements Plugin<VisTypeTimeseriesSetup> {
visDataRoutes(router, framework);
fieldsRoutes(router, framework);
if (plugins.usageCollection) {
registerTimeseriesUsageCollector(plugins.usageCollection, plugins.home);
}
return {
getVisData: async (
requestContext: VisTypeTimeseriesRequestHandlerContext,

View file

@ -1,14 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const mockStats = { somestat: 1 };
export const mockGetStats = jest.fn().mockResolvedValue(mockStats);
jest.doMock('./get_usage_collector', () => ({
getStats: mockGetStats,
}));

View file

@ -1,264 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getStats } from './get_usage_collector';
import { createCollectorFetchContextMock } from '@kbn/usage-collection-plugin/server/mocks';
import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server';
import { TIME_RANGE_DATA_MODES } from '../../common/enums';
const mockedSavedObject = {
saved_objects: [
{
attributes: {
visState: JSON.stringify({
type: 'metrics',
title: 'TSVB visualization 1',
params: {
type: 'gauge',
time_range_mode: TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE,
use_kibana_indexes: true,
},
}),
},
},
{
attributes: {
visState: JSON.stringify({
type: 'metrics',
title: 'TSVB visualization 2',
params: {
type: 'top_n',
time_range_mode: TIME_RANGE_DATA_MODES.LAST_VALUE,
use_kibana_indexes: false,
},
}),
},
},
{
attributes: {
visState: JSON.stringify({
type: 'metrics',
title: 'TSVB visualization 3',
params: {
type: 'markdown',
time_range_mode: undefined,
use_kibana_indexes: false,
},
}),
},
},
{
attributes: {
visState: JSON.stringify({
type: 'metrics',
title: 'TSVB visualization 4',
params: {
type: 'table',
series: [
{
aggregate_by: 'test',
aggregate_function: 'max',
},
],
},
}),
},
},
],
} as SavedObjectsFindResponse;
const mockedSavedObjectsByValue = [
{
attributes: {
panelsJSON: JSON.stringify({
type: 'visualization',
embeddableConfig: {
savedVis: {
type: 'metrics',
params: {
type: 'markdown',
time_range_mode: TIME_RANGE_DATA_MODES.LAST_VALUE,
use_kibana_indexes: false,
},
},
},
}),
},
},
{
attributes: {
panelsJSON: JSON.stringify({
type: 'visualization',
embeddableConfig: {
savedVis: {
type: 'metrics',
params: {
type: 'timeseries',
time_range_mode: TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE,
use_kibana_indexes: true,
},
},
},
}),
},
},
{
attributes: {
panelsJSON: JSON.stringify({
type: 'visualization',
embeddableConfig: {
savedVis: {
type: 'metrics',
params: {
type: 'table',
series: [
{
aggregate_by: 'test1',
aggregate_function: 'sum',
},
],
use_kibana_indexes: true,
},
},
},
}),
},
},
];
const getMockCollectorFetchContext = (
savedObjects: SavedObjectsFindResponse,
savedObjectsByValue: unknown[] = []
) => {
const fetchParamsMock = createCollectorFetchContextMock();
fetchParamsMock.soClient = {
find: jest.fn().mockResolvedValue({
saved_objects: savedObjectsByValue,
}),
createPointInTimeFinder: jest.fn().mockResolvedValue({
close: jest.fn(),
find: function* asyncGenerator() {
yield savedObjects;
},
}),
} as unknown as SavedObjectsClientContract;
return fetchParamsMock;
};
describe('Timeseries visualization usage collector', () => {
test('Returns undefined when no results found (undefined)', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext(
{ saved_objects: [] } as unknown as SavedObjectsFindResponse,
[]
);
const result = await getStats(mockCollectorFetchContext.soClient);
expect(result).toBeUndefined();
});
test('Returns undefined when no timeseries saved objects found', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext({
saved_objects: [
{
attributes: { visState: '{"type": "area"}' },
},
{
attributes: {
panelsJSON: JSON.stringify({
type: 'visualization',
embeddableConfig: {
savedVis: {
type: 'area',
},
},
}),
},
},
],
} as SavedObjectsFindResponse);
const result = await getStats(mockCollectorFetchContext.soClient);
expect(result).toBeUndefined();
});
test('Returns undefined when aggregate function is null', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext({
saved_objects: [
{
attributes: {
panelsJSON: JSON.stringify({
type: 'visualization',
embeddableConfig: {
savedVis: {
type: 'metrics',
params: {
type: 'table',
series: [
{
aggregate_by: null,
aggregate_function: null,
},
],
},
},
},
}),
},
},
{
attributes: {
panelsJSON: JSON.stringify({
type: 'visualization',
embeddableConfig: {
savedVis: {
type: 'metrics',
params: {
type: 'table',
series: [
{
axis_position: 'right',
},
],
},
},
},
}),
},
},
],
} as SavedObjectsFindResponse);
const result = await getStats(mockCollectorFetchContext.soClient);
expect(result).toBeUndefined();
});
test('Summarizes visualizations response data', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext(
mockedSavedObject,
mockedSavedObjectsByValue
);
const result = await getStats(mockCollectorFetchContext.soClient);
expect(result).toStrictEqual({
timeseries_use_last_value_mode_total: 5,
timeseries_use_es_indices_total: 4,
timeseries_table_use_aggregate_function: 2,
timeseries_types: {
gauge: 1,
markdown: 2,
metric: 0,
table: 2,
timeseries: 1,
top_n: 1,
},
});
});
});

View file

@ -1,182 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { findByValueEmbeddables } from '@kbn/dashboard-plugin/server';
import type {
SavedObjectsClientContract,
ISavedObjectsRepository,
SavedObjectsFindResult,
} from '@kbn/core/server';
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import type { SavedVisState } from '@kbn/visualizations-plugin/common';
import { TIME_RANGE_DATA_MODES } from '../../common/enums';
import type { Panel } from '../../common/types';
export interface TimeseriesUsage {
timeseries_use_last_value_mode_total: number;
timeseries_use_es_indices_total: number;
timeseries_table_use_aggregate_function: number;
timeseries_types: {
table: number;
gauge: number;
markdown: number;
top_n: number;
timeseries: number;
metric: number;
};
}
const doTelemetryFoVisualizations = async (
soClient: SavedObjectsClientContract | ISavedObjectsRepository,
calculateTelemetry: (savedVis: SavedVisState<Panel>) => void
) => {
const finder = await soClient.createPointInTimeFinder({
type: 'visualization',
perPage: 1000,
namespaces: ['*'],
});
for await (const response of finder.find()) {
(response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult<any>) => {
if (attributes?.visState) {
try {
const visState: SavedVisState<Panel> = JSON.parse(attributes.visState);
calculateTelemetry(visState);
} catch {
// nothing to be here, "so" not valid
}
}
});
}
await finder.close();
};
const doTelemetryForByValueVisualizations = async (
soClient: SavedObjectsClientContract | ISavedObjectsRepository,
telemetryUseLastValueMode: (savedVis: SavedVisState<Panel>) => void
) => {
const byValueVisualizations = await findByValueEmbeddables(soClient, 'visualization');
for (const item of byValueVisualizations) {
telemetryUseLastValueMode(item.savedVis as unknown as SavedVisState<Panel>);
}
};
const getDefaultTSVBVisualizations = (home?: HomeServerPluginSetup) => {
const titles: string[] = [];
const sampleDataSets = home?.sampleData.getSampleDatasets() ?? [];
sampleDataSets.forEach((sampleDataSet) =>
sampleDataSet.savedObjects.forEach((savedObject) => {
try {
if (savedObject.type === 'visualization') {
const visState = JSON.parse(savedObject.attributes?.visState);
if (visState.type === 'metrics') {
titles.push(visState.title);
}
}
} catch (e) {
// Let it go, visState is invalid and we'll don't need to handle it
}
})
);
return titles;
};
export const getStats = async (
soClient: SavedObjectsClientContract | ISavedObjectsRepository,
home?: HomeServerPluginSetup
): Promise<TimeseriesUsage | undefined> => {
const timeseriesUsage = {
timeseries_use_last_value_mode_total: 0,
timeseries_use_es_indices_total: 0,
timeseries_table_use_aggregate_function: 0,
timeseries_types: {
gauge: 0,
markdown: 0,
metric: 0,
table: 0,
timeseries: 0,
top_n: 0,
},
};
// we want to exclude the TSVB Sample Data visualizations from the stats
// in order to have more accurate results
const excludedFromStatsVisualizations = getDefaultTSVBVisualizations(home);
function telemetryUseLastValueMode(visState: SavedVisState<Panel>) {
if (
visState.type === 'metrics' &&
visState.params.type !== 'timeseries' &&
(!visState.params.time_range_mode ||
visState.params.time_range_mode === TIME_RANGE_DATA_MODES.LAST_VALUE) &&
!excludedFromStatsVisualizations.includes(visState.title)
) {
timeseriesUsage.timeseries_use_last_value_mode_total++;
}
}
function telemetryUseESIndices(visState: SavedVisState<Panel>) {
if (
visState.type === 'metrics' &&
!visState.params.use_kibana_indexes &&
!excludedFromStatsVisualizations.includes(visState.title)
) {
timeseriesUsage.timeseries_use_es_indices_total++;
}
}
function telemetryTableAggFunction(visState: SavedVisState<Panel>) {
if (
visState.type === 'metrics' &&
visState.params.type === 'table' &&
visState.params.series &&
visState.params.series.length > 0 &&
!excludedFromStatsVisualizations.includes(visState.title)
) {
const usesAggregateFunction = visState.params.series.some(
(s) => s.aggregate_by && s.aggregate_function
);
if (usesAggregateFunction) {
timeseriesUsage.timeseries_table_use_aggregate_function++;
}
}
}
function telemetryPanelTypes(visState: SavedVisState<Panel>) {
if (visState.type === 'metrics' && !excludedFromStatsVisualizations.includes(visState.title)) {
timeseriesUsage.timeseries_types[visState.params.type]++;
}
}
await Promise.all([
// last value usage telemetry
doTelemetryFoVisualizations(soClient, telemetryUseLastValueMode),
doTelemetryForByValueVisualizations(soClient, telemetryUseLastValueMode),
// elasticsearch indices usage telemetry
doTelemetryFoVisualizations(soClient, telemetryUseESIndices),
doTelemetryForByValueVisualizations(soClient, telemetryUseESIndices),
// table aggregate function telemetry
doTelemetryFoVisualizations(soClient, telemetryTableAggFunction),
doTelemetryForByValueVisualizations(soClient, telemetryTableAggFunction),
// panel types usage telemetry
doTelemetryFoVisualizations(soClient, telemetryPanelTypes),
doTelemetryForByValueVisualizations(soClient, telemetryPanelTypes),
]);
return timeseriesUsage.timeseries_use_last_value_mode_total ||
timeseriesUsage.timeseries_use_es_indices_total ||
timeseriesUsage.timeseries_table_use_aggregate_function ||
Object.values(timeseriesUsage.timeseries_types).some((visualizationCount) => visualizationCount)
? timeseriesUsage
: undefined;
};

View file

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

View file

@ -1,52 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { mockStats, mockGetStats } from './get_usage_collector.mock';
import { createUsageCollectionSetupMock } from '@kbn/usage-collection-plugin/server/mocks';
import { createCollectorFetchContextMock } from '@kbn/usage-collection-plugin/server/mocks';
import { registerTimeseriesUsageCollector } from './register_timeseries_collector';
describe('registerTimeseriesUsageCollector', () => {
it('makes a usage collector and registers it`', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerTimeseriesUsageCollector(mockCollectorSet);
expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
});
it('makeUsageCollector configs fit the shape', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerTimeseriesUsageCollector(mockCollectorSet);
expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
type: 'vis_type_timeseries',
isReady: expect.any(Function),
fetch: expect.any(Function),
schema: expect.any(Object),
});
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('makeUsageCollector config.isReady returns true', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerTimeseriesUsageCollector(mockCollectorSet);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('makeUsageCollector config.fetch calls getStats', async () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerTimeseriesUsageCollector(mockCollectorSet);
const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value;
const mockedCollectorFetchContext = createCollectorFetchContextMock();
const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext);
expect(mockGetStats).toBeCalledTimes(1);
expect(mockGetStats).toBeCalledWith(mockedCollectorFetchContext.soClient, undefined);
expect(fetchResult).toBe(mockStats);
});
});

View file

@ -1,46 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import { getStats, TimeseriesUsage } from './get_usage_collector';
export function registerTimeseriesUsageCollector(
collectorSet: UsageCollectionSetup,
home?: HomeServerPluginSetup
) {
const collector = collectorSet.makeUsageCollector<TimeseriesUsage | undefined>({
type: 'vis_type_timeseries',
isReady: () => true,
schema: {
timeseries_use_last_value_mode_total: {
type: 'long',
_meta: { description: 'Number of TSVB visualizations using "last value" as a time range' },
},
timeseries_use_es_indices_total: {
type: 'long',
_meta: { description: 'Number of TSVB visualizations using elasticsearch indices' },
},
timeseries_table_use_aggregate_function: {
type: 'long',
_meta: { description: 'Number of TSVB table visualizations using aggregate function' },
},
timeseries_types: {
table: { type: 'long' },
gauge: { type: 'long' },
markdown: { type: 'long' },
top_n: { type: 'long' },
timeseries: { type: 'long' },
metric: { type: 'long' },
},
},
fetch: async ({ soClient }) => await getStats(soClient, home),
});
collectorSet.registerCollector(collector);
}

View file

@ -23,7 +23,6 @@
{ "path": "../../dashboard/tsconfig.json" },
{ "path": "../../kibana_utils/tsconfig.json" },
{ "path": "../../kibana_react/tsconfig.json" },
{ "path": "../../usage_collection/tsconfig.json" },
{ "path": "../../unified_search/tsconfig.json" }
]
}

View file

@ -4,7 +4,7 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "visualizations", "mapsEms", "expressions", "inspector", "dataViews"],
"optionalPlugins": ["home","usageCollection"],
"optionalPlugins": ["home"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"],
"owner": {
"name": "Vis Editors",

View file

@ -7,25 +7,16 @@
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import { registerVegaUsageCollector } from './usage_collector';
import {
ConfigObservable,
VisTypeVegaPluginSetupDependencies,
VisTypeVegaPluginSetup,
VisTypeVegaPluginStart,
} from './types';
export class VisTypeVegaPlugin implements Plugin<VisTypeVegaPluginSetup, VisTypeVegaPluginStart> {
private readonly config: ConfigObservable;
constructor(initializerContext: PluginInitializerContext) {
this.config = initializerContext.config.legacy.globalConfig$;
}
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { home, usageCollection }: VisTypeVegaPluginSetupDependencies) {
if (usageCollection) {
registerVegaUsageCollector(usageCollection, this.config, { home });
}
return {};
}

View file

@ -1,14 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const mockStats = { somestat: 1 };
export const mockGetStats = jest.fn().mockResolvedValue(mockStats);
jest.doMock('./get_usage_collector', () => ({
getStats: mockGetStats,
}));

View file

@ -1,148 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getStats } from './get_usage_collector';
import { createCollectorFetchContextMock } from '@kbn/usage-collection-plugin/server/mocks';
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import type { SavedObjectsClientContract } from '@kbn/core/server';
const mockedSavedObjects = [
// vega-lite lib spec
{
attributes: {
visState: JSON.stringify({
type: 'vega',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v5.json" }',
},
}),
},
},
// vega lib spec
{
attributes: {
visState: JSON.stringify({
type: 'vega',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }',
},
}),
},
},
// map layout
{
attributes: {
visState: JSON.stringify({
type: 'vega',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega/v3.json" \n "config": { "kibana" : { "type": "map" }} }',
},
}),
},
},
];
const getMockCollectorFetchContext = (savedObjects?: unknown[]) => {
const fetchParamsMock = createCollectorFetchContextMock();
fetchParamsMock.soClient = {
createPointInTimeFinder: jest.fn().mockResolvedValue({
close: jest.fn(),
find: function* asyncGenerator() {
yield { saved_objects: savedObjects };
},
}),
} as unknown as SavedObjectsClientContract;
return fetchParamsMock;
};
describe('Vega visualization usage collector', () => {
const mockDeps = {
home: {
sampleData: {
getSampleDatasets: jest.fn().mockReturnValue([
{
savedObjects: [
{
type: 'visualization',
attributes: {
visState: JSON.stringify({
type: 'vega',
title: 'sample vega visualization',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }',
},
}),
},
},
],
},
]),
},
} as unknown as HomeServerPluginSetup,
};
test('Returns undefined when no results found (undefined)', async () => {
const result = await getStats(getMockCollectorFetchContext().soClient, mockDeps);
expect(result).toBeUndefined();
});
test('Returns undefined when no results found (0 results)', async () => {
const result = await getStats(getMockCollectorFetchContext([]).soClient, mockDeps);
expect(result).toBeUndefined();
});
test('Returns undefined when no vega saved objects found', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext([
{
_id: 'visualization:myvis-123',
_source: {
type: 'visualization',
visualization: { visState: '{"type": "area"}' },
},
},
]);
const result = await getStats(mockCollectorFetchContext.soClient, mockDeps);
expect(result).toBeUndefined();
});
test('Should ingnore sample data visualizations', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext([
{
attributes: {
visState: JSON.stringify({
type: 'vega',
title: 'sample vega visualization',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }',
},
}),
},
},
]);
const result = await getStats(mockCollectorFetchContext.soClient, mockDeps);
expect(result).toBeUndefined();
});
test('Summarizes visualizations response data', async () => {
const mockCollectorFetchContext = getMockCollectorFetchContext(mockedSavedObjects);
const result = await getStats(mockCollectorFetchContext.soClient, mockDeps);
expect(result).toMatchObject({
vega_lib_specs_total: 2,
vega_lite_lib_specs_total: 1,
vega_use_map_total: 1,
});
});
});

View file

@ -1,116 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { parse } from 'hjson';
import type { SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server';
import type { SavedVisState } from '@kbn/visualizations-plugin/common';
import type { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types';
type UsageCollectorDependencies = Pick<VisTypeVegaPluginSetupDependencies, 'home'>;
type VegaType = 'vega' | 'vega-lite';
export interface VegaUsage {
vega_lib_specs_total: number;
vega_lite_lib_specs_total: number;
vega_use_map_total: number;
}
function isVegaType(attributes: any): attributes is VegaSavedObjectAttributes {
return attributes && attributes.type === 'vega' && attributes.params?.spec;
}
const checkVegaSchemaType = (schemaURL: string, type: VegaType) =>
schemaURL.includes(`//vega.github.io/schema/${type}/`);
const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home']) => {
const titles: string[] = [];
const sampleDataSets = home?.sampleData.getSampleDatasets() ?? [];
sampleDataSets.forEach((sampleDataSet) =>
sampleDataSet.savedObjects.forEach((savedObject) => {
try {
if (savedObject.type === 'visualization') {
const visState = JSON.parse(savedObject.attributes?.visState);
if (isVegaType(visState)) {
titles.push(visState.title);
}
}
} catch (e) {
// Let it go, visState is invalid and we'll don't need to handle it
}
})
);
return titles;
};
export const getStats = async (
soClient: SavedObjectsClientContract,
{ home }: UsageCollectorDependencies
): Promise<VegaUsage | undefined> => {
let shouldPublishTelemetry = false;
const vegaUsage = {
vega_lib_specs_total: 0,
vega_lite_lib_specs_total: 0,
vega_use_map_total: 0,
};
// we want to exclude the Vega Sample Data visualizations from the stats
// in order to have more accurate results
const excludedFromStatsVisualizations = getDefaultVegaVisualizations(home);
const finder = await soClient.createPointInTimeFinder({
type: 'visualization',
perPage: 1000,
namespaces: ['*'],
});
const doTelemetry = ({ params }: SavedVisState) => {
try {
const spec = parse(params.spec as string, { legacyRoot: false });
if (spec) {
shouldPublishTelemetry = true;
if (checkVegaSchemaType(spec.$schema, 'vega')) {
vegaUsage.vega_lib_specs_total++;
}
if (checkVegaSchemaType(spec.$schema, 'vega-lite')) {
vegaUsage.vega_lite_lib_specs_total++;
}
if (spec.config?.kibana?.type === 'map') {
vegaUsage.vega_use_map_total++;
}
}
} catch (e) {
// Let it go, the data is invalid and we'll don't need to handle it
}
};
for await (const response of finder.find()) {
(response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult<any>) => {
if (attributes?.visState) {
try {
const visState: SavedVisState = JSON.parse(attributes.visState);
if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title)) {
doTelemetry(visState);
}
} catch {
// nothing to be here, "so" not valid
}
}
});
}
await finder.close();
return shouldPublishTelemetry ? vegaUsage : undefined;
};

View file

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

View file

@ -1,60 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
createUsageCollectionSetupMock,
createCollectorFetchContextMock,
} from '@kbn/usage-collection-plugin/server/mocks';
import { mockStats, mockGetStats } from './get_usage_collector.mock';
import { registerVegaUsageCollector } from './register_vega_collector';
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import type { ConfigObservable } from '../types';
describe('registerVegaUsageCollector', () => {
const mockDeps = { home: {} as unknown as HomeServerPluginSetup };
const mockConfig = {} as ConfigObservable;
test('makes a usage collector and registers it`', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
});
test('makeUsageCollector configs fit the shape', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
type: 'vis_type_vega',
isReady: expect.any(Function),
fetch: expect.any(Function),
schema: expect.any(Object),
});
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
test('makeUsageCollector config.isReady returns true', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
test('makeUsageCollector config.fetch calls getStats', async () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value;
const mockedCollectorFetchContext = createCollectorFetchContextMock();
const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext);
expect(mockGetStats).toBeCalledTimes(1);
expect(mockGetStats).toBeCalledWith(mockedCollectorFetchContext.soClient, mockDeps);
expect(fetchResult).toBe(mockStats);
});
});

View file

@ -1,30 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { getStats, VegaUsage } from './get_usage_collector';
import type { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types';
export function registerVegaUsageCollector(
collectorSet: UsageCollectionSetup,
config: ConfigObservable,
dependencies: Pick<VisTypeVegaPluginSetupDependencies, 'home'>
) {
const collector = collectorSet.makeUsageCollector<VegaUsage | undefined>({
type: 'vis_type_vega',
isReady: () => true,
schema: {
vega_lib_specs_total: { type: 'long' },
vega_lite_lib_specs_total: { type: 'long' },
vega_use_map_total: { type: 'long' },
},
fetch: async ({ soClient }) => await getStats(soClient, dependencies),
});
collectorSet.registerCollector(collector);
}

View file

@ -23,7 +23,6 @@
{ "path": "../../expressions/tsconfig.json" },
{ "path": "../../inspector/tsconfig.json" },
{ "path": "../../home/tsconfig.json" },
{ "path": "../../usage_collection/tsconfig.json" },
{ "path": "../../kibana_utils/tsconfig.json" },
{ "path": "../../kibana_react/tsconfig.json" },
{ "path": "../../vis_default_editor/tsconfig.json" },

View file

@ -9,20 +9,18 @@
import React, { useState, useEffect } from 'react';
import type { PaletteRegistry } from '@kbn/coloring';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { EuiFormRow, EuiRange } from '@elastic/eui';
import { SelectOption, SwitchOption, PalettePicker } from '@kbn/vis-default-editor-plugin/public';
import { ChartType } from '../../../../../common';
import { VisParams } from '../../../../types';
import { ValidationVisOptionsProps } from '../../common';
import { getPalettesService, getTrackUiMetric } from '../../../../services';
import { getPalettesService } from '../../../../services';
import { getFittingFunctions } from '../../../collections';
const fittingFunctions = getFittingFunctions();
export function ElasticChartsOptions(props: ValidationVisOptionsProps<VisParams>) {
const trackUiMetric = getTrackUiMetric();
const [palettesRegistry, setPalettesRegistry] = useState<PaletteRegistry | null>(null);
const { stateParams, setValue, aggs } = props;
@ -58,9 +56,6 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps<VisParams>
paramName="detailedTooltip"
value={stateParams.detailedTooltip}
setValue={(paramName, value) => {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'detailed_tooltip_switched');
}
setValue(paramName, value);
}}
/>
@ -75,9 +70,6 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps<VisParams>
paramName="fittingFunction"
value={stateParams.fittingFunction ?? fittingFunctions[2].value}
setValue={(paramName, value) => {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected');
}
setValue(paramName, value);
}}
/>
@ -89,9 +81,6 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps<VisParams>
activePalette={stateParams.palette}
paramName="palette"
setPalette={(paramName, value) => {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, 'palette_selected');
}
setValue(paramName, value);
}}
/>

View file

@ -16,7 +16,6 @@ import { ChartType } from '../../../../../common';
import { getAggs, getVis, getStateParams } from './point_series.mocks';
jest.mock('../../../../services', () => ({
getTrackUiMetric: jest.fn(() => null),
getPalettesService: jest.fn(() => {
return {
getPalettes: jest.fn(),

View file

@ -19,7 +19,6 @@ import {
setUISettings,
setDocLinks,
setPalettesService,
setTrackUiMetric,
setActiveCursor,
} from './services';
@ -85,9 +84,6 @@ export class VisTypeXyPlugin
expressions.registerFunction(expressionFunctions.visScale);
visTypesDefinitions.forEach(visualizations.createBaseVisualization);
setTrackUiMetric(usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_xy'));
return {};
}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { UiCounterMetricType } from '@kbn/analytics';
import { CoreSetup, DocLinksStart } from '@kbn/core/public';
import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
@ -31,8 +30,3 @@ export const [getPalettesService, setPalettesService] =
createGetterSetter<ChartsPluginSetup['palettes']>('xy charts.palette');
export const [getDocLinks, setDocLinks] = createGetterSetter<DocLinksStart>('DocLinks');
export const [getTrackUiMetric, setTrackUiMetric] =
createGetterSetter<(metricType: UiCounterMetricType, eventName: string | string[]) => void>(
'trackUiMetric'
);

View file

@ -18,7 +18,7 @@
"dataViews",
"dataViewEditor"
],
"optionalPlugins": ["home", "share", "usageCollection", "spaces", "savedObjectsTaggingOss"],
"optionalPlugins": ["home", "share", "spaces", "savedObjectsTaggingOss"],
"requiredBundles": ["kibanaUtils", "discover", "kibanaReact"],
"extraPublicDirs": ["common/constants", "common/utils", "common/expression_functions"],
"owner": {

View file

@ -14,7 +14,6 @@ import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
import { savedObjectsPluginMock } from '@kbn/saved-objects-plugin/public/mocks';
@ -52,7 +51,6 @@ const createInstance = async () => {
embeddable: embeddablePluginMock.createSetupContract(),
expressions: expressionsPluginMock.createSetupContract(),
inspector: inspectorPluginMock.createSetupContract(),
usageCollection: usageCollectionPluginMock.createSetupContract(),
urlForwarding: urlForwardingPluginMock.createSetupContract(),
uiActions: uiActionsPluginMock.createSetupContract(),
});

View file

@ -33,7 +33,6 @@ import type {
ApplicationStart,
SavedObjectsClientContract,
} from '@kbn/core/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
@ -50,7 +49,6 @@ import type { NavigationPublicPluginStart as NavigationStart } from '@kbn/naviga
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
@ -78,7 +76,6 @@ import {
setHttp,
setSearch,
setSavedObjects,
setUsageCollector,
setExpressions,
setUiActions,
setTimeFilter,
@ -112,7 +109,6 @@ export interface VisualizationsSetupDeps {
expressions: ExpressionsSetup;
inspector: InspectorSetup;
uiActions: UiActionsSetup;
usageCollection: UsageCollectionSetup;
urlForwarding: UrlForwardingSetup;
home?: HomePublicPluginSetup;
share?: SharePluginSetup;
@ -136,7 +132,6 @@ export interface VisualizationsStartDeps {
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
share?: SharePluginStart;
urlForwarding: UrlForwardingStart;
usageCollection?: UsageCollectionStart;
screenshotMode: ScreenshotModePluginStart;
fieldFormats: FieldFormatsStart;
}
@ -171,7 +166,6 @@ export class VisualizationsPlugin
{
expressions,
embeddable,
usageCollection,
data,
home,
urlForwarding,
@ -289,7 +283,6 @@ export class VisualizationsPlugin
setHeaderActionMenu: params.setHeaderActionMenu,
savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(),
presentationUtil: pluginsStart.presentationUtil,
usageCollection: pluginsStart.usageCollection,
getKibanaVersion: () => this.initializerContext.env.packageInfo.version,
spaces: pluginsStart.spaces,
visEditorsRegistry,
@ -335,7 +328,6 @@ export class VisualizationsPlugin
}
setUISettings(core.uiSettings);
setUsageCollector(usageCollection);
setTheme(core.theme);
expressions.registerFunction(rangeExpressionFunction);
@ -361,7 +353,6 @@ export class VisualizationsPlugin
savedObjects,
spaces,
savedObjectsTaggingOss,
usageCollection,
fieldFormats,
}: VisualizationsStartDeps
): VisualizationsStart {

View file

@ -20,7 +20,6 @@ import type {
ExecutionContextSetup,
} from '@kbn/core/public';
import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
@ -54,11 +53,6 @@ export const [getTimeFilter, setTimeFilter] = createGetterSetter<TimefilterContr
export const [getSearch, setSearch] = createGetterSetter<DataPublicPluginStart['search']>('Search');
export const [getUsageCollector, setUsageCollector] = createGetterSetter<UsageCollectionSetup>(
'UsageCollection',
false
);
export const [getExpressions, setExpressions] = createGetterSetter<ExpressionsStart>('Expressions');
export const [getUiActions, setUiActions] = createGetterSetter<UiActionsStart>('UiActions');

View file

@ -37,7 +37,6 @@ import type { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type {
Vis,
@ -107,7 +106,6 @@ export interface VisualizeServices extends CoreStart {
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
savedObjectsTagging?: SavedObjectsTaggingApi;
presentationUtil: PresentationUtilPluginStart;
usageCollection?: UsageCollectionStart;
getKibanaVersion: () => string;
spaces?: SpacesPluginStart;
theme: ThemeServiceStart;

View file

@ -7,7 +7,6 @@
*/
import React from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import {
EuiBetaBadge,
EuiButton,
@ -25,16 +24,6 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug
import { RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
import { VisualizationListItem } from '../..';
import { getVisualizeListItemLink } from './get_visualize_list_item_link';
import { getUsageCollector } from '../../services';
import { VISUALIZE_APP_NAME } from '../../../common/constants';
const doTelemetryForAddEvent = (visType?: string) => {
const usageCollection = getUsageCollector();
if (usageCollection && visType) {
usageCollection.reportUiCounter(VISUALIZE_APP_NAME, METRIC_TYPE.CLICK, `${visType}:add`);
}
};
const getBadge = (item: VisualizationListItem) => {
if (item.stage === 'beta') {
@ -106,12 +95,8 @@ export const getTableColumns = (
// In case an error occurs i.e. the vis has wrong type, we render the vis but without the link
!error ? (
<RedirectAppLinks application={application} className="visListingTable__titleLink">
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
href={getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl)}
onClick={() => {
doTelemetryForAddEvent(typeof type === 'string' ? type : type?.name);
}}
data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`}
>
{field}

View file

@ -9,7 +9,6 @@
import React from 'react';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { EuiBetaBadgeProps } from '@elastic/eui';
import { parse } from 'query-string';
@ -40,7 +39,7 @@ import {
VisualizeAppStateContainer,
VisualizeEditorVisInstance,
} from '../types';
import { VISUALIZE_APP_NAME, VisualizeConstants } from '../../../common/constants';
import { VisualizeConstants } from '../../../common/constants';
import { getEditBreadcrumbs } from './breadcrumbs';
import { VISUALIZE_APP_LOCATOR, VisualizeLocatorParams } from '../../../common/locator';
import { getUiActions } from '../../services';
@ -121,7 +120,6 @@ export const getTopNavConfig = (
i18n: { Context: I18nContext },
savedObjectsTagging,
presentationUtil,
usageCollection,
getKibanaVersion,
savedObjects,
}: VisualizeServices
@ -129,16 +127,6 @@ export const getTopNavConfig = (
const { vis, embeddableHandler } = visInstance;
const savedVis = visInstance.savedVis;
const doTelemetryForSaveEvent = (visType: string) => {
if (usageCollection) {
usageCollection.reportUiCounter(
originatingApp ?? VISUALIZE_APP_NAME,
METRIC_TYPE.CLICK,
`${visType}:save`
);
}
};
/**
* Called when the user clicks "Save" button.
*/
@ -523,8 +511,6 @@ export const getTopNavConfig = (
return { id: true };
}
doTelemetryForSaveEvent(vis.type.name);
// We're adding the viz to a library so we need to save it and then
// add to a dashboard if necessary
const response = await doSave(saveOptions);
@ -642,8 +628,6 @@ export const getTopNavConfig = (
}
},
run: async () => {
doTelemetryForSaveEvent(vis.type.name);
if (!savedVis?.id) {
return createVisReference();
}

View file

@ -18,7 +18,6 @@ import {
SavedObjectsStart,
DocLinksStart,
} from '@kbn/core/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
import { SearchSelection } from './search_selection';
import { GroupSelection } from './group_selection';
@ -36,7 +35,6 @@ interface TypeSelectionProps {
uiSettings: IUiSettingsClient;
docLinks: DocLinksStart;
savedObjects: SavedObjectsStart;
usageCollection?: UsageCollectionSetup;
application: ApplicationStart;
outsideVisualizeApp?: boolean;
stateTransfer?: EmbeddableStateTransfer;
@ -75,11 +73,6 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
showGroups: !this.props.showAggsSelection,
visType: this.props.selectedVisType,
};
this.trackUiMetric = this.props.usageCollection?.reportUiCounter.bind(
this.props.usageCollection,
'visualize'
);
}
public render() {

View file

@ -16,7 +16,6 @@ import {
getSavedObjects,
getTypes,
getUISettings,
getUsageCollector,
getApplication,
getEmbeddable,
getDocLinks,
@ -83,7 +82,6 @@ export function showNewVisModal({
addBasePath={getHttp().basePath.prepend}
uiSettings={getUISettings()}
savedObjects={getSavedObjects()}
usageCollection={getUsageCollector()}
application={getApplication()}
docLinks={getDocLinks()}
showAggsSelection={showAggsSelection}

View file

@ -17,10 +17,8 @@ import type {
Plugin,
Logger,
} from '@kbn/core/server';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
import { registerVisualizationsCollector } from './usage_collector';
import { capabilitiesProvider } from './capabilities_provider';
import type { VisualizationsPluginSetup, VisualizationsPluginStart } from './types';
@ -39,7 +37,6 @@ export class VisualizationsPlugin
public setup(
core: CoreSetup,
plugins: {
usageCollection?: UsageCollectionSetup;
embeddable: EmbeddableSetup;
data: DataPluginSetup;
}
@ -66,10 +63,6 @@ export class VisualizationsPlugin
},
});
if (plugins.usageCollection) {
registerVisualizationsCollector(plugins.usageCollection);
}
plugins.embeddable.registerEmbeddableFactory(
makeVisualizeEmbeddableFactory(getSearchSourceMigrations)()
);

View file

@ -1,24 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import moment from 'moment';
import { getPastDays } from './get_past_days';
describe('getPastDays', () => {
test('Returns 2 days that have passed from the current date', () => {
const pastDate = moment().subtract(2, 'days').startOf('day').toString();
expect(getPastDays(pastDate)).toEqual(2);
});
test('Returns 30 days that have passed from the current date', () => {
const pastDate = moment().subtract(30, 'days').startOf('day').toString();
expect(getPastDays(pastDate)).toEqual(30);
});
});

View file

@ -1,14 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const getPastDays = (dateString: string): number => {
const date = new Date(dateString);
const today = new Date();
const diff = Math.abs(date.getTime() - today.getTime());
return Math.trunc(diff / (1000 * 60 * 60 * 24));
};

View file

@ -1,13 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const mockStats = { somestat: 1 };
export const mockGetStats = jest.fn().mockResolvedValue(mockStats);
jest.doMock('./get_usage_collector', () => ({
getStats: mockGetStats,
}));

View file

@ -1,168 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import moment from 'moment';
import { getStats } from './get_usage_collector';
import type { SavedObjectsClientContract } from '@kbn/core/server';
const defaultMockSavedObjects = [
{
id: 'visualization:coolviz-123',
attributes: { visState: '{"type": "shell_beads"}' },
updated_at: moment().subtract(7, 'days').startOf('day').toString(),
},
];
const enlargedMockSavedObjects = [
// default space
{
id: 'visualization:coolviz-123',
namespaces: ['default'],
attributes: { visState: '{"type": "cave_painting"}' },
updated_at: moment().subtract(7, 'days').startOf('day').toString(),
},
{
id: 'visualization:coolviz-456',
namespaces: ['default'],
attributes: { visState: '{"type": "printing_press"}' },
updated_at: moment().subtract(20, 'days').startOf('day').toString(),
},
{
id: 'meat:visualization:coolviz-789',
namespaces: ['default'],
attributes: { visState: '{"type": "floppy_disk"}' },
updated_at: moment().subtract(2, 'months').startOf('day').toString(),
},
// meat space
{
id: 'meat:visualization:coolviz-789',
namespaces: ['meat'],
attributes: { visState: '{"type": "cave_painting"}' },
updated_at: moment().subtract(89, 'days').startOf('day').toString(),
},
{
id: 'meat:visualization:coolviz-789',
namespaces: ['meat'],
attributes: { visState: '{"type": "cuneiform"}' },
updated_at: moment().subtract(5, 'months').startOf('day').toString(),
},
{
id: 'meat:visualization:coolviz-789',
namespaces: ['meat'],
attributes: { visState: '{"type": "cuneiform"}' },
updated_at: moment().subtract(2, 'days').startOf('day').toString(),
},
{
id: 'meat:visualization:coolviz-789',
attributes: { visState: '{"type": "floppy_disk"}' },
updated_at: moment().subtract(7, 'days').startOf('day').toString(),
},
// cyber space
{
id: 'cyber:visualization:coolviz-789',
namespaces: ['cyber'],
attributes: { visState: '{"type": "floppy_disk"}' },
updated_at: moment().subtract(7, 'months').startOf('day').toString(),
},
{
id: 'cyber:visualization:coolviz-789',
namespaces: ['cyber'],
attributes: { visState: '{"type": "floppy_disk"}' },
updated_at: moment().subtract(3, 'days').startOf('day').toString(),
},
{
id: 'cyber:visualization:coolviz-123',
namespaces: ['cyber'],
attributes: { visState: '{"type": "cave_painting"}' },
updated_at: moment().subtract(15, 'days').startOf('day').toString(),
},
];
describe('Visualizations usage collector', () => {
const getMockCallCluster = (savedObjects: unknown[]) =>
({
createPointInTimeFinder: jest.fn().mockResolvedValue({
close: jest.fn(),
find: function* asyncGenerator() {
yield { saved_objects: savedObjects };
},
}),
} as unknown as SavedObjectsClientContract);
test('Returns undefined when no results found (undefined)', async () => {
const result = await getStats(getMockCallCluster(undefined as any));
expect(result).toBeUndefined();
});
test('Returns undefined when no results found (0 results)', async () => {
const result = await getStats(getMockCallCluster([]));
expect(result).toBeUndefined();
});
test('Summarizes visualizations response data', async () => {
const result = await getStats(getMockCallCluster(defaultMockSavedObjects));
expect(result).toMatchObject({
shell_beads: {
spaces_avg: 1,
spaces_max: 1,
spaces_min: 1,
total: 1,
saved_7_days_total: 1,
saved_30_days_total: 1,
saved_90_days_total: 1,
},
});
});
test('Summarizes visualizations response data per Space', async () => {
const expectedStats = {
cave_painting: {
total: 3,
spaces_min: 1,
spaces_max: 1,
spaces_avg: 1,
saved_7_days_total: 1,
saved_30_days_total: 2,
saved_90_days_total: 3,
},
printing_press: {
total: 1,
spaces_min: 1,
spaces_max: 1,
spaces_avg: 1,
saved_7_days_total: 0,
saved_30_days_total: 1,
saved_90_days_total: 1,
},
cuneiform: {
total: 2,
spaces_min: 2,
spaces_max: 2,
spaces_avg: 2,
saved_7_days_total: 1,
saved_30_days_total: 1,
saved_90_days_total: 1,
},
floppy_disk: {
total: 4,
spaces_min: 2,
spaces_max: 2,
spaces_avg: 2,
saved_7_days_total: 2,
saved_30_days_total: 2,
saved_90_days_total: 3,
},
};
const result = await getStats(getMockCallCluster(enlargedMockSavedObjects));
expect(result).toMatchObject(expectedStats);
});
});

View file

@ -1,83 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { countBy, groupBy, mapValues, max, min, values } from 'lodash';
import type { SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server';
import { getPastDays } from './get_past_days';
import type { SavedVisState } from '../../common';
interface VisSummary {
type: string;
space: string;
past_days: number;
}
export interface VisualizationUsage {
[x: string]: {
total: number;
spaces_min?: number;
spaces_max?: number;
spaces_avg: number;
saved_7_days_total: number;
saved_30_days_total: number;
saved_90_days_total: number;
};
}
/*
* Parse the response data into telemetry payload
*/
export async function getStats(
soClient: SavedObjectsClientContract
): Promise<VisualizationUsage | undefined> {
const finder = await soClient.createPointInTimeFinder({
type: 'visualization',
perPage: 1000,
namespaces: ['*'],
});
const visSummaries: VisSummary[] = [];
for await (const response of finder.find()) {
(response.saved_objects || []).forEach((so: SavedObjectsFindResult<any>) => {
if (so.attributes?.visState) {
const visState: SavedVisState = JSON.parse(so.attributes.visState);
visSummaries.push({
type: visState.type ?? '_na_',
space: so.namespaces?.[0] ?? 'default',
past_days: getPastDays(so.updated_at!),
});
}
});
}
await finder.close();
if (visSummaries.length) {
// organize stats per type
const visTypes = groupBy(visSummaries, 'type');
// get the final result
return mapValues(visTypes, (curr) => {
const total = curr.length;
const spacesBreakdown = countBy(curr, 'space');
const spaceCounts: number[] = values(spacesBreakdown);
return {
total,
spaces_min: min(spaceCounts),
spaces_max: max(spaceCounts),
spaces_avg: total / spaceCounts.length,
saved_7_days_total: curr.filter((c) => c.past_days <= 7).length,
saved_30_days_total: curr.filter((c) => c.past_days <= 30).length,
saved_90_days_total: curr.filter((c) => c.past_days <= 90).length,
};
});
}
}

View file

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

View file

@ -1,53 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
createUsageCollectionSetupMock,
createCollectorFetchContextMock,
} from '@kbn/usage-collection-plugin/server/mocks';
import { mockStats, mockGetStats } from './get_usage_collector.mock';
import { registerVisualizationsCollector } from './register_visualizations_collector';
describe('registerVisualizationsCollector', () => {
test('makes a usage collector and registers it`', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet);
expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
});
test('makeUsageCollector configs fit the shape', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet);
expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
type: 'visualization_types',
isReady: expect.any(Function),
fetch: expect.any(Function),
schema: expect.any(Object),
});
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
test('makeUsageCollector config.isReady returns true', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
test('makeUsageCollector config.fetch calls getStats', async () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet);
const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value;
const mockCollectorFetchContext = createCollectorFetchContextMock();
const fetchResult = await usageCollector.fetch(mockCollectorFetchContext);
expect(mockGetStats).toBeCalledTimes(1);
expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.soClient);
expect(fetchResult).toBe(mockStats);
});
});

View file

@ -1,30 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { getStats, VisualizationUsage } from './get_usage_collector';
export function registerVisualizationsCollector(collectorSet: UsageCollectionSetup) {
const collector = collectorSet.makeUsageCollector<VisualizationUsage | undefined>({
type: 'visualization_types',
isReady: () => true,
schema: {
DYNAMIC_KEY: {
total: { type: 'long' },
spaces_min: { type: 'long' },
spaces_max: { type: 'long' },
spaces_avg: { type: 'long' },
saved_7_days_total: { type: 'long' },
saved_30_days_total: { type: 'long' },
saved_90_days_total: { type: 'long' },
},
},
fetch: async ({ soClient }) => await getStats(soClient),
});
collectorSet.registerCollector(collector);
}

View file

@ -22,7 +22,6 @@
{ "path": "../inspector/tsconfig.json" },
{ "path": "../saved_objects/tsconfig.json" },
{ "path": "../saved_objects_tagging_oss/tsconfig.json" },
{ "path": "../usage_collection/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../kibana_react/tsconfig.json" },
{ "path": "../discover/tsconfig.json" },