mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Added hook to make it easier to track interactions in plugins (#56126)
* first stab at context away plugin tracking. * adding a plugin context and a hook to get hold of a tracker that always appends the plugin context information. * wip * improved the code a bit. * wip * Fixed type errors. * added datasource_uid to data sources. * fixed error message when trying to use hook outside of context. * small refactoring according to feedback. * using the correct provider for data source context. * check not needed. * enforcing the interaction name to start with grafana_plugin_ * exposing guards for the other context type. * added structure for writing reporter hook tests. * added some more tests. * added tests. * reverted back to inheritance between context types. * adding mock for getDataSourceSrv
This commit is contained in:
@@ -52,6 +52,7 @@
|
||||
"@rollup/plugin-node-resolve": "13.3.0",
|
||||
"@testing-library/dom": "8.13.0",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/angular": "1.8.4",
|
||||
"@types/history": "4.7.11",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { DataSourceInstanceSettings, PluginMeta } from '@grafana/data';
|
||||
|
||||
import { config } from '../../config';
|
||||
|
||||
export type PluginEventProperties = {
|
||||
grafana_version: string;
|
||||
plugin_type: string;
|
||||
plugin_version: string;
|
||||
plugin_id: string;
|
||||
plugin_name: string;
|
||||
};
|
||||
|
||||
export function createPluginEventProperties(meta: PluginMeta): PluginEventProperties {
|
||||
return {
|
||||
grafana_version: config.buildInfo.version,
|
||||
plugin_type: String(meta.type),
|
||||
plugin_version: meta.info.version,
|
||||
plugin_id: meta.id,
|
||||
plugin_name: meta.name,
|
||||
};
|
||||
}
|
||||
|
||||
export type DataSourcePluginEventProperties = PluginEventProperties & {
|
||||
datasource_uid: string;
|
||||
};
|
||||
|
||||
export function createDataSourcePluginEventProperties(
|
||||
instanceSettings: DataSourceInstanceSettings
|
||||
): DataSourcePluginEventProperties {
|
||||
return {
|
||||
...createPluginEventProperties(instanceSettings.meta),
|
||||
datasource_uid: instanceSettings.uid,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
DataSourceInstanceSettings,
|
||||
DataSourcePluginContextProvider,
|
||||
PluginContextProvider,
|
||||
PluginMeta,
|
||||
PluginMetaInfo,
|
||||
PluginSignatureStatus,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { reportInteraction } from '../utils';
|
||||
|
||||
import { usePluginInteractionReporter } from './usePluginInteractionReporter';
|
||||
|
||||
jest.mock('../utils', () => ({ reportInteraction: jest.fn() }));
|
||||
const reportInteractionMock = jest.mocked(reportInteraction);
|
||||
|
||||
describe('usePluginInteractionReporter', () => {
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('within a panel plugin', () => {
|
||||
it('should report interaction with plugin context info for internal panel', () => {
|
||||
const report = renderPluginReporterHook({
|
||||
id: 'gauge',
|
||||
name: 'Gauge',
|
||||
type: PluginType.panel,
|
||||
info: createPluginMetaInfo({
|
||||
version: '',
|
||||
}),
|
||||
});
|
||||
|
||||
report('grafana_plugin_gradient_mode_changed');
|
||||
|
||||
const [args] = reportInteractionMock.mock.calls;
|
||||
const [interactionName, properties] = args;
|
||||
|
||||
expect(reportInteractionMock.mock.calls.length).toBe(1);
|
||||
expect(interactionName).toBe('grafana_plugin_gradient_mode_changed');
|
||||
expect(properties).toEqual({
|
||||
grafana_version: '1.0',
|
||||
plugin_type: 'panel',
|
||||
plugin_version: '',
|
||||
plugin_id: 'gauge',
|
||||
plugin_name: 'Gauge',
|
||||
});
|
||||
});
|
||||
|
||||
it('should report interaction with plugin context info for external panel', () => {
|
||||
const report = renderPluginReporterHook({
|
||||
id: 'grafana-clock-panel',
|
||||
name: 'Clock',
|
||||
type: PluginType.panel,
|
||||
info: createPluginMetaInfo({
|
||||
version: '2.1.0',
|
||||
}),
|
||||
});
|
||||
|
||||
report('grafana_plugin_time_zone_changed');
|
||||
|
||||
const [args] = reportInteractionMock.mock.calls;
|
||||
const [interactionName, properties] = args;
|
||||
|
||||
expect(reportInteractionMock.mock.calls.length).toBe(1);
|
||||
expect(interactionName).toBe('grafana_plugin_time_zone_changed');
|
||||
expect(properties).toEqual({
|
||||
grafana_version: '1.0',
|
||||
plugin_type: 'panel',
|
||||
plugin_version: '2.1.0',
|
||||
plugin_id: 'grafana-clock-panel',
|
||||
plugin_name: 'Clock',
|
||||
});
|
||||
});
|
||||
|
||||
it('should report interaction with plugin context info and extra info provided when reporting', () => {
|
||||
const report = renderPluginReporterHook({
|
||||
id: 'grafana-clock-panel',
|
||||
name: 'Clock',
|
||||
type: PluginType.panel,
|
||||
info: createPluginMetaInfo({
|
||||
version: '2.1.0',
|
||||
}),
|
||||
});
|
||||
|
||||
report('grafana_plugin_time_zone_changed', {
|
||||
time_zone: 'Europe/Stockholm',
|
||||
});
|
||||
|
||||
const [args] = reportInteractionMock.mock.calls;
|
||||
const [interactionName, properties] = args;
|
||||
|
||||
expect(reportInteractionMock.mock.calls.length).toBe(1);
|
||||
expect(interactionName).toBe('grafana_plugin_time_zone_changed');
|
||||
expect(properties).toEqual({
|
||||
grafana_version: '1.0',
|
||||
plugin_type: 'panel',
|
||||
plugin_version: '2.1.0',
|
||||
plugin_id: 'grafana-clock-panel',
|
||||
plugin_name: 'Clock',
|
||||
time_zone: 'Europe/Stockholm',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('within a data source plugin', () => {
|
||||
it('should report interaction with plugin context info for internal data source', () => {
|
||||
const report = renderDataSourcePluginReporterHook({
|
||||
uid: 'qeSI8VV7z',
|
||||
meta: createPluginMeta({
|
||||
id: 'prometheus',
|
||||
name: 'Prometheus',
|
||||
type: PluginType.datasource,
|
||||
info: createPluginMetaInfo({
|
||||
version: '',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
report('grafana_plugin_query_mode_changed');
|
||||
|
||||
const [args] = reportInteractionMock.mock.calls;
|
||||
const [interactionName, properties] = args;
|
||||
|
||||
expect(reportInteractionMock.mock.calls.length).toBe(1);
|
||||
expect(interactionName).toBe('grafana_plugin_query_mode_changed');
|
||||
expect(properties).toEqual({
|
||||
grafana_version: '1.0',
|
||||
plugin_type: 'datasource',
|
||||
plugin_version: '',
|
||||
plugin_id: 'prometheus',
|
||||
plugin_name: 'Prometheus',
|
||||
datasource_uid: 'qeSI8VV7z',
|
||||
});
|
||||
});
|
||||
|
||||
it('should report interaction with plugin context info for external data source', () => {
|
||||
const report = renderDataSourcePluginReporterHook({
|
||||
uid: 'PD8C576611E62080A',
|
||||
meta: createPluginMeta({
|
||||
id: 'grafana-github-datasource',
|
||||
name: 'GitHub',
|
||||
type: PluginType.datasource,
|
||||
info: createPluginMetaInfo({
|
||||
version: '1.2.0',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
report('grafana_plugin_repository_selected');
|
||||
|
||||
const [args] = reportInteractionMock.mock.calls;
|
||||
const [interactionName, properties] = args;
|
||||
|
||||
expect(reportInteractionMock.mock.calls.length).toBe(1);
|
||||
expect(interactionName).toBe('grafana_plugin_repository_selected');
|
||||
expect(properties).toEqual({
|
||||
grafana_version: '1.0',
|
||||
plugin_type: 'datasource',
|
||||
plugin_version: '1.2.0',
|
||||
plugin_id: 'grafana-github-datasource',
|
||||
plugin_name: 'GitHub',
|
||||
datasource_uid: 'PD8C576611E62080A',
|
||||
});
|
||||
});
|
||||
|
||||
it('should report interaction with plugin context info and extra info provided when reporting', () => {
|
||||
const report = renderDataSourcePluginReporterHook({
|
||||
uid: 'PD8C576611E62080A',
|
||||
meta: createPluginMeta({
|
||||
id: 'grafana-github-datasource',
|
||||
name: 'GitHub',
|
||||
type: PluginType.datasource,
|
||||
info: createPluginMetaInfo({
|
||||
version: '1.2.0',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
report('grafana_plugin_repository_selected', {
|
||||
repository: 'grafana/grafana',
|
||||
});
|
||||
|
||||
const [args] = reportInteractionMock.mock.calls;
|
||||
const [interactionName, properties] = args;
|
||||
|
||||
expect(reportInteractionMock.mock.calls.length).toBe(1);
|
||||
expect(interactionName).toBe('grafana_plugin_repository_selected');
|
||||
expect(properties).toEqual({
|
||||
grafana_version: '1.0',
|
||||
plugin_type: 'datasource',
|
||||
plugin_version: '1.2.0',
|
||||
plugin_id: 'grafana-github-datasource',
|
||||
plugin_name: 'GitHub',
|
||||
datasource_uid: 'PD8C576611E62080A',
|
||||
repository: 'grafana/grafana',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensure interaction name follows convention', () => {
|
||||
it('should throw name does not start with "grafana_plugin_"', () => {
|
||||
const report = renderDataSourcePluginReporterHook();
|
||||
expect(() => report('select_query_type')).toThrow();
|
||||
});
|
||||
|
||||
it('should throw if name is exactly "grafana_plugin_"', () => {
|
||||
const report = renderPluginReporterHook();
|
||||
expect(() => report('grafana_plugin_')).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderPluginReporterHook(meta?: Partial<PluginMeta>): typeof reportInteraction {
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<PluginContextProvider meta={createPluginMeta(meta)}>{children}</PluginContextProvider>
|
||||
);
|
||||
const { result } = renderHook(() => usePluginInteractionReporter(), { wrapper });
|
||||
return result.current;
|
||||
}
|
||||
|
||||
function renderDataSourcePluginReporterHook(settings?: Partial<DataSourceInstanceSettings>): typeof reportInteraction {
|
||||
const wrapper = ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<DataSourcePluginContextProvider instanceSettings={createDataSourceInstanceSettings(settings)}>
|
||||
{children}
|
||||
</DataSourcePluginContextProvider>
|
||||
);
|
||||
const { result } = renderHook(() => usePluginInteractionReporter(), { wrapper });
|
||||
return result.current;
|
||||
}
|
||||
|
||||
function createPluginMeta(meta: Partial<PluginMeta> = {}): PluginMeta {
|
||||
return {
|
||||
id: 'gauge',
|
||||
name: 'Gauge',
|
||||
type: PluginType.panel,
|
||||
info: createPluginMetaInfo(),
|
||||
module: 'app/plugins/panel/gauge/module',
|
||||
baseUrl: '',
|
||||
signature: PluginSignatureStatus.internal,
|
||||
...meta,
|
||||
};
|
||||
}
|
||||
|
||||
function createPluginMetaInfo(info: Partial<PluginMetaInfo> = {}): PluginMetaInfo {
|
||||
return {
|
||||
author: { name: 'Grafana Labs' },
|
||||
description: 'Standard gauge visualization',
|
||||
links: [],
|
||||
logos: {
|
||||
large: 'public/app/plugins/panel/gauge/img/icon_gauge.svg',
|
||||
small: 'public/app/plugins/panel/gauge/img/icon_gauge.svg',
|
||||
},
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '',
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
function createDataSourceInstanceSettings(
|
||||
settings: Partial<DataSourceInstanceSettings> = {}
|
||||
): DataSourceInstanceSettings {
|
||||
const { meta, ...rest } = settings;
|
||||
|
||||
return {
|
||||
id: 1,
|
||||
uid: '',
|
||||
name: '',
|
||||
meta: createPluginMeta(meta),
|
||||
type: PluginType.datasource,
|
||||
readOnly: false,
|
||||
jsonData: {},
|
||||
access: 'proxy',
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isDataSourcePluginContext, usePluginContext } from '@grafana/data';
|
||||
|
||||
import { reportInteraction } from '../utils';
|
||||
|
||||
import { createDataSourcePluginEventProperties, createPluginEventProperties } from './eventProperties';
|
||||
|
||||
const namePrefix = 'grafana_plugin_';
|
||||
|
||||
export function usePluginInteractionReporter(): typeof reportInteraction {
|
||||
const context = usePluginContext();
|
||||
|
||||
return useMemo(() => {
|
||||
const info = isDataSourcePluginContext(context)
|
||||
? createDataSourcePluginEventProperties(context.instanceSettings)
|
||||
: createPluginEventProperties(context.meta);
|
||||
|
||||
return (interactionName: string, properties?: Record<string, unknown>) => {
|
||||
if (!validInteractionName(interactionName)) {
|
||||
throw new Error(`Interactions reported in plugins should start with: "${namePrefix}".`);
|
||||
}
|
||||
return reportInteraction(interactionName, { ...properties, ...info });
|
||||
};
|
||||
}, [context]);
|
||||
}
|
||||
|
||||
function validInteractionName(interactionName: string): boolean {
|
||||
return interactionName.startsWith(namePrefix) && interactionName.length > namePrefix.length;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { config } from '../config';
|
||||
import { locationService } from '../services';
|
||||
import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
|
||||
|
||||
import {
|
||||
ExperimentViewEchoEvent,
|
||||
InteractionEchoEvent,
|
||||
MetaAnalyticsEvent,
|
||||
MetaAnalyticsEventPayload,
|
||||
PageviewEchoEvent,
|
||||
} from '../types/analytics';
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Helper function to report meta analytics to the {@link EchoSrv}.
|
||||
@@ -42,7 +43,7 @@ export const reportPageview = () => {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const reportInteraction = (interactionName: string, properties?: Record<string, any>) => {
|
||||
export const reportInteraction = (interactionName: string, properties?: Record<string, unknown>) => {
|
||||
getEchoSrv().addEvent<InteractionEchoEvent>({
|
||||
type: EchoEventType.Interaction,
|
||||
payload: {
|
||||
@@ -5,9 +5,9 @@
|
||||
*/
|
||||
export * from './services';
|
||||
export * from './config';
|
||||
export * from './types';
|
||||
export * from './analytics/types';
|
||||
export { loadPluginCss, SystemJS, type PluginCssOptions } from './utils/plugin';
|
||||
export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './utils/analytics';
|
||||
export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils';
|
||||
export { featureEnabled } from './utils/licensing';
|
||||
export { logInfo, logDebug, logWarning, logError } from './utils/logging';
|
||||
export {
|
||||
@@ -35,3 +35,10 @@ export {
|
||||
type DataSourcePickerProps,
|
||||
type DataSourcePickerState,
|
||||
} from './components/DataSourcePicker';
|
||||
export {
|
||||
type PluginEventProperties,
|
||||
createPluginEventProperties,
|
||||
type DataSourcePluginEventProperties,
|
||||
createDataSourcePluginEventProperties,
|
||||
} from './analytics/plugins/eventProperties';
|
||||
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './analytics';
|
||||
Reference in New Issue
Block a user