mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Echo: mechanism for collecting custom events lazily (#20365)
* Introduce Echo for collecting frontend metrics * Update public/app/core/services/echo/Echo.ts Co-Authored-By: Peter Holmberg <peterholmberg@users.noreply.github.com> * 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
This commit is contained in:
committed by
Torkel Ödegaard
parent
4b8a50e70b
commit
178bb1d3ab
@@ -256,6 +256,7 @@
|
|||||||
"tether": "1.4.5",
|
"tether": "1.4.5",
|
||||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||||
"tinycolor2": "1.4.1",
|
"tinycolor2": "1.4.1",
|
||||||
|
"tti-polyfill": "0.2.2",
|
||||||
"xss": "1.0.3"
|
"xss": "1.0.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './services';
|
export * from './services';
|
||||||
export * from './config';
|
export * from './config';
|
||||||
|
export * from './types';
|
||||||
export { loadPluginCss, SystemJS } from './utils/plugin';
|
export { loadPluginCss, SystemJS } from './utils/plugin';
|
||||||
|
export { reportMetaAnalytics } from './utils/analytics';
|
||||||
|
|||||||
57
packages/grafana-runtime/src/services/EchoSrv.ts
Normal file
57
packages/grafana-runtime/src/services/EchoSrv.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
interface SizeMeta {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EchoMeta {
|
||||||
|
screenSize: SizeMeta;
|
||||||
|
windowSize: SizeMeta;
|
||||||
|
userAgent: string;
|
||||||
|
url?: string;
|
||||||
|
/**
|
||||||
|
* A unique browser session
|
||||||
|
*/
|
||||||
|
sessionId: string;
|
||||||
|
userLogin: string;
|
||||||
|
userId: number;
|
||||||
|
userSignedIn: boolean;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EchoBackend<T extends EchoEvent = any, O = any> {
|
||||||
|
options: O;
|
||||||
|
supportedEvents: EchoEventType[];
|
||||||
|
flush: () => void;
|
||||||
|
addEvent: (event: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EchoEvent<T extends EchoEventType = any, P = any> {
|
||||||
|
type: EchoEventType;
|
||||||
|
payload: P;
|
||||||
|
meta: EchoMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EchoEventType {
|
||||||
|
Performance = 'performance',
|
||||||
|
MetaAnalytics = 'meta-analytics',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EchoSrv {
|
||||||
|
flush(): void;
|
||||||
|
addBackend(backend: EchoBackend): void;
|
||||||
|
addEvent<T extends EchoEvent>(event: Omit<T, 'meta'>, meta?: {}): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let singletonInstance: EchoSrv;
|
||||||
|
|
||||||
|
export function setEchoSrv(instance: EchoSrv) {
|
||||||
|
singletonInstance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEchoSrv(): EchoSrv {
|
||||||
|
return singletonInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const registerEchoBackend = (backend: EchoBackend) => {
|
||||||
|
getEchoSrv().addBackend(backend);
|
||||||
|
};
|
||||||
@@ -2,3 +2,4 @@ export * from './backendSrv';
|
|||||||
export * from './AngularLoader';
|
export * from './AngularLoader';
|
||||||
export * from './dataSourceSrv';
|
export * from './dataSourceSrv';
|
||||||
export * from './LocationSrv';
|
export * from './LocationSrv';
|
||||||
|
export * from './EchoSrv';
|
||||||
|
|||||||
18
packages/grafana-runtime/src/types/analytics.ts
Normal file
18
packages/grafana-runtime/src/types/analytics.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { EchoEvent, EchoEventType } from '../services/EchoSrv';
|
||||||
|
|
||||||
|
export interface MetaAnalyticsEventPayload {
|
||||||
|
eventName: string;
|
||||||
|
dashboardId?: number;
|
||||||
|
dashboardUid?: string;
|
||||||
|
dashboardName?: string;
|
||||||
|
folderName?: string;
|
||||||
|
panelId?: number;
|
||||||
|
panelName?: string;
|
||||||
|
datasourceName: string;
|
||||||
|
datasourceId?: number;
|
||||||
|
error?: string;
|
||||||
|
duration: number;
|
||||||
|
dataSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetaAnalyticsEvent extends EchoEvent<EchoEventType.MetaAnalytics, MetaAnalyticsEventPayload> {}
|
||||||
1
packages/grafana-runtime/src/types/index.ts
Normal file
1
packages/grafana-runtime/src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './analytics';
|
||||||
9
packages/grafana-runtime/src/utils/analytics.ts
Normal file
9
packages/grafana-runtime/src/utils/analytics.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
|
||||||
|
import { MetaAnalyticsEvent, MetaAnalyticsEventPayload } from '../types/analytics';
|
||||||
|
|
||||||
|
export const reportMetaAnalytics = (payload: MetaAnalyticsEventPayload) => {
|
||||||
|
getEchoSrv().addEvent<MetaAnalyticsEvent>({
|
||||||
|
type: EchoEventType.MetaAnalytics,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -16,6 +16,8 @@ import 'vendor/angular-other/angular-strap';
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
// @ts-ignore
|
||||||
|
import ttiPolyfill from 'tti-polyfill';
|
||||||
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
|
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AppEvents, setMarkdownOptions, setLocale } from '@grafana/data';
|
import { AppEvents, setMarkdownOptions, setLocale } from '@grafana/data';
|
||||||
@@ -34,6 +36,10 @@ _.move = (array: [], fromIndex: number, toIndex: number) => {
|
|||||||
import { coreModule, angularModules } from 'app/core/core_module';
|
import { coreModule, angularModules } from 'app/core/core_module';
|
||||||
import { registerAngularDirectives } from 'app/core/core';
|
import { registerAngularDirectives } from 'app/core/core';
|
||||||
import { setupAngularRoutes } from 'app/routes/routes';
|
import { setupAngularRoutes } from 'app/routes/routes';
|
||||||
|
import { setEchoSrv, registerEchoBackend } from '@grafana/runtime';
|
||||||
|
import { Echo } from './core/services/echo/Echo';
|
||||||
|
import { reportPerformance } from './core/services/echo/EchoSrv';
|
||||||
|
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
|
||||||
|
|
||||||
import 'app/routes/GrafanaCtrl';
|
import 'app/routes/GrafanaCtrl';
|
||||||
import 'app/features/all';
|
import 'app/features/all';
|
||||||
@@ -163,6 +169,26 @@ export class GrafanaApp {
|
|||||||
importPluginModule(modulePath);
|
importPluginModule(modulePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initEchoSrv() {
|
||||||
|
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
||||||
|
|
||||||
|
ttiPolyfill.getFirstConsistentlyInteractive().then((tti: any) => {
|
||||||
|
// Collecting paint metrics first
|
||||||
|
const paintMetrics = performance.getEntriesByType('paint');
|
||||||
|
|
||||||
|
for (const metric of paintMetrics) {
|
||||||
|
reportPerformance(metric.name, Math.round(metric.startTime + metric.duration));
|
||||||
|
}
|
||||||
|
reportPerformance('tti', tti);
|
||||||
|
});
|
||||||
|
|
||||||
|
registerEchoBackend(new PerformanceBackend({}));
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
reportPerformance('dcl', Math.round(performance.now()));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new GrafanaApp();
|
export default new GrafanaApp();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
orgCount: 2,
|
orgCount: 2,
|
||||||
orgRole: '',
|
orgRole: '',
|
||||||
orgId: 1,
|
orgId: 1,
|
||||||
|
login: 'hello',
|
||||||
orgName: 'Grafana',
|
orgName: 'Grafana',
|
||||||
timezone: 'UTC',
|
timezone: 'UTC',
|
||||||
helpFlags1: 1,
|
helpFlags1: 1,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class User {
|
|||||||
orgRole: any;
|
orgRole: any;
|
||||||
orgId: number;
|
orgId: number;
|
||||||
orgName: string;
|
orgName: string;
|
||||||
|
login: string;
|
||||||
orgCount: number;
|
orgCount: number;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
helpFlags1: number;
|
helpFlags1: number;
|
||||||
|
|||||||
89
public/app/core/services/echo/Echo.ts
Normal file
89
public/app/core/services/echo/Echo.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { EchoBackend, EchoMeta, EchoEvent, EchoSrv } from '@grafana/runtime';
|
||||||
|
import { contextSrv } from '../context_srv';
|
||||||
|
|
||||||
|
interface EchoConfig {
|
||||||
|
// How often should metrics be reported
|
||||||
|
flushInterval: number;
|
||||||
|
// Enables debug mode
|
||||||
|
debug: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echo is a service for collecting events from Grafana client-app
|
||||||
|
* It collects events, distributes them across registered backend and flushes once per configured interval
|
||||||
|
* It's up to the registered backend to decide what to do with a given type of metric
|
||||||
|
*/
|
||||||
|
export class Echo implements EchoSrv {
|
||||||
|
private config: EchoConfig = {
|
||||||
|
flushInterval: 10000, // By default Echo flushes every 10s
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private backends: EchoBackend[] = [];
|
||||||
|
// meta data added to every event collected
|
||||||
|
|
||||||
|
constructor(config?: Partial<EchoConfig>) {
|
||||||
|
this.config = {
|
||||||
|
...this.config,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
setInterval(this.flush, this.config.flushInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug = (...msg: any) => {
|
||||||
|
if (this.config.debug) {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
// console.debug('ECHO:', ...msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
flush = () => {
|
||||||
|
for (const backend of this.backends) {
|
||||||
|
backend.flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addBackend = (backend: EchoBackend) => {
|
||||||
|
this.logDebug('Adding backend', backend);
|
||||||
|
this.backends.push(backend);
|
||||||
|
};
|
||||||
|
|
||||||
|
addEvent = <T extends EchoEvent>(event: Omit<T, 'meta'>, _meta?: {}) => {
|
||||||
|
const meta = this.getMeta();
|
||||||
|
const _event = {
|
||||||
|
...event,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
..._meta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const backend of this.backends) {
|
||||||
|
if (backend.supportedEvents.length === 0 || backend.supportedEvents.indexOf(_event.type) > -1) {
|
||||||
|
backend.addEvent(_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logDebug('Adding event', _event);
|
||||||
|
};
|
||||||
|
|
||||||
|
getMeta = (): EchoMeta => {
|
||||||
|
return {
|
||||||
|
sessionId: '',
|
||||||
|
userId: contextSrv.user.id,
|
||||||
|
userLogin: contextSrv.user.login,
|
||||||
|
userSignedIn: contextSrv.user.isSignedIn,
|
||||||
|
screenSize: {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
},
|
||||||
|
windowSize: {
|
||||||
|
width: window.screen.width,
|
||||||
|
height: window.screen.height,
|
||||||
|
},
|
||||||
|
userAgent: window.navigator.userAgent,
|
||||||
|
ts: performance.now(),
|
||||||
|
url: window.location.href,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
12
public/app/core/services/echo/EchoSrv.ts
Normal file
12
public/app/core/services/echo/EchoSrv.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { getEchoSrv, EchoEventType } from '@grafana/runtime';
|
||||||
|
import { PerformanceEvent } from './backends/PerformanceBackend';
|
||||||
|
|
||||||
|
export const reportPerformance = (metric: string, value: number) => {
|
||||||
|
getEchoSrv().addEvent<PerformanceEvent>({
|
||||||
|
type: EchoEventType.Performance,
|
||||||
|
payload: {
|
||||||
|
metricName: metric,
|
||||||
|
duration: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
49
public/app/core/services/echo/backends/PerformanceBackend.ts
Normal file
49
public/app/core/services/echo/backends/PerformanceBackend.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export interface PerformanceEventPayload {
|
||||||
|
metricName: string;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceEvent extends EchoEvent<EchoEventType.Performance, PerformanceEventPayload> {}
|
||||||
|
|
||||||
|
export interface PerformanceBackendOptions {
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echo's performance metrics consumer
|
||||||
|
* Reports performance metrics to given url (TODO)
|
||||||
|
*/
|
||||||
|
export class PerformanceBackend implements EchoBackend<PerformanceEvent, PerformanceBackendOptions> {
|
||||||
|
private buffer: PerformanceEvent[] = [];
|
||||||
|
supportedEvents = [EchoEventType.Performance];
|
||||||
|
|
||||||
|
constructor(public options: PerformanceBackendOptions) {}
|
||||||
|
|
||||||
|
addEvent = (e: EchoEvent) => {
|
||||||
|
this.buffer.push(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
flush = () => {
|
||||||
|
if (this.buffer.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
metrics: this.buffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Currently we don have API for sending the metrics hence loging to console in dev environment
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('PerformanceBackend flushing:', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffer = [];
|
||||||
|
|
||||||
|
// TODO: Enable backend request when we have metrics API
|
||||||
|
// if (this.options.url) {
|
||||||
|
// getBackendSrv().post(this.options.url, result);
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||||
import { PanelData, DataQueryRequest, dateTime, ScopedVars } from '@grafana/data';
|
import { PanelData, DataQueryRequest, dateTime, ScopedVars } from '@grafana/data';
|
||||||
import { PanelModel } from './PanelModel';
|
import { DashboardModel } from './index';
|
||||||
|
import { setEchoSrv } from '@grafana/runtime';
|
||||||
|
import { Echo } from '../../../core/services/echo/Echo';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
|
|
||||||
// Defined within setup functions
|
const dashboardModel = new DashboardModel({
|
||||||
const panelsForCurrentDashboardMock: { [key: number]: PanelModel } = {};
|
panels: [{ id: 1, type: 'graph' }],
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
|
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
|
||||||
getDashboardSrv: () => {
|
getDashboardSrv: () => {
|
||||||
return {
|
return {
|
||||||
getCurrent: () => {
|
getCurrent: () => dashboardModel,
|
||||||
return {
|
|
||||||
getPanelById: (id: number) => {
|
|
||||||
return panelsForCurrentDashboardMock[id];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -68,6 +65,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
setEchoSrv(new Echo());
|
||||||
setupFn();
|
setupFn();
|
||||||
|
|
||||||
const datasource: any = {
|
const datasource: any = {
|
||||||
@@ -103,13 +101,6 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
panelsForCurrentDashboardMock[1] = {
|
|
||||||
id: 1,
|
|
||||||
getQueryRunner: () => {
|
|
||||||
return ctx.runner;
|
|
||||||
},
|
|
||||||
} as PanelModel;
|
|
||||||
|
|
||||||
ctx.events = [];
|
ctx.events = [];
|
||||||
ctx.runner.run(args);
|
ctx.runner.run(args);
|
||||||
});
|
});
|
||||||
|
|||||||
56
public/app/features/dashboard/state/analyticsProcessor.ts
Normal file
56
public/app/features/dashboard/state/analyticsProcessor.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { getDashboardSrv } from '../services/DashboardSrv';
|
||||||
|
|
||||||
|
import { PanelData, LoadingState, DataSourceApi } from '@grafana/data';
|
||||||
|
|
||||||
|
import { reportMetaAnalytics, MetaAnalyticsEventPayload } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export function getAnalyticsProcessor(datasource: DataSourceApi) {
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
return (data: PanelData) => {
|
||||||
|
if (!data.request || done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.state !== LoadingState.Done && data.state !== LoadingState.Error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData: MetaAnalyticsEventPayload = {
|
||||||
|
datasourceName: datasource.name,
|
||||||
|
datasourceId: datasource.id,
|
||||||
|
panelId: data.request.panelId,
|
||||||
|
dashboardId: data.request.dashboardId,
|
||||||
|
// app: 'dashboard',
|
||||||
|
// count: 1,
|
||||||
|
dataSize: 0,
|
||||||
|
duration: data.request.endTime - data.request.startTime,
|
||||||
|
eventName: 'data-request',
|
||||||
|
// sessionId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// enrich with dashboard info
|
||||||
|
const dashboard = getDashboardSrv().getCurrent();
|
||||||
|
if (dashboard) {
|
||||||
|
eventData.dashboardId = dashboard.id;
|
||||||
|
eventData.dashboardName = dashboard.title;
|
||||||
|
eventData.dashboardUid = dashboard.uid;
|
||||||
|
eventData.folderName = dashboard.meta.folderTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.series.length > 0) {
|
||||||
|
// estimate size
|
||||||
|
eventData.dataSize = data.series.length * data.series[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
eventData.error = data.error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
reportMetaAnalytics(eventData);
|
||||||
|
|
||||||
|
// this done check is to make sure we do not double emit events in case
|
||||||
|
// there are multiple responses with done state
|
||||||
|
done = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,9 +10,24 @@ import {
|
|||||||
import { Subscriber, Observable, Subscription } from 'rxjs';
|
import { Subscriber, Observable, Subscription } from 'rxjs';
|
||||||
import { runRequest } from './runRequest';
|
import { runRequest } from './runRequest';
|
||||||
import { deepFreeze } from '../../../../test/core/redux/reducerTester';
|
import { deepFreeze } from '../../../../test/core/redux/reducerTester';
|
||||||
|
import { DashboardModel } from './DashboardModel';
|
||||||
|
import { setEchoSrv } from '@grafana/runtime';
|
||||||
|
import { Echo } from '../../../core/services/echo/Echo';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
|
|
||||||
|
const dashboardModel = new DashboardModel({
|
||||||
|
panels: [{ id: 1, type: 'graph' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
|
||||||
|
getDashboardSrv: () => {
|
||||||
|
return {
|
||||||
|
getCurrent: () => dashboardModel,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
class ScenarioCtx {
|
class ScenarioCtx {
|
||||||
ds: DataSourceApi;
|
ds: DataSourceApi;
|
||||||
request: DataQueryRequest;
|
request: DataQueryRequest;
|
||||||
@@ -84,6 +99,7 @@ function runRequestScenario(desc: string, fn: (ctx: ScenarioCtx) => void) {
|
|||||||
const ctx = new ScenarioCtx();
|
const ctx = new ScenarioCtx();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
setEchoSrv(new Echo());
|
||||||
ctx.reset();
|
ctx.reset();
|
||||||
return ctx.setupFn();
|
return ctx.setupFn();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import { Observable, of, timer, merge, from } from 'rxjs';
|
import { Observable, of, timer, merge, from } from 'rxjs';
|
||||||
import { flatten, map as lodashMap, isArray, isString } from 'lodash';
|
import { flatten, map as lodashMap, isArray, isString } from 'lodash';
|
||||||
import { map, catchError, takeUntil, mapTo, share, finalize } from 'rxjs/operators';
|
import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators';
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
// Types
|
// Types
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
DataFrame,
|
DataFrame,
|
||||||
guessFieldTypes,
|
guessFieldTypes,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { getAnalyticsProcessor } from './analyticsProcessor';
|
||||||
import { ExpressionDatasourceID, expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
import { ExpressionDatasourceID, expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||||
|
|
||||||
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
||||||
@@ -119,6 +120,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
|
|||||||
error: processQueryError(err),
|
error: processQueryError(err),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
tap(getAnalyticsProcessor(datasource)),
|
||||||
// finalize is triggered when subscriber unsubscribes
|
// finalize is triggered when subscriber unsubscribes
|
||||||
// This makes sure any still running network requests are cancelled
|
// This makes sure any still running network requests are cancelled
|
||||||
finalize(cancelNetworkRequestsOnUnsubscribe(request)),
|
finalize(cancelNetworkRequestsOnUnsubscribe(request)),
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
import app from './app';
|
import app from './app';
|
||||||
|
|
||||||
|
app.initEchoSrv();
|
||||||
app.init();
|
app.init();
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ export function grafanaAppDirective(
|
|||||||
controller: GrafanaCtrl,
|
controller: GrafanaCtrl,
|
||||||
link: (scope: IRootScopeService & AppEventEmitter, elem: JQuery) => {
|
link: (scope: IRootScopeService & AppEventEmitter, elem: JQuery) => {
|
||||||
const body = $('body');
|
const body = $('body');
|
||||||
|
|
||||||
// see https://github.com/zenorocha/clipboard.js/issues/155
|
// see https://github.com/zenorocha/clipboard.js/issues/155
|
||||||
$.fn.modal.Constructor.prototype.enforceFocus = () => {};
|
$.fn.modal.Constructor.prototype.enforceFocus = () => {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,50 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<script>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
// https://github.com/GoogleChromeLabs/tti-polyfill
|
||||||
<meta name="viewport" content="width=device-width">
|
!(function() {
|
||||||
<meta name="theme-color" content="#000">
|
if ('PerformanceLongTaskTiming' in window) {
|
||||||
|
var g = (window.__tti = { e: [] });
|
||||||
|
g.o = new PerformanceObserver(function(l) {
|
||||||
|
g.e = g.e.concat(l.getEntries());
|
||||||
|
});
|
||||||
|
g.o.observe({ entryTypes: ['longtask'] });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="theme-color" content="#000" />
|
||||||
|
|
||||||
<title>Grafana</title>
|
<title>Grafana</title>
|
||||||
|
|
||||||
<base href="[[.AppSubUrl]]/" />
|
<base href="[[.AppSubUrl]]/" />
|
||||||
|
|
||||||
<link rel="preload" href="public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2" as="font" crossorigin />
|
<link
|
||||||
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
rel="preload"
|
||||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
href="public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2"
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png">
|
as="font"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/png" href="public/img/fav32.png" />
|
||||||
|
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css">
|
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
performance.mark('css done blocking');
|
||||||
|
</script>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
<meta name="msapplication-TileColor" content="#2b5797">
|
<meta name="msapplication-TileColor" content="#2b5797" />
|
||||||
<meta name="msapplication-config" content="public/img/browserconfig.xml">
|
<meta name="msapplication-config" content="public/img/browserconfig.xml" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="theme-[[ .Theme ]] [[.AppNameBodyClass]]">
|
<body class="theme-[[ .Theme ]] [[.AppNameBodyClass]]">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.preloader {
|
.preloader {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -39,7 +58,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation-name: preloader-fade-in;
|
animation-name: preloader-fade-in;
|
||||||
animation-iteration-count: 1;
|
animation-iteration-count: 1;
|
||||||
animation-duration: .9s;
|
animation-duration: 0.9s;
|
||||||
animation-delay: 1.35s;
|
animation-delay: 1.35s;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
@@ -47,14 +66,14 @@
|
|||||||
.preloader__bounce {
|
.preloader__bounce {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
animation-name: preloader-bounce;
|
animation-name: preloader-bounce;
|
||||||
animation-duration: .9s;
|
animation-duration: 0.9s;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preloader__logo {
|
.preloader__logo {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
animation-name: preloader-squash;
|
animation-name: preloader-squash;
|
||||||
animation-duration: .9s;
|
animation-duration: 0.9s;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
@@ -70,7 +89,7 @@
|
|||||||
font-family: Sans-serif;
|
font-family: Sans-serif;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation-name: preloader-fade-in;
|
animation-name: preloader-fade-in;
|
||||||
animation-duration: .9s;
|
animation-duration: 0.9s;
|
||||||
animation-delay: 1.8s;
|
animation-delay: 1.8s;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
@@ -87,7 +106,7 @@
|
|||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
/*animation-timing-function: linear;*/
|
/*animation-timing-function: linear;*/
|
||||||
animation-timing-function: cubic-bezier(0, 0, 0.5, 1)
|
animation-timing-function: cubic-bezier(0, 0, 0.5, 1);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -98,37 +117,37 @@
|
|||||||
from,
|
from,
|
||||||
to {
|
to {
|
||||||
transform: translateY(0px);
|
transform: translateY(0px);
|
||||||
animation-timing-function: cubic-bezier(0.3, 0.0, 0.1, 1)
|
animation-timing-function: cubic-bezier(0.3, 0, 0.1, 1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: translateY(-50px);
|
transform: translateY(-50px);
|
||||||
animation-timing-function: cubic-bezier(.9, 0, .7, 1)
|
animation-timing-function: cubic-bezier(0.9, 0, 0.7, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes preloader-squash {
|
@keyframes preloader-squash {
|
||||||
0% {
|
0% {
|
||||||
transform: scaleX(1.3) scaleY(.8);
|
transform: scaleX(1.3) scaleY(0.8);
|
||||||
animation-timing-function: cubic-bezier(.3, 0, .1, 1);
|
animation-timing-function: cubic-bezier(0.3, 0, 0.1, 1);
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
}
|
}
|
||||||
15% {
|
15% {
|
||||||
transform: scaleX(.75) scaleY(1.25);
|
transform: scaleX(0.75) scaleY(1.25);
|
||||||
animation-timing-function: cubic-bezier(0, 0, .7, .75);
|
animation-timing-function: cubic-bezier(0, 0, 0.7, 0.75);
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
}
|
}
|
||||||
55% {
|
55% {
|
||||||
transform: scaleX(1.05) scaleY(.95);
|
transform: scaleX(1.05) scaleY(0.95);
|
||||||
animation-timing-function: cubic-bezier(.9, 0, 1, 1);
|
animation-timing-function: cubic-bezier(0.9, 0, 1, 1);
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
}
|
}
|
||||||
95% {
|
95% {
|
||||||
transform: scaleX(.75) scaleY(1.25);
|
transform: scaleX(0.75) scaleY(1.25);
|
||||||
animation-timing-function: cubic-bezier(0, 0, 0, 1);
|
animation-timing-function: cubic-bezier(0, 0, 0, 1);
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: scaleX(1.3) scaleY(.8);
|
transform: scaleX(1.3) scaleY(0.8);
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
animation-timing-function: cubic-bezier(0, 0, 0.7, 1);
|
animation-timing-function: cubic-bezier(0, 0, 0.7, 1);
|
||||||
}
|
}
|
||||||
@@ -179,8 +198,11 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
1. This could be caused by your reverse proxy settings.<br /><br />
|
1. This could be caused by your reverse proxy settings.<br /><br />
|
||||||
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath<br /> <br />
|
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath<br />
|
||||||
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn build<br /> <br />
|
<br />
|
||||||
|
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn
|
||||||
|
build<br />
|
||||||
|
<br />
|
||||||
4. Sometimes restarting grafana-server can help<br />
|
4. Sometimes restarting grafana-server can help<br />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,25 +275,54 @@
|
|||||||
[[if .GoogleTagManagerId]]
|
[[if .GoogleTagManagerId]]
|
||||||
<!-- Google Tag Manager -->
|
<!-- Google Tag Manager -->
|
||||||
<script>
|
<script>
|
||||||
dataLayer = [{
|
dataLayer = [
|
||||||
'IsSignedIn': '[[.User.IsSignedIn]]',
|
{
|
||||||
'Email': '[[.User.Email]]',
|
IsSignedIn: '[[.User.IsSignedIn]]',
|
||||||
'Name': '[[.User.Name]]',
|
Email: '[[.User.Email]]',
|
||||||
'UserId': '[[.User.Id]]',
|
Name: '[[.User.Name]]',
|
||||||
'OrgId': '[[.User.OrgId]]',
|
UserId: '[[.User.Id]]',
|
||||||
'OrgName': '[[.User.OrgName]]',
|
OrgId: '[[.User.OrgId]]',
|
||||||
}];
|
OrgName: '[[.User.OrgName]]',
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<iframe src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]" height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
<iframe
|
||||||
|
src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]"
|
||||||
|
height="0"
|
||||||
|
width="0"
|
||||||
|
style="display:none;visibility:hidden"
|
||||||
|
></iframe>
|
||||||
</noscript>
|
</noscript>
|
||||||
<script>(function (w, d, s, l, i) {
|
<script>
|
||||||
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0],
|
(function(w, d, s, l, i) {
|
||||||
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
w[l] = w[l] || [];
|
||||||
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');</script>
|
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||||
|
var f = d.getElementsByTagName(s)[0],
|
||||||
|
j = d.createElement(s),
|
||||||
|
dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||||
|
j.async = true;
|
||||||
|
j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
|
||||||
|
f.parentNode.insertBefore(j, f);
|
||||||
|
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');
|
||||||
|
</script>
|
||||||
<!-- End Google Tag Manager -->
|
<!-- End Google Tag Manager -->
|
||||||
[[end]]
|
[[end]]
|
||||||
|
|
||||||
|
<%
|
||||||
|
for (key in htmlWebpackPlugin.files.chunks) { %><%
|
||||||
|
if (htmlWebpackPlugin.files.jsIntegrity) { %>
|
||||||
|
<script
|
||||||
|
src="<%= htmlWebpackPlugin.files.chunks[key].entry %>"
|
||||||
|
type="text/javascript"
|
||||||
|
integrity="<%= htmlWebpackPlugin.files.jsIntegrity[htmlWebpackPlugin.files.js.indexOf(htmlWebpackPlugin.files.chunks[key].entry)] %>"
|
||||||
|
crossorigin="<%= webpackConfig.output.crossOriginLoading %>"></script><%
|
||||||
|
} else { %>
|
||||||
|
<script src="<%= htmlWebpackPlugin.files.chunks[key].entry %>" type="text/javascript"></script><%
|
||||||
|
} %><%
|
||||||
|
} %>
|
||||||
|
<script>
|
||||||
|
performance.mark('js done blocking');
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ module.exports = (env = {}) =>
|
|||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||||
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||||
inject: 'body',
|
inject: false,
|
||||||
chunksSortMode: 'none',
|
chunksSortMode: 'none',
|
||||||
excludeChunks: ['dark', 'light']
|
excludeChunks: ['dark', 'light']
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ module.exports = merge(common, {
|
|||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||||
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||||
inject: 'body',
|
inject: false,
|
||||||
excludeChunks: ['manifest', 'dark', 'light'],
|
excludeChunks: ['manifest', 'dark', 'light'],
|
||||||
chunksSortMode: 'none'
|
chunksSortMode: 'none'
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -20371,6 +20371,11 @@ tsutils@^3.9.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.8.1"
|
tslib "^1.8.1"
|
||||||
|
|
||||||
|
tti-polyfill@0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tti-polyfill/-/tti-polyfill-0.2.2.tgz#f7bbf71b13afa9edf60c8bb0d0c05f134e1513b9"
|
||||||
|
integrity sha512-URIoJxvsHThbQEJij29hIBUDHx9UNoBBCQVjy7L8PnzkqY8N6lsAI6h8JrT1Wt2lA0avus/DkuiJxd9qpfCpqw==
|
||||||
|
|
||||||
tty-browserify@0.0.0:
|
tty-browserify@0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||||
|
|||||||
Reference in New Issue
Block a user