Skip to content

Commit 178bb1d

Browse files
dprokoptorkelo
authored andcommitted
Echo: mechanism for collecting custom events lazily (grafana#20365)
* Introduce Echo for collecting frontend metrics * Update public/app/core/services/echo/Echo.ts Co-Authored-By: Peter Holmberg <[email protected]> * Custom meta when adding event * Rename consumer to backend * Remove buffer from Echo * Minor tweaks * Update package.json * Update public/app/app.ts * Update public/app/app.ts * Collect paint metrics when collecting tti. Remove echoBackendFactory * Update yarn.lock * Move Echo interfaces to runtime * progress on meta and echo * Collect meta analytics events * Move MetaanalyticsBackend to enterprise repo * Fixed unit tests * Removed unused type from test * Fixed issues with chunk loading (reverted index-template changes) * Restored changes * Fixed webpack prod
1 parent 4b8a50e commit 178bb1d

File tree

24 files changed

+649
-260
lines changed

24 files changed

+649
-260
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@
256256
"tether": "1.4.5",
257257
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
258258
"tinycolor2": "1.4.1",
259+
"tti-polyfill": "0.2.2",
259260
"xss": "1.0.3"
260261
},
261262
"resolutions": {

packages/grafana-runtime/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './services';
22
export * from './config';
3+
export * from './types';
34
export { loadPluginCss, SystemJS } from './utils/plugin';
5+
export { reportMetaAnalytics } from './utils/analytics';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
interface SizeMeta {
2+
width: number;
3+
height: number;
4+
}
5+
6+
export interface EchoMeta {
7+
screenSize: SizeMeta;
8+
windowSize: SizeMeta;
9+
userAgent: string;
10+
url?: string;
11+
/**
12+
* A unique browser session
13+
*/
14+
sessionId: string;
15+
userLogin: string;
16+
userId: number;
17+
userSignedIn: boolean;
18+
ts: number;
19+
}
20+
21+
export interface EchoBackend<T extends EchoEvent = any, O = any> {
22+
options: O;
23+
supportedEvents: EchoEventType[];
24+
flush: () => void;
25+
addEvent: (event: T) => void;
26+
}
27+
28+
export interface EchoEvent<T extends EchoEventType = any, P = any> {
29+
type: EchoEventType;
30+
payload: P;
31+
meta: EchoMeta;
32+
}
33+
34+
export enum EchoEventType {
35+
Performance = 'performance',
36+
MetaAnalytics = 'meta-analytics',
37+
}
38+
39+
export interface EchoSrv {
40+
flush(): void;
41+
addBackend(backend: EchoBackend): void;
42+
addEvent<T extends EchoEvent>(event: Omit<T, 'meta'>, meta?: {}): void;
43+
}
44+
45+
let singletonInstance: EchoSrv;
46+
47+
export function setEchoSrv(instance: EchoSrv) {
48+
singletonInstance = instance;
49+
}
50+
51+
export function getEchoSrv(): EchoSrv {
52+
return singletonInstance;
53+
}
54+
55+
export const registerEchoBackend = (backend: EchoBackend) => {
56+
getEchoSrv().addBackend(backend);
57+
};

packages/grafana-runtime/src/services/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './backendSrv';
22
export * from './AngularLoader';
33
export * from './dataSourceSrv';
44
export * from './LocationSrv';
5+
export * from './EchoSrv';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { EchoEvent, EchoEventType } from '../services/EchoSrv';
2+
3+
export interface MetaAnalyticsEventPayload {
4+
eventName: string;
5+
dashboardId?: number;
6+
dashboardUid?: string;
7+
dashboardName?: string;
8+
folderName?: string;
9+
panelId?: number;
10+
panelName?: string;
11+
datasourceName: string;
12+
datasourceId?: number;
13+
error?: string;
14+
duration: number;
15+
dataSize?: number;
16+
}
17+
18+
export interface MetaAnalyticsEvent extends EchoEvent<EchoEventType.MetaAnalytics, MetaAnalyticsEventPayload> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './analytics';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
2+
import { MetaAnalyticsEvent, MetaAnalyticsEventPayload } from '../types/analytics';
3+
4+
export const reportMetaAnalytics = (payload: MetaAnalyticsEventPayload) => {
5+
getEchoSrv().addEvent<MetaAnalyticsEvent>({
6+
type: EchoEventType.MetaAnalytics,
7+
payload,
8+
});
9+
};

packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ $btn-drag-image: '../img/grab_dark.svg';
187187
188188
$navbar-btn-gicon-brightness: brightness(0.5);
189189
190-
$btn-active-box-shadow: 0px 0px 4px rgba(255,120,10,0.5);
190+
$btn-active-box-shadow: 0px 0px 4px rgba(255, 120, 10, 0.5);
191191
192192
// Forms
193193
// -------------------------

public/app/app.ts

+26
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'vendor/angular-other/angular-strap';
1616
import $ from 'jquery';
1717
import angular from 'angular';
1818
import config from 'app/core/config';
19+
// @ts-ignore
20+
import ttiPolyfill from 'tti-polyfill';
1921
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
2022
import _ from 'lodash';
2123
import { AppEvents, setMarkdownOptions, setLocale } from '@grafana/data';
@@ -34,6 +36,10 @@ _.move = (array: [], fromIndex: number, toIndex: number) => {
3436
import { coreModule, angularModules } from 'app/core/core_module';
3537
import { registerAngularDirectives } from 'app/core/core';
3638
import { setupAngularRoutes } from 'app/routes/routes';
39+
import { setEchoSrv, registerEchoBackend } from '@grafana/runtime';
40+
import { Echo } from './core/services/echo/Echo';
41+
import { reportPerformance } from './core/services/echo/EchoSrv';
42+
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
3743

3844
import 'app/routes/GrafanaCtrl';
3945
import 'app/features/all';
@@ -163,6 +169,26 @@ export class GrafanaApp {
163169
importPluginModule(modulePath);
164170
}
165171
}
172+
173+
initEchoSrv() {
174+
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
175+
176+
ttiPolyfill.getFirstConsistentlyInteractive().then((tti: any) => {
177+
// Collecting paint metrics first
178+
const paintMetrics = performance.getEntriesByType('paint');
179+
180+
for (const metric of paintMetrics) {
181+
reportPerformance(metric.name, Math.round(metric.startTime + metric.duration));
182+
}
183+
reportPerformance('tti', tti);
184+
});
185+
186+
registerEchoBackend(new PerformanceBackend({}));
187+
188+
window.addEventListener('DOMContentLoaded', () => {
189+
reportPerformance('dcl', Math.round(performance.now()));
190+
});
191+
}
166192
}
167193

168194
export default new GrafanaApp();

public/app/core/components/sidemenu/BottomNavLinks.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const setup = (propOverrides?: object) => {
2121
orgCount: 2,
2222
orgRole: '',
2323
orgId: 1,
24+
login: 'hello',
2425
orgName: 'Grafana',
2526
timezone: 'UTC',
2627
helpFlags1: 1,

public/app/core/services/context_srv.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class User {
99
orgRole: any;
1010
orgId: number;
1111
orgName: string;
12+
login: string;
1213
orgCount: number;
1314
timezone: string;
1415
helpFlags1: number;

public/app/core/services/echo/Echo.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { EchoBackend, EchoMeta, EchoEvent, EchoSrv } from '@grafana/runtime';
2+
import { contextSrv } from '../context_srv';
3+
4+
interface EchoConfig {
5+
// How often should metrics be reported
6+
flushInterval: number;
7+
// Enables debug mode
8+
debug: boolean;
9+
}
10+
11+
/**
12+
* Echo is a service for collecting events from Grafana client-app
13+
* It collects events, distributes them across registered backend and flushes once per configured interval
14+
* It's up to the registered backend to decide what to do with a given type of metric
15+
*/
16+
export class Echo implements EchoSrv {
17+
private config: EchoConfig = {
18+
flushInterval: 10000, // By default Echo flushes every 10s
19+
debug: false,
20+
};
21+
22+
private backends: EchoBackend[] = [];
23+
// meta data added to every event collected
24+
25+
constructor(config?: Partial<EchoConfig>) {
26+
this.config = {
27+
...this.config,
28+
...config,
29+
};
30+
setInterval(this.flush, this.config.flushInterval);
31+
}
32+
33+
logDebug = (...msg: any) => {
34+
if (this.config.debug) {
35+
// tslint:disable-next-line
36+
// console.debug('ECHO:', ...msg);
37+
}
38+
};
39+
40+
flush = () => {
41+
for (const backend of this.backends) {
42+
backend.flush();
43+
}
44+
};
45+
46+
addBackend = (backend: EchoBackend) => {
47+
this.logDebug('Adding backend', backend);
48+
this.backends.push(backend);
49+
};
50+
51+
addEvent = <T extends EchoEvent>(event: Omit<T, 'meta'>, _meta?: {}) => {
52+
const meta = this.getMeta();
53+
const _event = {
54+
...event,
55+
meta: {
56+
...meta,
57+
..._meta,
58+
},
59+
};
60+
61+
for (const backend of this.backends) {
62+
if (backend.supportedEvents.length === 0 || backend.supportedEvents.indexOf(_event.type) > -1) {
63+
backend.addEvent(_event);
64+
}
65+
}
66+
67+
this.logDebug('Adding event', _event);
68+
};
69+
70+
getMeta = (): EchoMeta => {
71+
return {
72+
sessionId: '',
73+
userId: contextSrv.user.id,
74+
userLogin: contextSrv.user.login,
75+
userSignedIn: contextSrv.user.isSignedIn,
76+
screenSize: {
77+
width: window.innerWidth,
78+
height: window.innerHeight,
79+
},
80+
windowSize: {
81+
width: window.screen.width,
82+
height: window.screen.height,
83+
},
84+
userAgent: window.navigator.userAgent,
85+
ts: performance.now(),
86+
url: window.location.href,
87+
};
88+
};
89+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getEchoSrv, EchoEventType } from '@grafana/runtime';
2+
import { PerformanceEvent } from './backends/PerformanceBackend';
3+
4+
export const reportPerformance = (metric: string, value: number) => {
5+
getEchoSrv().addEvent<PerformanceEvent>({
6+
type: EchoEventType.Performance,
7+
payload: {
8+
metricName: metric,
9+
duration: value,
10+
},
11+
});
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime';
2+
3+
export interface PerformanceEventPayload {
4+
metricName: string;
5+
duration: number;
6+
}
7+
8+
export interface PerformanceEvent extends EchoEvent<EchoEventType.Performance, PerformanceEventPayload> {}
9+
10+
export interface PerformanceBackendOptions {
11+
url?: string;
12+
}
13+
14+
/**
15+
* Echo's performance metrics consumer
16+
* Reports performance metrics to given url (TODO)
17+
*/
18+
export class PerformanceBackend implements EchoBackend<PerformanceEvent, PerformanceBackendOptions> {
19+
private buffer: PerformanceEvent[] = [];
20+
supportedEvents = [EchoEventType.Performance];
21+
22+
constructor(public options: PerformanceBackendOptions) {}
23+
24+
addEvent = (e: EchoEvent) => {
25+
this.buffer.push(e);
26+
};
27+
28+
flush = () => {
29+
if (this.buffer.length === 0) {
30+
return;
31+
}
32+
33+
const result = {
34+
metrics: this.buffer,
35+
};
36+
37+
// Currently we don have API for sending the metrics hence loging to console in dev environment
38+
if (process.env.NODE_ENV === 'development') {
39+
console.log('PerformanceBackend flushing:', result);
40+
}
41+
42+
this.buffer = [];
43+
44+
// TODO: Enable backend request when we have metrics API
45+
// if (this.options.url) {
46+
// getBackendSrv().post(this.options.url, result);
47+
// }
48+
};
49+
}

public/app/features/dashboard/state/PanelQueryRunner.test.ts

+8-17
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import { PanelQueryRunner } from './PanelQueryRunner';
22
import { PanelData, DataQueryRequest, dateTime, ScopedVars } from '@grafana/data';
3-
import { PanelModel } from './PanelModel';
3+
import { DashboardModel } from './index';
4+
import { setEchoSrv } from '@grafana/runtime';
5+
import { Echo } from '../../../core/services/echo/Echo';
46

57
jest.mock('app/core/services/backend_srv');
68

7-
// Defined within setup functions
8-
const panelsForCurrentDashboardMock: { [key: number]: PanelModel } = {};
9+
const dashboardModel = new DashboardModel({
10+
panels: [{ id: 1, type: 'graph' }],
11+
});
912

1013
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
1114
getDashboardSrv: () => {
1215
return {
13-
getCurrent: () => {
14-
return {
15-
getPanelById: (id: number) => {
16-
return panelsForCurrentDashboardMock[id];
17-
},
18-
};
19-
},
16+
getCurrent: () => dashboardModel,
2017
};
2118
},
2219
}));
@@ -68,6 +65,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
6865
};
6966

7067
beforeEach(async () => {
68+
setEchoSrv(new Echo());
7169
setupFn();
7270

7371
const datasource: any = {
@@ -103,13 +101,6 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
103101
},
104102
});
105103

106-
panelsForCurrentDashboardMock[1] = {
107-
id: 1,
108-
getQueryRunner: () => {
109-
return ctx.runner;
110-
},
111-
} as PanelModel;
112-
113104
ctx.events = [];
114105
ctx.runner.run(args);
115106
});

0 commit comments

Comments
 (0)