Datasources: Emit event on dashboard load with queries info (#52052)

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
Co-authored-by: Andres Martinez <andres.martinez@grafana.com>
This commit is contained in:
Yaelle Chaudy 2022-08-11 09:30:14 +02:00 committed by GitHub
parent d7556bd189
commit dfe33a63fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 256 additions and 2 deletions

View File

@ -40,3 +40,17 @@ export class DataSelectEvent extends BusEventWithPayload<DataHoverPayload> {
export class AnnotationChangeEvent extends BusEventWithPayload<Partial<AnnotationEvent>> {
static type = 'annotation-event';
}
// Loaded the first time a dashboard is loaded (not on every render)
export type DashboardLoadedEventPayload<T> = {
dashboardId: string;
orgId?: number;
userId?: number;
grafanaVersion?: string;
queries: Record<string, T[]>;
};
/** @alpha */
export class DashboardLoadedEvent<T> extends BusEventWithPayload<DashboardLoadedEventPayload<T>> {
static type = 'dashboard-loaded';
}

View File

@ -3,6 +3,7 @@ import thunk from 'redux-thunk';
import { Subject } from 'rxjs';
import { FetchError, locationService, setEchoSrv } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { keybindingSrv } from 'app/core/services/keybindingSrv';
import { variableAdapters } from 'app/features/variables/adapters';
@ -42,6 +43,25 @@ jest.mock('app/core/services/context_srv', () => ({
user: { orgId: 1, orgName: 'TestOrg' },
},
}));
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
getDataSourceSrv: jest.fn().mockImplementation(() => ({
...original.getDataSourceSrv(),
getInstanceSettings: jest.fn(),
})),
};
});
jest.mock('@grafana/data', () => {
const original = jest.requireActual('@grafana/data');
return {
...original,
EventBusSrv: jest.fn().mockImplementation(() => ({
publish: jest.fn(),
})),
};
});
variableAdapters.register(createConstantVariableAdapter());
const mockStore = configureMockStore([thunk]);
@ -77,11 +97,73 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
id: 2,
targets: [
{
datasource: {
type: 'grafana-azure-monitor-datasource',
uid: 'DSwithQueriesOnInitDashboard',
name: 'azMonitor',
},
queryType: 'Azure Log Analytics',
refId: 'A',
expr: 'old expr',
},
{
datasource: {
type: 'cloudwatch',
uid: '1234',
name: 'Cloud Watch',
},
refId: 'B',
},
],
},
{
collapsed: true,
gridPos: {
h: 1,
w: 24,
x: 0,
y: 8,
},
id: 22,
panels: [
{
datasource: {
type: 'grafana-redshift-datasource',
uid: 'V6_lLJf7k',
},
gridPos: {
h: 8,
w: 12,
x: 12,
y: 9,
},
id: 8,
targets: [
{
datasource: {
type: 'grafana-redshift-datasource',
uid: 'V6_lLJf7k',
},
rawSQL: '',
refId: 'A',
},
{
datasource: {
type: 'grafana-azure-monitor-datasource',
uid: 'DSwithQueriesOnInitDashboard',
name: 'azMonitor',
},
queryType: 'Azure Monitor',
refId: 'B',
},
],
title: 'Redshift and Azure',
type: 'stat',
},
],
title: 'Collapsed Panel',
type: 'row',
},
],
templating: {
list: [constantBuilder().build()],
@ -241,9 +323,65 @@ describeInitScenario('Initializing existing dashboard', (ctx) => {
ctx.setup(() => {
ctx.storeState.user.orgId = 12;
ctx.storeState.user.user = { id: 34 };
ctx.storeState.explore.left.queries = mockQueries;
});
it('should send dashboard_loaded event', () => {
expect(appEvents.publish).toHaveBeenCalledWith({
payload: {
queries: {
cloudwatch: [
{
datasource: {
name: 'Cloud Watch',
type: 'cloudwatch',
uid: '1234',
},
refId: 'B',
},
],
'grafana-azure-monitor-datasource': [
{
datasource: {
name: 'azMonitor',
type: 'grafana-azure-monitor-datasource',
uid: 'DSwithQueriesOnInitDashboard',
},
expr: 'old expr',
queryType: 'Azure Log Analytics',
refId: 'A',
},
{
datasource: {
name: 'azMonitor',
type: 'grafana-azure-monitor-datasource',
uid: 'DSwithQueriesOnInitDashboard',
},
queryType: 'Azure Monitor',
refId: 'B',
},
],
'grafana-redshift-datasource': [
{
datasource: {
type: 'grafana-redshift-datasource',
uid: 'V6_lLJf7k',
},
rawSQL: '',
refId: 'A',
},
],
},
dashboardId: 'DGmvKKxZz',
orgId: 12,
userId: 34,
grafanaVersion: '1.0',
},
type: 'dashboard-loaded',
});
});
it('Should send action dashboardInitFetching', () => {
expect(ctx.actions[0].type).toBe(dashboardInitFetching.type);
});

View File

@ -1,6 +1,7 @@
import { locationUtil, setWeekStart } from '@grafana/data';
import { DataQuery, locationUtil, setWeekStart, DashboardLoadedEvent } from '@grafana/data';
import { config, isFetchError, locationService } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions';
import appEvents from 'app/core/app_events';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { backendSrv } from 'app/core/services/backend_srv';
import { keybindingSrv } from 'app/core/services/keybindingSrv';
@ -17,6 +18,7 @@ import { initVariablesTransaction } from '../../variables/state/actions';
import { getIfExistsLastKey } from '../../variables/state/selectors';
import { DashboardModel } from './DashboardModel';
import { PanelModel } from './PanelModel';
import { emitDashboardViewEvent } from './analyticsProcessor';
import { dashboardInitCompleted, dashboardInitFailed, dashboardInitFetching, dashboardInitServices } from './reducers';
@ -106,6 +108,28 @@ async function fetchDashboard(
}
}
const getQueriesByDatasource = (
panels: PanelModel[],
queries: { [datasourceId: string]: DataQuery[] } = {}
): { [datasourceId: string]: DataQuery[] } => {
panels.forEach((panel) => {
if (panel.panels) {
getQueriesByDatasource(panel.panels, queries);
} else {
panel.targets.forEach((target) => {
if (target.datasource?.type) {
if (queries[target.datasource.type]) {
queries[target.datasource.type].push(target);
} else {
queries[target.datasource.type] = [target];
}
}
});
}
});
return queries;
};
/**
* This action (or saga) does everything needed to bootstrap a dashboard & dashboard model.
* First it handles the process of fetching the dashboard, correcting the url if required (causing redirects/url updates)
@ -213,6 +237,17 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
setWeekStart(config.bootData.user.weekStart);
}
// Propagate an app-wide event about the dashboard being loaded
appEvents.publish(
new DashboardLoadedEvent({
dashboardId: dashboard.uid,
orgId: storeState.user.orgId,
userId: storeState.user.user?.id,
grafanaVersion: config.buildInfo.version,
queries: getQueriesByDatasource(dashboard.panels),
})
);
// yay we are done
dispatch(dashboardInitCompleted(dashboard));
};

View File

@ -0,0 +1,45 @@
import { DashboardLoadedEvent } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import './module';
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
getAppEvents: () => ({
subscribe: jest.fn((e, handler) => {
// Trigger test event
handler(
new DashboardLoadedEvent({
dashboardId: 'dashboard123',
orgId: 1,
userId: 2,
grafanaVersion: 'v9.0.0',
queries: {
'grafana-azure-monitor-datasource': [
{ queryType: 'Azure Monitor', hide: true },
{ queryType: 'Azure Resource Graph', hide: false },
],
},
})
);
}),
}),
};
});
describe('queriesOnInitDashboard', () => {
it('should report a `grafana_ds_azuremonitor_dashboard_loaded` interaction ', () => {
// subscribeDashboardLoadedEvent();
expect(reportInteraction).toHaveBeenCalledWith('grafana_ds_azuremonitor_dashboard_loaded', {
dashboard_id: 'dashboard123',
grafana_version: 'v9.0.0',
org_id: 1,
user_id: 2,
queries: [
{ query_type: 'Azure Monitor', hidden: true },
{ query_type: 'Azure Resource Graph', hidden: false },
],
});
});
});

View File

@ -1,10 +1,32 @@
import { DataSourcePlugin } from '@grafana/data';
import { DataSourcePlugin, DashboardLoadedEvent } from '@grafana/data';
import { reportInteraction, getAppEvents } from '@grafana/runtime';
import { ConfigEditor } from './components/ConfigEditor';
import AzureMonitorQueryEditor from './components/QueryEditor';
import Datasource from './datasource';
import { id } from './plugin.json';
import { AzureMonitorQuery, AzureDataSourceJsonData } from './types';
export const plugin = new DataSourcePlugin<Datasource, AzureMonitorQuery, AzureDataSourceJsonData>(Datasource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(AzureMonitorQueryEditor);
// Track dashboard loads to RudderStack
getAppEvents().subscribe<DashboardLoadedEvent<AzureMonitorQuery>>(
DashboardLoadedEvent,
({ payload: { dashboardId, orgId, userId, grafanaVersion, queries } }) => {
const azureQueries = queries[id];
if (azureQueries && azureQueries.length > 0) {
reportInteraction('grafana_ds_azuremonitor_dashboard_loaded', {
grafana_version: grafanaVersion,
dashboard_id: dashboardId,
org_id: orgId,
user_id: userId,
queries: azureQueries.map((query: AzureMonitorQuery) => ({
hidden: !!query.hide,
query_type: query.queryType,
})),
});
}
}
);