Skip to content

Commit

Permalink
[Controls] Collect Telemetry (#130498)
Browse files Browse the repository at this point in the history
* collect telemetry for controls
  • Loading branch information
ThomThomson authored Apr 20, 2022
1 parent 0082e0c commit 3b37b27
Show file tree
Hide file tree
Showing 25 changed files with 566 additions and 136 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pageLoadAssetSize:
reporting: 57003
visTypeHeatmap: 25340
expressionGauge: 25000
controls: 34788
controls: 40000
expressionPartitionVis: 26338
sharedUX: 16225
ux: 20784
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,7 @@
* Side Public License, v 1.
*/

import { ControlGroupInput } from '..';
import { ControlStyle, ControlWidth } from '../types';

export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine';

export const getDefaultControlGroupInput = (): Omit<ControlGroupInput, 'id'> => ({
panels: {},
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
controlStyle: DEFAULT_CONTROL_STYLE,
chainingSystem: 'HIERARCHICAL',
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
ignoreValidations: false,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,53 @@
*/

import { SerializableRecord } from '@kbn/utility-types';
import { ControlGroupInput, getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
import { RawControlGroupAttributes } from '../types';
import deepEqual from 'fast-deep-equal';

export const getDefaultDashboardControlGroupInput = getDefaultControlGroupInput;
import { pick } from 'lodash';
import { ControlGroupInput } from '..';
import { DEFAULT_CONTROL_STYLE, DEFAULT_CONTROL_WIDTH } from './control_group_constants';
import { PersistableControlGroupInput, RawControlGroupAttributes } from './types';

export const controlGroupInputToRawAttributes = (
const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
if (!jsonString && typeof jsonString !== 'string') return;
try {
return JSON.parse(jsonString) as OutType;
} catch {
return;
}
};

export const getDefaultControlGroupInput = (): Omit<ControlGroupInput, 'id'> => ({
panels: {},
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
controlStyle: DEFAULT_CONTROL_STYLE,
chainingSystem: 'HIERARCHICAL',
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
ignoreValidations: false,
},
});

export const persistableControlGroupInputIsEqual = (
a: PersistableControlGroupInput | undefined,
b: PersistableControlGroupInput | undefined
) => {
const defaultInput = getDefaultControlGroupInput();
const inputA = {
...defaultInput,
...pick(a, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
};
const inputB = {
...defaultInput,
...pick(b, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
};
if (deepEqual(inputA, inputB)) return true;
return false;
};

export const controlGroupInputToRawControlGroupAttributes = (
controlGroupInput: Omit<ControlGroupInput, 'id'>
): RawControlGroupAttributes => {
return {
Expand All @@ -23,16 +64,7 @@ export const controlGroupInputToRawAttributes = (
};
};

const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
if (!jsonString && typeof jsonString !== 'string') return;
try {
return JSON.parse(jsonString) as OutType;
} catch {
return;
}
};

export const rawAttributesToControlGroupInput = (
export const rawControlGroupAttributesToControlGroupInput = (
rawControlGroupAttributes: RawControlGroupAttributes
): Omit<ControlGroupInput, 'id'> | undefined => {
const defaultControlGroupInput = getDefaultControlGroupInput();
Expand All @@ -50,7 +82,7 @@ export const rawAttributesToControlGroupInput = (
};
};

export const rawAttributesToSerializable = (
export const rawControlGroupAttributesToSerializable = (
rawControlGroupAttributes: Omit<RawControlGroupAttributes, 'id'>
): SerializableRecord => {
const defaultControlGroupInput = getDefaultControlGroupInput();
Expand All @@ -62,7 +94,7 @@ export const rawAttributesToSerializable = (
};
};

export const serializableToRawAttributes = (
export const serializableToRawControlGroupAttributes = (
serializable: SerializableRecord
): Omit<RawControlGroupAttributes, 'id' | 'type'> => {
return {
Expand Down
33 changes: 33 additions & 0 deletions src/plugins/controls/common/control_group/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,36 @@ export interface ControlGroupInput extends EmbeddableInput, ControlInput {
controlStyle: ControlStyle;
panels: ControlsPanels;
}

// only parts of the Control Group Input should be persisted
export type PersistableControlGroupInput = Pick<
ControlGroupInput,
'panels' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettings'
>;

// panels are json stringified for storage in a saved object.
export type RawControlGroupAttributes = Omit<
PersistableControlGroupInput,
'panels' | 'ignoreParentSettings'
> & {
ignoreParentSettingsJSON: string;
panelsJSON: string;
};
export interface ControlGroupTelemetry {
total: number;
chaining_system: {
[key: string]: number;
};
label_position: {
[key: string]: number;
};
ignore_settings: {
[key: string]: number;
};
by_type: {
[key: string]: {
total: number;
details: { [key: string]: number };
};
};
}
39 changes: 31 additions & 8 deletions src/plugins/controls/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,37 @@
*/

export type { ControlWidth } from './types';
export type { ControlPanelState, ControlsPanels, ControlGroupInput } from './control_group/types';
export type { OptionsListEmbeddableInput } from './control_types/options_list/types';
export type { RangeSliderEmbeddableInput } from './control_types/range_slider/types';

export { CONTROL_GROUP_TYPE } from './control_group/types';
export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types';
export { RANGE_SLIDER_CONTROL } from './control_types/range_slider/types';

export { getDefaultControlGroupInput } from './control_group/control_group_constants';
// Control Group exports
export {
CONTROL_GROUP_TYPE,
type ControlPanelState,
type ControlsPanels,
type ControlGroupInput,
type ControlGroupTelemetry,
type RawControlGroupAttributes,
type PersistableControlGroupInput,
} from './control_group/types';
export {
controlGroupInputToRawControlGroupAttributes,
rawControlGroupAttributesToControlGroupInput,
rawControlGroupAttributesToSerializable,
serializableToRawControlGroupAttributes,
persistableControlGroupInputIsEqual,
getDefaultControlGroupInput,
} from './control_group/control_group_persistence';
export {
DEFAULT_CONTROL_WIDTH,
DEFAULT_CONTROL_STYLE,
} from './control_group/control_group_constants';

// Control Type exports
export {
OPTIONS_LIST_CONTROL,
type OptionsListEmbeddableInput,
} from './control_types/options_list/types';
export {
type RangeSliderEmbeddableInput,
RANGE_SLIDER_CONTROL,
} from './control_types/range_slider/types';
export { TIME_SLIDER_CONTROL } from './control_types/time_slider/types';
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ import { ControlStyle, ControlWidth } from '../../types';
import { ParentIgnoreSettings } from '../..';
import { ControlsPanels } from '../types';
import { ControlGroupInput } from '..';
import {
DEFAULT_CONTROL_WIDTH,
getDefaultControlGroupInput,
} from '../../../common/control_group/control_group_constants';
import { DEFAULT_CONTROL_WIDTH, getDefaultControlGroupInput } from '../../../common';

interface EditControlGroupProps {
initialInput: ControlGroupInput;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
createControlGroupExtract,
createControlGroupInject,
} from '../../../common/control_group/control_group_persistable_state';
import { getDefaultControlGroupInput } from '../../../common/control_group/control_group_constants';
import { getDefaultControlGroupInput } from '../../../common';

export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition {
public readonly isContainerType = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createControlGroupInject,
migrations,
} from '../../common/control_group/control_group_persistable_state';
import { controlGroupTelemetry } from './control_group_telemetry';

export const controlGroupContainerPersistableStateServiceFactory = (
persistableStateService: EmbeddablePersistableStateService
Expand All @@ -22,6 +23,7 @@ export const controlGroupContainerPersistableStateServiceFactory = (
id: CONTROL_GROUP_TYPE,
extract: createControlGroupExtract(persistableStateService),
inject: createControlGroupInject(persistableStateService),
telemetry: controlGroupTelemetry,
migrations,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ControlGroupTelemetry, RawControlGroupAttributes } from '../../common';
import { controlGroupTelemetry, initializeControlGroupTelemetry } from './control_group_telemetry';

// controls attributes with all settings ignored + 3 options lists + hierarchical chaining + label above
const rawControlAttributes1: RawControlGroupAttributes = {
controlStyle: 'twoLine',
chainingSystem: 'NONE',
panelsJSON:
'{"6fc71ac6-62f9-4ff4-bf5a-d1e066065376":{"order":0,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"6fc71ac6-62f9-4ff4-bf5a-d1e066065376","enhancements":{}}},"1ca90451-908b-4eae-ac4d-535f2e30c4ad":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestAirportID","fieldName":"DestAirportID","id":"1ca90451-908b-4eae-ac4d-535f2e30c4ad","enhancements":{}}},"71086bac-316d-415f-8aa8-b9a921bc7f58":{"order":1,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestRegion","fieldName":"DestRegion","id":"71086bac-316d-415f-8aa8-b9a921bc7f58","enhancements":{}}}}',
ignoreParentSettingsJSON:
'{"ignoreFilters":true,"ignoreQuery":true,"ignoreTimerange":true,"ignoreValidations":true}',
};

// controls attributes with some settings ignored + 2 range sliders, 1 time slider + No chaining + label inline
const rawControlAttributes2: RawControlGroupAttributes = {
controlStyle: 'oneLine',
chainingSystem: 'NONE',
panelsJSON:
'{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"f6b076c6-9ef5-483e-b08d-d313d60d4b8c":{"order":2,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceMiles","fieldName":"DistanceMiles","id":"f6b076c6-9ef5-483e-b08d-d313d60d4b8c","enhancements":{}}}}',
ignoreParentSettingsJSON:
'{"ignoreFilters":true,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
};

// controls attributes with no settings ignored + 2 options lists, 1 range slider, 1 time slider + hierarchical chaining + label inline
const rawControlAttributes3: RawControlGroupAttributes = {
controlStyle: 'oneLine',
chainingSystem: 'HIERARCHICAL',
panelsJSON:
'{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"ee325e9e-6ec1-41f9-953f-423d59850d44":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"ee325e9e-6ec1-41f9-953f-423d59850d44","enhancements":{}}},"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b":{"order":3,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestCityName","fieldName":"DestCityName","id":"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b","enhancements":{}}}}',
ignoreParentSettingsJSON:
'{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
};

describe('Initialize telemetry', () => {
test('initializes telemetry when given blank object', () => {
const initializedTelemetry = initializeControlGroupTelemetry({});
expect(initializedTelemetry.total).toBe(0);
expect(initializedTelemetry.chaining_system).toEqual({});
expect(initializedTelemetry.ignore_settings).toEqual({});
expect(initializedTelemetry.by_type).toEqual({});
});

test('initializes telemetry without overwriting any keys when given a partial telemetry object', () => {
const partialTelemetry: Partial<ControlGroupTelemetry> = {
total: 77,
chaining_system: { TESTCHAIN: 10, OTHERCHAIN: 1 },
by_type: { test1: { total: 10, details: {} } },
};
const initializedTelemetry = initializeControlGroupTelemetry(partialTelemetry);
expect(initializedTelemetry.total).toBe(77);
expect(initializedTelemetry.chaining_system).toEqual({ TESTCHAIN: 10, OTHERCHAIN: 1 });
expect(initializedTelemetry.ignore_settings).toEqual({});
expect(initializedTelemetry.by_type).toEqual({ test1: { total: 10, details: {} } });
expect(initializedTelemetry.label_position).toEqual({});
});

test('initiailizes telemetry without overwriting any keys when given a completed telemetry object', () => {
const partialTelemetry: Partial<ControlGroupTelemetry> = {
total: 5,
chaining_system: { TESTCHAIN: 10, OTHERCHAIN: 1 },
by_type: { test1: { total: 10, details: {} } },
ignore_settings: { ignoreValidations: 12 },
label_position: { inline: 10, above: 12 },
};
const initializedTelemetry = initializeControlGroupTelemetry(partialTelemetry);
expect(initializedTelemetry.total).toBe(5);
expect(initializedTelemetry.chaining_system).toEqual({ TESTCHAIN: 10, OTHERCHAIN: 1 });
expect(initializedTelemetry.ignore_settings).toEqual({ ignoreValidations: 12 });
expect(initializedTelemetry.by_type).toEqual({ test1: { total: 10, details: {} } });
expect(initializedTelemetry.label_position).toEqual({ inline: 10, above: 12 });
});
});

describe('Control group telemetry function', () => {
let finalTelemetry: ControlGroupTelemetry;

beforeAll(() => {
const allControlGroups = [rawControlAttributes1, rawControlAttributes2, rawControlAttributes3];

finalTelemetry = allControlGroups.reduce<ControlGroupTelemetry>(
(telemetrySoFar, rawControlGroupAttributes) => {
return controlGroupTelemetry(
rawControlGroupAttributes,
telemetrySoFar
) as ControlGroupTelemetry;
},
{} as ControlGroupTelemetry
);
});

test('counts all telemetry over multiple runs', () => {
expect(finalTelemetry.total).toBe(10);
});

test('counts control types over multiple runs.', () => {
expect(finalTelemetry.by_type).toEqual({
optionsListControl: {
details: {},
total: 5,
},
rangeSliderControl: {
details: {},
total: 3,
},
timeSlider: {
details: {},
total: 2,
},
});
});

test('collects ignore settings over multiple runs.', () => {
expect(finalTelemetry.ignore_settings).toEqual({
ignoreFilters: 2,
ignoreQuery: 1,
ignoreTimerange: 1,
ignoreValidations: 1,
});
});

test('counts various chaining systems over multiple runs.', () => {
expect(finalTelemetry.chaining_system).toEqual({
HIERARCHICAL: 1,
NONE: 2,
});
});

test('counts label positions over multiple runs.', () => {
expect(finalTelemetry.label_position).toEqual({
oneLine: 2,
twoLine: 1,
});
});
});
Loading

0 comments on commit 3b37b27

Please sign in to comment.