[Vega] Add vega maps statistics to usage collector (#78546)

* telemetry

* [Vega] Add vega maps statistics to usage collector

Closes: #78269

* add tests

* ignore sample data visualizations

* fix PR comment

* get default vega vis from home plugin

* match_phrase doesn't work for full text search

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-09-29 20:29:53 +03:00 committed by GitHub
parent ef5702bb8e
commit 65cafcd9c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 450 additions and 7 deletions

View file

@ -4,5 +4,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"],
"optionalPlugins": ["home","usageCollection"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"]
}

View file

@ -17,9 +17,10 @@
* under the License.
*/
import { PluginConfigDescriptor } from 'kibana/server';
import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
import { configSchema, ConfigSchema } from '../config';
import { VisTypeVegaPlugin } from './plugin';
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {
@ -32,7 +33,8 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
],
};
export const plugin = () => ({
setup() {},
start() {},
});
export function plugin(initializerContext: PluginInitializerContext) {
return new VisTypeVegaPlugin(initializerContext);
}
export { VisTypeVegaPluginStart, VisTypeVegaPluginSetup } from './types';

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../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$;
}
public setup(core: CoreSetup, { home, usageCollection }: VisTypeVegaPluginSetupDependencies) {
if (usageCollection) {
registerVegaUsageCollector(usageCollection, this.config, { home });
}
return {};
}
public start(core: CoreStart) {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable } from 'rxjs';
import { HomeServerPluginSetup } from '../../home/server';
import { UsageCollectionSetup } from '../../usage_collection/server';
export type ConfigObservable = Observable<{ kibana: { index: string } }>;
export interface VegaSavedObjectAttributes {
title: string;
type: string;
params: {
spec: string;
};
}
export interface VisTypeVegaPluginSetupDependencies {
usageCollection?: UsageCollectionSetup;
home?: HomeServerPluginSetup;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface VisTypeVegaPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface VisTypeVegaPluginStart {}

View file

@ -0,0 +1,172 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { of } from 'rxjs';
import { LegacyAPICaller } from 'src/core/server';
import { getUsageCollector } from './get_usage_collector';
import { HomeServerPluginSetup } from '../../../home/server';
const mockedSavedObjects = [
// vega-lite lib spec
{
_id: 'visualization:vega-1',
_source: {
type: 'visualization',
visualization: {
visState: JSON.stringify({
type: 'vega',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v4.json" }',
},
}),
},
},
},
// vega lib spec
{
_id: 'visualization:vega-2',
_source: {
type: 'visualization',
visualization: {
visState: JSON.stringify({
type: 'vega',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }',
},
}),
},
},
},
// map layout
{
_id: 'visualization:vega-3',
_source: {
type: 'visualization',
visualization: {
visState: JSON.stringify({
type: 'vega',
params: {
spec:
'{"$schema": "https://vega.github.io/schema/vega/v3.json" \n "config": { "kibana" : { "type": "map" }} }',
},
}),
},
},
},
];
const getMockCallCluster = (hits?: unknown[]) =>
jest.fn().mockReturnValue(Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller;
describe('Vega visualization usage collector', () => {
const configMock = of({ kibana: { index: '' } });
const usageCollector = getUsageCollector(configMock, {
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('Should fit the shape', () => {
expect(usageCollector.type).toBe('vis_type_vega');
expect(usageCollector.isReady()).toBe(true);
expect(usageCollector.fetch).toEqual(expect.any(Function));
});
test('Returns undefined when no results found (undefined)', async () => {
const result = await usageCollector.fetch(getMockCallCluster());
expect(result).toBeUndefined();
});
test('Returns undefined when no results found (0 results)', async () => {
const result = await usageCollector.fetch(getMockCallCluster([]));
expect(result).toBeUndefined();
});
test('Returns undefined when no vega saved objects found', async () => {
const result = await usageCollector.fetch(
getMockCallCluster([
{
_id: 'visualization:myvis-123',
_source: {
type: 'visualization',
visualization: { visState: '{"type": "area"}' },
},
},
])
);
expect(result).toBeUndefined();
});
test('Should ingnore sample data visualizations', async () => {
const callCluster = getMockCallCluster([
{
_id: 'visualization:sampledata-123',
_source: {
type: 'visualization',
visualization: {
visState: JSON.stringify({
type: 'vega',
title: 'sample vega visualization',
params: {
spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }',
},
}),
},
},
},
]);
const result = await usageCollector.fetch(callCluster);
expect(result).toBeUndefined();
});
test('Summarizes visualizations response data', async () => {
const result = await usageCollector.fetch(getMockCallCluster(mockedSavedObjects));
expect(result).toMatchObject({
vega_lib_specs_total: 2,
vega_lite_lib_specs_total: 1,
vega_use_map_total: 1,
});
});
});

View file

@ -0,0 +1,147 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { parse } from 'hjson';
import { first } from 'rxjs/operators';
import { SearchResponse } from 'elasticsearch';
import { LegacyAPICaller, SavedObject } from 'src/core/server';
import {
ConfigObservable,
VegaSavedObjectAttributes,
VisTypeVegaPluginSetupDependencies,
} from '../types';
type UsageCollectorDependencies = Pick<VisTypeVegaPluginSetupDependencies, 'home'>;
type ESResponse = SearchResponse<{ visualization: { visState: string } }>;
type VegaType = 'vega' | 'vega-lite';
const VEGA_USAGE_TYPE = 'vis_type_vega';
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: SavedObject<any>) => {
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;
};
const getStats = async (
callCluster: LegacyAPICaller,
index: string,
{ home }: UsageCollectorDependencies
) => {
const searchParams = {
size: 10000,
index,
ignoreUnavailable: true,
filterPath: ['hits.hits._id', 'hits.hits._source.visualization'],
body: {
query: {
bool: {
filter: { term: { type: 'visualization' } },
},
},
},
};
const esResponse: ESResponse = await callCluster('search', searchParams);
const size = esResponse?.hits?.hits?.length ?? 0;
let shouldPublishTelemetry = false;
if (!size) {
return;
}
// we want to exclude the Vega Sample Data visualizations from the stats
// in order to have more accurate results
const excludedFromStatsVisualizations = getDefaultVegaVisualizations(home);
const finalTelemetry = esResponse.hits.hits.reduce(
(telemetry, hit) => {
const visualization = hit._source?.visualization;
const visState = JSON.parse(visualization?.visState ?? '{}');
if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title))
try {
const spec = parse(visState.params.spec, { legacyRoot: false });
if (spec) {
shouldPublishTelemetry = true;
if (checkVegaSchemaType(spec.$schema, 'vega')) {
telemetry.vega_lib_specs_total++;
}
if (checkVegaSchemaType(spec.$schema, 'vega-lite')) {
telemetry.vega_lite_lib_specs_total++;
}
if (spec.config?.kibana?.type === 'map') {
telemetry.vega_use_map_total++;
}
}
} catch (e) {
// Let it go, the data is invalid and we'll don't need to handle it
}
return telemetry;
},
{
vega_lib_specs_total: 0,
vega_lite_lib_specs_total: 0,
vega_use_map_total: 0,
}
);
return shouldPublishTelemetry ? finalTelemetry : undefined;
};
export function getUsageCollector(
config: ConfigObservable,
dependencies: UsageCollectorDependencies
) {
return {
type: VEGA_USAGE_TYPE,
isReady: () => true,
fetch: async (callCluster: LegacyAPICaller) => {
const { index } = (await config.pipe(first()).toPromise()).kibana;
return await getStats(callCluster, index, dependencies);
},
};
}

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getUsageCollector } from './get_usage_collector';
import { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types';
export function registerVegaUsageCollector(
collectorSet: UsageCollectionSetup,
config: ConfigObservable,
dependencies: Pick<VisTypeVegaPluginSetupDependencies, 'home'>
) {
const collector = collectorSet.makeUsageCollector(getUsageCollector(config, dependencies));
collectorSet.registerCollector(collector);
}

View file

@ -3,6 +3,7 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector", "dashboard"],
"requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector", "dashboard"],
"optionalPlugins": ["usageCollection"],
"requiredBundles": ["kibanaUtils", "discover", "savedObjects"]
}

View file

@ -25,7 +25,7 @@ import { getUsageCollector } from './get_usage_collector';
export function registerVisualizationsCollector(
collectorSet: UsageCollectionSetup,
config: Observable<{ kibana: { index: string } }>
): void {
) {
const collector = collectorSet.makeUsageCollector(getUsageCollector(config));
collectorSet.registerCollector(collector);
}