mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* [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:
parent
3ad38669c8
commit
33b9dafde9
4 changed files with 208 additions and 258 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue