[7.17] [Dashboard] [Telemetry] Report panels in dashboards by type (#130166) (#130805)

* [Dashboard] [Telemetry] Report panels in dashboards by type (#130166)

* Add new panel telemetry

* Restructure panel data to sort by type first

* Fix jest tests + slight restructure

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 64d7bccacf)

* Fix conflicts

Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
Co-authored-by: heenawter <hannah.wright@elastic.co>
This commit is contained in:
Kibana Machine 2022-04-21 15:08:28 -05:00 committed by GitHub
parent 3ad38669c8
commit 33b9dafde9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 208 additions and 258 deletions

View file

@ -7,12 +7,10 @@
*/
import { SavedDashboardPanel730ToLatest } from '../../common';
import {
collectDashboardInfo,
getEmptyTelemetryData,
collectByValueVisualizationInfo,
collectByValueLensInfo,
} from './dashboard_telemetry';
import { getEmptyDashboardData, collectPanelsByType } from './dashboard_telemetry';
import { EmbeddableStateWithType } from '../../../embeddable/common';
import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks';
import { SavedObjectReference } from 'kibana/public';
const visualizationType1ByValue = {
embeddableConfig: {
@ -31,6 +29,7 @@ const visualizationType2ByValue = {
},
type: 'visualization',
} as unknown as SavedDashboardPanel730ToLatest;
const visualizationType2ByReference = {
...visualizationType2ByValue,
id: '11111',
@ -44,6 +43,7 @@ const lensTypeAByValue = {
},
},
} as unknown as SavedDashboardPanel730ToLatest;
const lensTypeAByReference = {
...lensTypeAByValue,
id: '22222',
@ -93,91 +93,101 @@ const lensXYSeriesB = {
},
} as unknown as SavedDashboardPanel730ToLatest;
const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock();
describe('dashboard telemetry', () => {
beforeAll(() => {
embeddablePersistableStateService.extract.mockImplementationOnce(
(state: EmbeddableStateWithType) => {
const { HARDCODED_ID, ...restOfState } = state as unknown as Record<string, unknown>;
return {
state: restOfState as EmbeddableStateWithType,
references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }],
};
}
);
embeddablePersistableStateService.inject.mockImplementationOnce(
(state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
const ref = references.find((r: SavedObjectReference) => r.name === 'refName');
return {
...state,
HARDCODED_ID: ref!.id,
};
}
);
});
it('collects information about dashboard panels', () => {
const panels = [
visualizationType1ByValue,
visualizationType2ByValue,
visualizationType2ByReference,
];
const collectorData = getEmptyTelemetryData();
const collectorData = getEmptyDashboardData();
collectPanelsByType(panels, collectorData, embeddablePersistableStateService);
collectDashboardInfo(panels, collectorData);
expect(collectorData.panels).toBe(panels.length);
expect(collectorData.panelsByValue).toBe(2);
expect(collectorData.panels.total).toBe(panels.length);
expect(collectorData.panels.by_value).toBe(2);
expect(collectorData.panels.by_reference).toBe(1);
});
describe('visualizations', () => {
it('collects information about by value visualizations', () => {
const panels = [
visualizationType1ByValue,
visualizationType1ByValue,
visualizationType2ByValue,
visualizationType2ByReference,
];
it('collects information about visualizations', () => {
const panels = [
visualizationType1ByValue,
visualizationType1ByValue,
visualizationType2ByValue,
visualizationType2ByReference,
];
const collectorData = getEmptyTelemetryData();
const collectorData = getEmptyDashboardData();
collectPanelsByType(panels, collectorData, embeddablePersistableStateService);
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);
});
expect(collectorData.panels.by_type.visualization.total).toBe(panels.length);
expect(collectorData.panels.by_type.visualization.by_value).toBe(3);
expect(collectorData.panels.by_type.visualization.by_reference).toBe(1);
});
describe('lens', () => {
it('collects information about by value lens', () => {
const panels = [
lensTypeAByValue,
lensTypeAByValue,
lensTypeAByValue,
lensTypeAByReference,
lensXYSeriesA,
lensXYSeriesA,
lensXYSeriesB,
];
it('collects information about lens', () => {
const panels = [
lensTypeAByValue,
lensTypeAByValue,
lensTypeAByValue,
lensTypeAByReference,
lensXYSeriesA,
lensXYSeriesA,
lensXYSeriesB,
];
const collectorData = getEmptyTelemetryData();
const collectorData = getEmptyDashboardData();
collectPanelsByType(panels, collectorData, embeddablePersistableStateService);
collectByValueLensInfo(panels, collectorData);
expect(collectorData.panels.by_type.lens.total).toBe(panels.length);
expect(collectorData.panels.by_type.lens.by_value).toBe(6);
expect(collectorData.panels.by_type.lens.by_reference).toBe(1);
});
expect(collectorData.lensByValue.a).toBe(3);
expect(collectorData.lensByValue.seriesA).toBe(2);
expect(collectorData.lensByValue.seriesB).toBe(1);
expect(collectorData.lensByValue.formula).toBe(1);
});
it('collects information about a mix of panel types', () => {
const panels = [
visualizationType1ByValue,
visualizationType1ByValue,
visualizationType2ByReference,
lensTypeAByValue,
lensTypeAByValue,
lensTypeAByValue,
lensTypeAByReference,
lensXYSeriesA,
];
it('handles misshapen lens panels', () => {
const badPanel = {
type: 'lens',
embeddableConfig: {
oops: 'no visualization type',
},
} as unknown as SavedDashboardPanel730ToLatest;
const collectorData = getEmptyDashboardData();
collectPanelsByType(panels, collectorData, embeddablePersistableStateService);
const panels = [badPanel, lensTypeAByValue];
const collectorData = getEmptyTelemetryData();
collectByValueLensInfo(panels, collectorData);
expect(collectorData.lensByValue.a).toBe(1);
});
expect(collectorData.panels.total).toBe(panels.length);
expect(collectorData.panels.by_type.lens.total).toBe(5);
expect(collectorData.panels.by_type.lens.by_value).toBe(4);
expect(collectorData.panels.by_type.lens.by_reference).toBe(1);
expect(collectorData.panels.by_type.visualization.total).toBe(3);
expect(collectorData.panels.by_type.visualization.by_value).toBe(2);
expect(collectorData.panels.by_type.visualization.by_reference).toBe(1);
});
});

View file

@ -10,151 +10,70 @@ 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;
};
datasourceStates?: {
indexpattern?: {
layers: Record<
string,
{
columns: Record<string, { operationType: string }>;
}
>;
};
export interface DashboardCollectorData {
panels: {
total: number;
by_reference: number;
by_value: number;
by_type: {
[key: string]: {
total: number;
by_reference: number;
by_value: number;
details: {
[key: string]: number;
};
};
};
};
}
export interface DashboardCollectorData {
panels: number;
panelsByValue: number;
lensByValue: {
[key: string]: number;
};
visualizationByValue: {
[key: string]: number;
};
embeddable: {
[key: string]: number;
};
}
export const getEmptyTelemetryData = (): DashboardCollectorData => ({
panels: 0,
panelsByValue: 0,
lensByValue: {},
visualizationByValue: {},
embeddable: {},
export const getEmptyDashboardData = (): DashboardCollectorData => ({
panels: {
total: 0,
by_reference: 0,
by_value: 0,
by_type: {},
},
});
type DashboardCollectorFunction = (
panels: SavedDashboardPanel730ToLatest[],
collectorData: DashboardCollectorData
) => void;
export const getEmptyPanelTypeData = () => ({
total: 0,
by_reference: 0,
by_value: 0,
details: {},
});
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;
const hasFormula = Object.values(
lensPanel.embeddableConfig.attributes.state?.datasourceStates?.indexpattern?.layers || {}
).some((layer) =>
Object.values(layer.columns).some((column) => column.operationType === 'formula')
);
if (hasFormula && !collectorData.lensByValue.formula) {
collectorData.lensByValue.formula = 0;
}
if (hasFormula) {
collectorData.lensByValue.formula++;
}
}
}
};
export const collectForPanels: DashboardCollectorFunction = (panels, collectorData) => {
collectDashboardInfo(panels, collectorData);
collectByValueVisualizationInfo(panels, collectorData);
collectByValueLensInfo(panels, collectorData);
};
export const collectEmbeddableData = (
export const collectPanelsByType = (
panels: SavedDashboardPanel730ToLatest[],
collectorData: DashboardCollectorData,
embeddableService: EmbeddablePersistableStateService
) => {
collectorData.panels.total += panels.length;
for (const panel of panels) {
collectorData.embeddable = embeddableService.telemetry(
const type = panel.type;
if (!collectorData.panels.by_type[type]) {
collectorData.panels.by_type[type] = getEmptyPanelTypeData();
}
collectorData.panels.by_type[type].total += 1;
if (panel.id === undefined) {
collectorData.panels.by_value += 1;
collectorData.panels.by_type[type].by_value += 1;
} else {
collectorData.panels.by_reference += 1;
collectorData.panels.by_type[type].by_reference += 1;
}
// the following "details" need a follow-up that will actually properly consolidate
// the data from all embeddables - right now, the only data that is kept is the
// telemetry for the **final** embeddable of that type
collectorData.panels.by_type[type].details = embeddableService.telemetry(
{
...panel.embeddableConfig,
id: panel.id || '',
type: panel.type,
},
collectorData.embeddable
collectorData.panels.by_type[type].details
);
}
};
@ -163,7 +82,7 @@ export async function collectDashboardTelemetry(
savedObjectClient: Pick<ISavedObjectsRepository, 'find'>,
embeddableService: EmbeddablePersistableStateService
) {
const collectorData = getEmptyTelemetryData();
const collectorData = getEmptyDashboardData();
const dashboards = await savedObjectClient.find<SavedObjectAttributes>({
type: 'dashboard',
});
@ -177,8 +96,7 @@ export async function collectDashboardTelemetry(
attributes.panelsJSON as string
) as unknown as SavedDashboardPanel730ToLatest[];
collectForPanels(panels, collectorData);
collectEmbeddableData(panels, collectorData, embeddableService);
collectPanelsByType(panels, collectorData, embeddableService);
}
return collectorData;

View file

@ -22,32 +22,41 @@ export function registerDashboardUsageCollector(
return await collectDashboardTelemetry(soClient, embeddableService);
},
schema: {
panels: { type: 'long' },
panelsByValue: { type: 'long' },
lensByValue: {
DYNAMIC_KEY: {
type: 'long',
_meta: {
description:
'Collection of telemetry metrics for Lens visualizations, which are added to dashboard by "value".',
},
},
},
visualizationByValue: {
DYNAMIC_KEY: {
type: 'long',
_meta: {
description:
'Collection of telemetry metrics for visualizations, which are added to dashboard by "value".',
},
},
},
embeddable: {
DYNAMIC_KEY: {
type: 'long',
_meta: {
description:
'Collection of telemetry metrics that embeddable service reports. Embeddable service internally calls each embeddable, which in turn calls its dynamic actions, which calls each drill down attached to that embeddable.',
panels: {
total: { type: 'long' },
by_reference: { type: 'long' },
by_value: { type: 'long' },
by_type: {
DYNAMIC_KEY: {
total: {
type: 'long',
_meta: {
description: 'The number of panels that have been added to all dashboards.',
},
},
by_reference: {
type: 'long',
_meta: {
description:
'The number of "by reference" panels that have been added to all dashboards.',
},
},
by_value: {
type: 'long',
_meta: {
description:
'The number of "by value" panels that have been added to all dashboards.',
},
},
details: {
DYNAMIC_KEY: {
type: 'long',
_meta: {
description:
'Collection of telemetry metrics that embeddable service reports. Embeddable service internally calls each embeddable, which in turn calls its dynamic actions, which calls each drill down attached to that embeddable.',
},
},
},
},
},
},

View file

@ -3,37 +3,50 @@
"dashboard": {
"properties": {
"panels": {
"type": "long"
},
"panelsByValue": {
"type": "long"
},
"lensByValue": {
"properties": {
"DYNAMIC_KEY": {
"type": "long",
"_meta": {
"description": "Collection of telemetry metrics for Lens visualizations, which are added to dashboard by \"value\"."
}
}
}
},
"visualizationByValue": {
"properties": {
"DYNAMIC_KEY": {
"type": "long",
"_meta": {
"description": "Collection of telemetry metrics for visualizations, which are added to dashboard by \"value\"."
}
}
}
},
"embeddable": {
"properties": {
"DYNAMIC_KEY": {
"type": "long",
"_meta": {
"description": "Collection of telemetry metrics that embeddable service reports. Embeddable service internally calls each embeddable, which in turn calls its dynamic actions, which calls each drill down attached to that embeddable."
"total": {
"type": "long"
},
"by_reference": {
"type": "long"
},
"by_value": {
"type": "long"
},
"by_type": {
"properties": {
"DYNAMIC_KEY": {
"properties": {
"total": {
"type": "long",
"_meta": {
"description": "The number of panels that have been added to all dashboards."
}
},
"by_reference": {
"type": "long",
"_meta": {
"description": "The number of \"by reference\" panels that have been added to all dashboards."
}
},
"by_value": {
"type": "long",
"_meta": {
"description": "The number of \"by value\" panels that have been added to all dashboards."
}
},
"details": {
"properties": {
"DYNAMIC_KEY": {
"type": "long",
"_meta": {
"description": "Collection of telemetry metrics that embeddable service reports. Embeddable service internally calls each embeddable, which in turn calls its dynamic actions, which calls each drill down attached to that embeddable."
}
}
}
}
}
}
}
}
}