[Dashboard] Adds dashboard collector for byValue panels (#85867)

* Adds dashboard collector for byValue panels

* Fix telemetry schema

* Remove unused import
This commit is contained in:
Corey Robertson 2020-12-15 16:26:03 -05:00 committed by GitHub
parent 74ae348f2d
commit 6672e26fe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 408 additions and 0 deletions

View file

@ -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 {};
}

View file

@ -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);
});
});
});

View file

@ -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<ISavedObjectsRepository, 'find'>,
embeddableService: EmbeddablePersistableStateService
) {
const collectorData = getEmptyTelemetryData();
const dashboards = await savedObjectClient.find<SavedObjectAttributes>({
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;
}

View file

@ -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<DashboardCollectorData>({
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);
}

View file

@ -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": {