Skip to content

Commit cc341b3

Browse files
authored
Telemetry for Dyanmic Actions (Drilldowns) (#84580)
* feat: ๐ŸŽธ set up telemetry for UiActions * feat: ๐ŸŽธ improve ui_actions_enhanced collector * feat: ๐ŸŽธ namespace ui actions telemetry stats * refactor: ๐Ÿ’ก improve dynamic actions collector setup * feat: ๐ŸŽธ add tests for dynamicActionsCollector * feat: ๐ŸŽธ collect dynamic action trigger statistics * refactor: ๐Ÿ’ก standartize metric naming * feat: ๐ŸŽธ aggregate action x trigger counts * test: ๐Ÿ’ add tests for factory stats * docs: โœ๏ธ add ui actions enhanced telemetry docs * fix: ๐Ÿ› revert type change * refactor: ๐Ÿ’ก make dynamic action stats global * refactor: ๐Ÿ’ก use global telemetry stats in action factories
1 parent 90a18cc commit cc341b3

File tree

7 files changed

+536
-7
lines changed

7 files changed

+536
-7
lines changed

โ€Žx-pack/plugins/ui_actions_enhanced/README.mdโ€Ž

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,66 @@
33
Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work.
44

55
- [__Dashboard drilldown user docs__](https://www.elastic.co/guide/en/kibana/master/drilldowns.html)
6+
7+
## Dynamic Actions Telemetry
8+
9+
Dynamic actions (drilldowns) report telemetry. Below is the summary of dynamic action metrics that are reported using telemetry.
10+
11+
### Dynamic action count
12+
13+
Total count of dynamic actions (drilldowns) on a saved object.
14+
15+
```
16+
dynamicActions.count
17+
```
18+
19+
### Count by factory ID
20+
21+
Count of active dynamic actions (drilldowns) on a saved object by factory ID (drilldown type).
22+
23+
```
24+
dynamicActions.actions.<factory_id>.count
25+
```
26+
27+
For example:
28+
29+
```
30+
dynamicActions.actions.DASHBOARD_TO_DASHBOARD_DRILLDOWN.count
31+
dynamicActions.actions.URL_DRILLDOWN.count
32+
```
33+
34+
### Count by trigger
35+
36+
Count of active dynamic actions (drilldowns) on a saved object by a trigger to which they are attached.
37+
38+
```
39+
dynamicActions.triggers.<trigger>.count
40+
```
41+
42+
For example:
43+
44+
```
45+
dynamicActions.triggers.VALUE_CLICK_TRIGGER.count
46+
dynamicActions.triggers.RANGE_SELECT_TRIGGER.count
47+
```
48+
49+
### Count by factory and trigger
50+
51+
Count of active dynamic actions (drilldowns) on a saved object by a factory ID and trigger ID.
52+
53+
```
54+
dynamicActions.action_triggers.<factory_id>_<trigger>.count
55+
```
56+
57+
For example:
58+
59+
```
60+
dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_VALUE_CLICK_TRIGGER.count
61+
dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_RANGE_SELECT_TRIGGER.count
62+
dynamicActions.action_triggers.URL_DRILLDOWN_VALUE_CLICK_TRIGGER.count
63+
```
64+
65+
### Factory metrics
66+
67+
Each dynamic action factory (drilldown type) can report its own stats, which is
68+
done using the `.telemetry()` method on dynamic action factories.

โ€Žx-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.tsโ€Ž

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddabl
88
import { SavedObjectReference } from '../../../../src/core/types';
99
import { ActionFactory, DynamicActionsState, SerializedEvent } from './types';
1010
import { SerializableState } from '../../../../src/plugins/kibana_utils/common';
11+
import { dynamicActionsCollector } from './telemetry/dynamic_actions_collector';
12+
import { dynamicActionFactoriesCollector } from './telemetry/dynamic_action_factories_collector';
1113

1214
export const dynamicActionEnhancement = (
1315
getActionFactory: (id: string) => undefined | ActionFactory
1416
): EnhancementRegistryDefinition => {
1517
return {
1618
id: 'dynamicActions',
17-
telemetry: (state: SerializableState, telemetry: Record<string, any>) => {
18-
let telemetryData = telemetry;
19-
(state as DynamicActionsState).events.forEach((event: SerializedEvent) => {
20-
const factory = getActionFactory(event.action.factoryId);
21-
if (factory) telemetryData = factory.telemetry(event, telemetryData);
22-
});
23-
return telemetryData;
19+
telemetry: (serializableState: SerializableState, stats: Record<string, any>) => {
20+
const state = serializableState as DynamicActionsState;
21+
stats = dynamicActionsCollector(state, stats);
22+
stats = dynamicActionFactoriesCollector(getActionFactory, state, stats);
23+
24+
return stats;
2425
},
2526
extract: (state: SerializableState) => {
2627
const references: SavedObjectReference[] = [];
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
/* eslint-disable @typescript-eslint/naming-convention */
8+
9+
import { dynamicActionFactoriesCollector } from './dynamic_action_factories_collector';
10+
import { DynamicActionsState } from '../../common';
11+
import { ActionFactory } from '../types';
12+
13+
type GetActionFactory = (id: string) => undefined | ActionFactory;
14+
15+
const factories: Record<string, ActionFactory> = {
16+
FACTORY_ID_1: ({
17+
id: 'FACTORY_ID_1',
18+
telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => {
19+
stats.myStat_1 = 1;
20+
stats.myStat_2 = 123;
21+
return stats;
22+
}),
23+
} as unknown) as ActionFactory,
24+
FACTORY_ID_2: ({
25+
id: 'FACTORY_ID_2',
26+
telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => stats),
27+
} as unknown) as ActionFactory,
28+
FACTORY_ID_3: ({
29+
id: 'FACTORY_ID_3',
30+
telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => {
31+
stats.myStat_1 = 2;
32+
stats.stringStat = 'abc';
33+
return stats;
34+
}),
35+
} as unknown) as ActionFactory,
36+
};
37+
38+
const getActionFactory: GetActionFactory = (id: string) => factories[id];
39+
40+
const state: DynamicActionsState = {
41+
events: [
42+
{
43+
eventId: 'eventId-1',
44+
triggers: ['TRIGGER_1'],
45+
action: {
46+
factoryId: 'FACTORY_ID_1',
47+
name: 'Click me!',
48+
config: {},
49+
},
50+
},
51+
{
52+
eventId: 'eventId-2',
53+
triggers: ['TRIGGER_2', 'TRIGGER_3'],
54+
action: {
55+
factoryId: 'FACTORY_ID_2',
56+
name: 'Click me, too!',
57+
config: {
58+
doCleanup: true,
59+
},
60+
},
61+
},
62+
{
63+
eventId: 'eventId-3',
64+
triggers: ['TRIGGER_4', 'TRIGGER_1'],
65+
action: {
66+
factoryId: 'FACTORY_ID_3',
67+
name: 'Go to documentation',
68+
config: {
69+
url: 'http://google.com',
70+
iamFeelingLucky: true,
71+
},
72+
},
73+
},
74+
],
75+
};
76+
77+
beforeEach(() => {
78+
Object.values(factories).forEach((factory) => {
79+
((factory.telemetry as unknown) as jest.SpyInstance).mockClear();
80+
});
81+
});
82+
83+
describe('dynamicActionFactoriesCollector', () => {
84+
test('returns empty stats when there are not dynamic actions', () => {
85+
const stats = dynamicActionFactoriesCollector(
86+
getActionFactory,
87+
{
88+
events: [],
89+
},
90+
{}
91+
);
92+
93+
expect(stats).toEqual({});
94+
});
95+
96+
test('calls .telemetry() method of a supplied factory', () => {
97+
const currentState = {
98+
events: [state.events[0]],
99+
};
100+
dynamicActionFactoriesCollector(getActionFactory, currentState, {});
101+
102+
const spy1 = (factories.FACTORY_ID_1.telemetry as unknown) as jest.SpyInstance;
103+
const spy2 = (factories.FACTORY_ID_2.telemetry as unknown) as jest.SpyInstance;
104+
105+
expect(spy1).toHaveBeenCalledTimes(1);
106+
expect(spy2).toHaveBeenCalledTimes(0);
107+
108+
expect(spy1.mock.calls[0][0]).toEqual(currentState.events[0]);
109+
expect(typeof spy1.mock.calls[0][1]).toBe('object');
110+
expect(!!spy1.mock.calls[0][1]).toBe(true);
111+
});
112+
113+
test('returns stats received from factory', () => {
114+
const currentState = {
115+
events: [state.events[0]],
116+
};
117+
const stats = dynamicActionFactoriesCollector(getActionFactory, currentState, {});
118+
119+
expect(stats).toEqual({
120+
myStat_1: 1,
121+
myStat_2: 123,
122+
});
123+
});
124+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { DynamicActionsState } from '../../common';
8+
import { ActionFactory } from '../types';
9+
10+
export const dynamicActionFactoriesCollector = (
11+
getActionFactory: (id: string) => undefined | ActionFactory,
12+
state: DynamicActionsState,
13+
stats: Record<string, any>
14+
): Record<string, any> => {
15+
for (const event of state.events) {
16+
const factory = getActionFactory(event.action.factoryId);
17+
18+
if (factory) {
19+
stats = factory.telemetry(event, stats);
20+
}
21+
}
22+
23+
return stats;
24+
};

0 commit comments

Comments
ย (0)