From dfe33a63fbddf046f6c8155aced57bc99d49e6e2 Mon Sep 17 00:00:00 2001 From: Yaelle Chaudy <42030685+yaelleC@users.noreply.github.com> Date: Thu, 11 Aug 2022 09:30:14 +0200 Subject: [PATCH] Datasources: Emit event on dashboard load with queries info (#52052) Co-authored-by: Levente Balogh Co-authored-by: Andres Martinez --- packages/grafana-data/src/events/common.ts | 14 ++ .../dashboard/state/initDashboard.test.ts | 138 ++++++++++++++++++ .../features/dashboard/state/initDashboard.ts | 37 ++++- .../module.test.ts | 45 ++++++ .../module.ts | 24 ++- 5 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/module.test.ts diff --git a/packages/grafana-data/src/events/common.ts b/packages/grafana-data/src/events/common.ts index 4ed44ff945a..e83ce88e0d6 100644 --- a/packages/grafana-data/src/events/common.ts +++ b/packages/grafana-data/src/events/common.ts @@ -40,3 +40,17 @@ export class DataSelectEvent extends BusEventWithPayload { export class AnnotationChangeEvent extends BusEventWithPayload> { static type = 'annotation-event'; } + +// Loaded the first time a dashboard is loaded (not on every render) +export type DashboardLoadedEventPayload = { + dashboardId: string; + orgId?: number; + userId?: number; + grafanaVersion?: string; + queries: Record; +}; + +/** @alpha */ +export class DashboardLoadedEvent extends BusEventWithPayload> { + static type = 'dashboard-loaded'; +} diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts index 219d67bb011..468634c22a2 100644 --- a/public/app/features/dashboard/state/initDashboard.test.ts +++ b/public/app/features/dashboard/state/initDashboard.test.ts @@ -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); }); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 2902bdfbd43..7593c199573 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -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 { 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)); }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.test.ts new file mode 100644 index 00000000000..ec3d0b3bc90 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.test.ts @@ -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 }, + ], + }); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts index 04653d79235..df300cd26b9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts @@ -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) .setConfigEditor(ConfigEditor) .setQueryEditor(AzureMonitorQueryEditor); + +// Track dashboard loads to RudderStack +getAppEvents().subscribe>( + 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, + })), + }); + } + } +);