mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
ef5702bb8e
commit
65cafcd9c9
9 changed files with 450 additions and 7 deletions
|
@ -4,5 +4,6 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"],
|
||||
"optionalPlugins": ["home","usageCollection"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
47
src/plugins/vis_type_vega/server/plugin.ts
Normal file
47
src/plugins/vis_type_vega/server/plugin.ts
Normal 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() {}
|
||||
}
|
41
src/plugins/vis_type_vega/server/types.ts
Normal file
41
src/plugins/vis_type_vega/server/types.ts
Normal 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 {}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
32
src/plugins/vis_type_vega/server/usage_collector/index.ts
Normal file
32
src/plugins/vis_type_vega/server/usage_collector/index.ts
Normal 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);
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue