From 6ac3348021aa103b4d8ceaca36e54ed7641c9635 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Mon, 7 Aug 2023 08:31:13 -0400 Subject: [PATCH] Datasource Plugins: Allow tracking for configuration usage (#72650) Datasource Plugins: Allow tracking for configuration usage --- packages/grafana-data/src/events/common.ts | 8 +- .../datasources/state/actions.test.ts | 41 +++++++- .../app/features/datasources/state/actions.ts | 17 +++- .../components/ConfigEditor.test.tsx | 94 ++++++++++++------- .../cloudwatch/components/ConfigEditor.tsx | 20 ++++ 5 files changed, 139 insertions(+), 41 deletions(-) diff --git a/packages/grafana-data/src/events/common.ts b/packages/grafana-data/src/events/common.ts index 4b9ded63bc6..588b6a6d30d 100644 --- a/packages/grafana-data/src/events/common.ts +++ b/packages/grafana-data/src/events/common.ts @@ -54,7 +54,13 @@ export type DashboardLoadedEventPayload = { export class DashboardLoadedEvent extends BusEventWithPayload> { static type = 'dashboard-loaded'; } - export class DataSourceUpdatedSuccessfully extends BusEventBase { static type = 'datasource-updated-successfully'; } +export class DataSourceTestSucceeded extends BusEventBase { + static type = 'datasource-test-succeeded'; +} + +export class DataSourceTestFailed extends BusEventBase { + static type = 'datasource-test-failed'; +} diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index dc88dc9bee9..e67fc2ca756 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -2,6 +2,7 @@ import { thunkTester } from 'test/core/thunk/thunkTester'; import { AppPluginMeta, DataSourceSettings, PluginMetaInfo, PluginType } from '@grafana/data'; import { FetchError } from '@grafana/runtime'; +import { appEvents } from 'app/core/core'; import { ThunkResult, ThunkDispatch } from 'app/types'; import { getMockDataSource } from '../__mocks__'; @@ -30,7 +31,12 @@ import { jest.mock('../api'); jest.mock('app/core/services/backend_srv'); -jest.mock('app/core/core'); +jest.mock('app/core/core', () => ({ + ...jest.requireActual('app/core/core'), + appEvents: { + publish: jest.fn(), + }, +})); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getDataSourceSrv: jest.fn().mockReturnValue({ reload: jest.fn() }), @@ -331,6 +337,39 @@ describe('testDataSource', () => { const dispatchedActions = await failDataSourceTest(error); expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]); }); + + it('publishes an app event when the test succeeds', async () => { + const dependencies: TestDataSourceDependencies = { + getDatasourceSrv: () => ({ + get: jest.fn().mockReturnValue({ + testDatasource: jest.fn().mockReturnValue({ + status: 'success', + message: '', + }), + type: 'cloudwatch', + uid: 'CW1234', + }), + }), + getBackendSrv: getBackendSrvMock, + }; + await thunkTester({}) + .givenThunk(testDataSource) + .whenThunkIsDispatched('CloudWatch', DATASOURCES_ROUTES.Edit, dependencies); + expect(appEvents.publish).toHaveBeenCalledWith({ type: 'datasource-test-succeeded' }); + }); + + it('publishes an app event when the test fails', async () => { + const error: FetchError = { + config: { + url: '', + }, + data: {}, + statusText: 'Bad Request', + status: 400, + }; + await failDataSourceTest(error); + expect(appEvents.publish).toHaveBeenCalledWith({ type: 'datasource-test-failed' }); + }); }); }); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 2bede5467dd..1e7f9d769ad 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -1,15 +1,22 @@ -import { DataSourcePluginMeta, DataSourceSettings, locationUtil, TestDataSourceResponse } from '@grafana/data'; +import { + DataSourcePluginMeta, + DataSourceSettings, + locationUtil, + TestDataSourceResponse, + DataSourceTestSucceeded, + DataSourceTestFailed, +} from '@grafana/data'; import { config, + DataSourceSrv, DataSourceWithBackend, - getDataSourceSrv, HealthCheckError, HealthCheckResultDetails, isFetchError, locationService, } from '@grafana/runtime'; import { updateNavIndex } from 'app/core/actions'; -import { contextSrv } from 'app/core/core'; +import { appEvents, contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -53,7 +60,7 @@ export interface InitDataSourceSettingDependencies { } export interface TestDataSourceDependencies { - getDatasourceSrv: typeof getDataSourceSrv; + getDatasourceSrv: () => Pick; getBackendSrv: typeof getBackendSrv; } @@ -150,6 +157,7 @@ export const testDataSource = ( success: true, path: editLink, }); + appEvents.publish(new DataSourceTestSucceeded()); } catch (err) { const formattedError = parseHealthCheckError(err); @@ -161,6 +169,7 @@ export const testDataSource = ( success: false, path: editLink, }); + appEvents.publish(new DataSourceTestFailed()); } }); }; diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index 95df690de0c..92354c0460c 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { AwsAuthType } from '@grafana/aws-sdk'; +import { PluginContextProvider, PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data'; import { configureStore } from 'app/store/configureStore'; import { CloudWatchSettings, setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; @@ -25,12 +26,16 @@ jest.mock('./XrayLinkConfig', () => ({ const putMock = jest.fn(); const getMock = jest.fn(); +const mockAppEvents = { + subscribe: () => ({ unsubscribe: jest.fn() }), +}; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ put: putMock, get: getMock, }), + getAppEvents: () => mockAppEvents, })); const props: Props = { @@ -82,11 +87,21 @@ const setup = (optionOverrides?: Partial) => { ...optionOverrides, }, }; + const meta: PluginMeta = { + ...newProps.options, + id: 'cloudwatch', + type: PluginType.datasource, + info: {} as PluginMetaInfo, + module: '', + baseUrl: '', + }; - render( - - - + return render( + + + + + ); }; @@ -177,43 +192,19 @@ describe('Render', () => { it('should load the data source if it was saved before', async () => { const SAVED_VERSION = 2; - const newProps = { - ...props, - options: { - ...props.options, - version: SAVED_VERSION, - }, - }; - - render(); + setup({ version: SAVED_VERSION }); await waitFor(async () => expect(loadDataSourceMock).toHaveBeenCalled()); }); it('should not load the data source if it wasnt saved before', async () => { const SAVED_VERSION = undefined; - const newProps = { - ...props, - options: { - ...props.options, - version: SAVED_VERSION, - }, - }; - - render(); + setup({ version: SAVED_VERSION }); await waitFor(async () => expect(loadDataSourceMock).not.toHaveBeenCalled()); }); it('should show error message if Select log group button is clicked when data source is never saved', async () => { const SAVED_VERSION = undefined; - const newProps = { - ...props, - options: { - ...props.options, - version: SAVED_VERSION, - }, - }; - - render(); + setup({ version: SAVED_VERSION }); await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); await userEvent.click(screen.getByText('Select log groups')); @@ -231,7 +222,20 @@ describe('Render', () => { version: SAVED_VERSION, }, }; - const { rerender } = render(); + const meta: PluginMeta = { + ...newProps.options, + id: 'cloudwatch', + type: PluginType.datasource, + info: {} as PluginMetaInfo, + module: '', + baseUrl: '', + }; + + const { rerender } = render( + + + + ); await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); const rerenderProps = { ...newProps, @@ -243,7 +247,11 @@ describe('Render', () => { }, }, }; - rerender(); + rerender( + + + + ); await waitFor(() => expect(screen.getByText('AWS SDK Default')).toBeInTheDocument()); await userEvent.click(screen.getByText('Select log groups')); await waitFor(() => @@ -264,7 +272,19 @@ describe('Render', () => { version: SAVED_VERSION, }, }; - const { rerender } = render(); + const meta: PluginMeta = { + ...newProps.options, + id: 'cloudwatch', + type: PluginType.datasource, + info: {} as PluginMetaInfo, + module: '', + baseUrl: '', + }; + const { rerender } = render( + + + + ); await waitFor(() => expect(screen.getByText('Select log groups')).toBeInTheDocument()); const rerenderProps = { ...newProps, @@ -273,7 +293,11 @@ describe('Render', () => { version: 1, }, }; - rerender(); + rerender( + + + + ); await userEvent.click(screen.getByText('Select log groups')); await waitFor(() => expect(screen.getByText('Log group name prefix')).toBeInTheDocument()); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index 0263c9117c2..7ef5462197d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -7,7 +7,10 @@ import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOption, updateDatasourcePluginJsonDataOption, + DataSourceTestSucceeded, + DataSourceTestFailed, } from '@grafana/data'; +import { getAppEvents, usePluginInteractionReporter } from '@grafana/runtime'; import { Input, InlineField, FieldProps, SecureSocksProxySettings } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { config } from 'app/core/config'; @@ -37,6 +40,23 @@ export const ConfigEditor = (props: Props) => { invalid: false, }); useEffect(() => setLogGroupFieldState({ invalid: false }), [props.options]); + const report = usePluginInteractionReporter(); + useEffect(() => { + const successSubscription = getAppEvents().subscribe(DataSourceTestSucceeded, () => { + report('grafana_plugin_cloudwatch_save_succeeded', { + auth_type: options.jsonData.authType, + }); + }); + const failSubscription = getAppEvents().subscribe(DataSourceTestFailed, () => { + report('grafana_plugin_cloudwatch_save_failed', { + auth_type: options.jsonData.authType, + }); + }); + return () => { + successSubscription.unsubscribe(); + failSubscription.unsubscribe(); + }; + }, [options.jsonData.authType, report]); return ( <>