From 6672e26fe5d7f7f99d54890d34209b546ad95429 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 15 Dec 2020 16:26:03 -0500 Subject: [PATCH] [Dashboard] Adds dashboard collector for byValue panels (#85867) * Adds dashboard collector for byValue panels * Fix telemetry schema * Remove unused import --- src/plugins/dashboard/server/plugin.ts | 4 + .../server/usage/dashboard_telemetry.test.ts | 177 ++++++++++++++++++ .../server/usage/dashboard_telemetry.ts | 151 +++++++++++++++ .../server/usage/register_collector.ts | 52 +++++ src/plugins/telemetry/schema/oss_plugins.json | 24 +++ 5 files changed, 408 insertions(+) create mode 100644 src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts create mode 100644 src/plugins/dashboard/server/usage/dashboard_telemetry.ts create mode 100644 src/plugins/dashboard/server/usage/register_collector.ts diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index 6a4c297f2588..e331f0a828ff 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -30,9 +30,12 @@ import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; import { EmbeddableSetup } from '../../embeddable/server'; +import { UsageCollectionSetup } from '../../usage_collection/server'; +import { registerDashboardUsageCollector } from './usage/register_collector'; interface SetupDeps { embeddable: EmbeddableSetup; + usageCollection: UsageCollectionSetup; } export class DashboardPlugin @@ -55,6 +58,7 @@ export class DashboardPlugin ); core.capabilities.registerProvider(capabilitiesProvider); + registerDashboardUsageCollector(plugins.usageCollection, plugins.embeddable); return {}; } diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts new file mode 100644 index 000000000000..a57473a8def5 --- /dev/null +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts @@ -0,0 +1,177 @@ +/* + * 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 { SavedDashboardPanel730ToLatest } from '../../common'; +import { + collectDashboardInfo, + getEmptyTelemetryData, + collectByValueVisualizationInfo, + collectByValueLensInfo, +} from './dashboard_telemetry'; + +const visualizationType1ByValue = ({ + embeddableConfig: { + savedVis: { + type: 'type1', + }, + }, + type: 'visualization', +} as unknown) as SavedDashboardPanel730ToLatest; + +const visualizationType2ByValue = ({ + embeddableConfig: { + savedVis: { + type: 'type2', + }, + }, + type: 'visualization', +} as unknown) as SavedDashboardPanel730ToLatest; +const visualizationType2ByReference = { + ...visualizationType2ByValue, + id: '11111', +}; + +const lensTypeAByValue = ({ + type: 'lens', + embeddableConfig: { + attributes: { + visualizationType: 'a', + }, + }, +} as unknown) as SavedDashboardPanel730ToLatest; +const lensTypeAByReference = { + ...lensTypeAByValue, + id: '22222', +}; + +const lensXYSeriesA = ({ + type: 'lens', + embeddableConfig: { + attributes: { + visualizationType: 'lnsXY', + state: { + visualization: { + preferredSeriesType: 'seriesA', + }, + }, + }, + }, +} as unknown) as SavedDashboardPanel730ToLatest; + +const lensXYSeriesB = ({ + type: 'lens', + embeddableConfig: { + attributes: { + visualizationType: 'lnsXY', + state: { + visualization: { + preferredSeriesType: 'seriesB', + }, + }, + }, + }, +} as unknown) as SavedDashboardPanel730ToLatest; + +describe('dashboard telemetry', () => { + it('collects information about dashboard panels', () => { + const panels = [ + visualizationType1ByValue, + visualizationType2ByValue, + visualizationType2ByReference, + ]; + const collectorData = getEmptyTelemetryData(); + + collectDashboardInfo(panels, collectorData); + + expect(collectorData.panels).toBe(panels.length); + expect(collectorData.panelsByValue).toBe(2); + }); + + describe('visualizations', () => { + it('collects information about by value visualizations', () => { + const panels = [ + visualizationType1ByValue, + visualizationType1ByValue, + visualizationType2ByValue, + visualizationType2ByReference, + ]; + + const collectorData = getEmptyTelemetryData(); + + collectByValueVisualizationInfo(panels, collectorData); + + expect(collectorData.visualizationByValue.type1).toBe(2); + expect(collectorData.visualizationByValue.type2).toBe(1); + }); + + it('handles misshapen visualization panels without errors', () => { + const badVisualizationPanel = ({ + embeddableConfig: {}, + type: 'visualization', + } as unknown) as SavedDashboardPanel730ToLatest; + + const panels = [badVisualizationPanel, visualizationType1ByValue]; + + const collectorData = getEmptyTelemetryData(); + + collectByValueVisualizationInfo(panels, collectorData); + + expect(Object.keys(collectorData.visualizationByValue)).toHaveLength(1); + }); + }); + + describe('lens', () => { + it('collects information about by value lens', () => { + const panels = [ + lensTypeAByValue, + lensTypeAByValue, + lensTypeAByValue, + lensTypeAByReference, + lensXYSeriesA, + lensXYSeriesA, + lensXYSeriesB, + ]; + + const collectorData = getEmptyTelemetryData(); + + collectByValueLensInfo(panels, collectorData); + + expect(collectorData.lensByValue.a).toBe(3); + expect(collectorData.lensByValue.seriesA).toBe(2); + expect(collectorData.lensByValue.seriesB).toBe(1); + }); + + it('handles misshapen lens panels', () => { + const badPanel = ({ + type: 'lens', + embeddableConfig: { + oops: 'no visualization type', + }, + } as unknown) as SavedDashboardPanel730ToLatest; + + const panels = [badPanel, lensTypeAByValue]; + + const collectorData = getEmptyTelemetryData(); + + collectByValueLensInfo(panels, collectorData); + + expect(collectorData.lensByValue.a).toBe(1); + }); + }); +}); diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts new file mode 100644 index 000000000000..2ad9b4f09bc2 --- /dev/null +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts @@ -0,0 +1,151 @@ +/* + * 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 { ISavedObjectsRepository, SavedObjectAttributes } from 'src/core/server'; +import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common'; +import { SavedDashboardPanel730ToLatest } from '../../common'; +import { injectReferences } from '../../common/saved_dashboard_references'; + +interface VisualizationPanel extends SavedDashboardPanel730ToLatest { + embeddableConfig: { + savedVis?: { + type?: string; + }; + }; +} + +interface LensPanel extends SavedDashboardPanel730ToLatest { + embeddableConfig: { + attributes?: { + visualizationType?: string; + state?: { + visualization?: { + preferredSeriesType?: string; + }; + }; + }; + }; +} + +export interface DashboardCollectorData { + panels: number; + panelsByValue: number; + lensByValue: { + [key: string]: number; + }; + visualizationByValue: { + [key: string]: number; + }; +} + +export const getEmptyTelemetryData = (): DashboardCollectorData => ({ + panels: 0, + panelsByValue: 0, + lensByValue: {}, + visualizationByValue: {}, +}); + +type DashboardCollectorFunction = ( + panels: SavedDashboardPanel730ToLatest[], + collectorData: DashboardCollectorData +) => void; + +export const collectDashboardInfo: DashboardCollectorFunction = (panels, collectorData) => { + collectorData.panels += panels.length; + collectorData.panelsByValue += panels.filter((panel) => panel.id === undefined).length; +}; + +export const collectByValueVisualizationInfo: DashboardCollectorFunction = ( + panels, + collectorData +) => { + const byValueVisualizations = panels.filter( + (panel) => panel.id === undefined && panel.type === 'visualization' + ); + + for (const panel of byValueVisualizations) { + const visPanel = panel as VisualizationPanel; + + if ( + visPanel.embeddableConfig.savedVis !== undefined && + visPanel.embeddableConfig.savedVis.type !== undefined + ) { + const type = visPanel.embeddableConfig.savedVis.type; + + if (!collectorData.visualizationByValue[type]) { + collectorData.visualizationByValue[type] = 0; + } + + collectorData.visualizationByValue[type] = collectorData.visualizationByValue[type] + 1; + } + } +}; + +export const collectByValueLensInfo: DashboardCollectorFunction = (panels, collectorData) => { + const byValueLens = panels.filter((panel) => panel.id === undefined && panel.type === 'lens'); + + for (const panel of byValueLens) { + const lensPanel = panel as LensPanel; + + if (lensPanel.embeddableConfig.attributes?.visualizationType !== undefined) { + let type = lensPanel.embeddableConfig.attributes.visualizationType; + + if (type === 'lnsXY') { + type = + lensPanel.embeddableConfig.attributes.state?.visualization?.preferredSeriesType || type; + } + + if (!collectorData.lensByValue[type]) { + collectorData.lensByValue[type] = 0; + } + + collectorData.lensByValue[type] = collectorData.lensByValue[type] + 1; + } + } +}; + +export const collectForPanels: DashboardCollectorFunction = (panels, collectorData) => { + collectDashboardInfo(panels, collectorData); + collectByValueVisualizationInfo(panels, collectorData); + collectByValueLensInfo(panels, collectorData); +}; + +export async function collectDashboardTelemetry( + savedObjectClient: Pick, + embeddableService: EmbeddablePersistableStateService +) { + const collectorData = getEmptyTelemetryData(); + const dashboards = await savedObjectClient.find({ + type: 'dashboard', + }); + + for (const dashboard of dashboards.saved_objects) { + const attributes = injectReferences(dashboard, { + embeddablePersistableStateService: embeddableService, + }); + + const panels = (JSON.parse( + attributes.panelsJSON as string + ) as unknown) as SavedDashboardPanel730ToLatest[]; + + collectForPanels(panels, collectorData); + } + + return collectorData; +} diff --git a/src/plugins/dashboard/server/usage/register_collector.ts b/src/plugins/dashboard/server/usage/register_collector.ts new file mode 100644 index 000000000000..ae3dd7b18fac --- /dev/null +++ b/src/plugins/dashboard/server/usage/register_collector.ts @@ -0,0 +1,52 @@ +/* + * 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 { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common'; + +import { collectDashboardTelemetry, DashboardCollectorData } from './dashboard_telemetry'; + +export function registerDashboardUsageCollector( + usageCollection: UsageCollectionSetup, + embeddableService: EmbeddablePersistableStateService +) { + const dashboardCollector = usageCollection.makeUsageCollector({ + type: 'dashboard', + isReady: () => true, + fetch: async ({ soClient }) => { + return await collectDashboardTelemetry(soClient, embeddableService); + }, + schema: { + panels: { type: 'long' }, + panelsByValue: { type: 'long' }, + lensByValue: { + DYNAMIC_KEY: { + type: 'long', + }, + }, + visualizationByValue: { + DYNAMIC_KEY: { + type: 'long', + }, + }, + }, + }); + + usageCollection.registerCollector(dashboardCollector); +} diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index d486c06568c1..5a7da84439bf 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1,5 +1,29 @@ { "properties": { + "dashboard": { + "properties": { + "panels": { + "type": "long" + }, + "panelsByValue": { + "type": "long" + }, + "lensByValue": { + "properties": { + "DYNAMIC_KEY": { + "type": "long" + } + } + }, + "visualizationByValue": { + "properties": { + "DYNAMIC_KEY": { + "type": "long" + } + } + } + } + }, "kql": { "properties": { "optInCount": {