PluginExtensions: Reports user interactions with UI extensions (#74355)

* Added tracking information to how UI links are being used by users.

* Fixed nit.
This commit is contained in:
Marcus Andersson 2023-09-12 10:52:16 +02:00 committed by GitHub
parent 1a8a19a9ed
commit 05ce7e5789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 10 deletions

View File

@ -1,10 +1,18 @@
import { PluginExtensionLinkConfig, PluginExtensionTypes } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { createPluginExtensionRegistry } from './createPluginExtensionRegistry';
import { getPluginExtensions } from './getPluginExtensions';
import { isReadOnlyProxy } from './utils';
import { assertPluginExtensionLink } from './validators';
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
};
});
describe('getPluginExtensions()', () => {
const extensionPoint1 = 'grafana/dashboard/panel/menu';
const extensionPoint2 = 'plugins/myorg-basic-app/start';
@ -30,6 +38,7 @@ describe('getPluginExtensions()', () => {
};
global.console.warn = jest.fn();
jest.mocked(reportInteraction).mockReset();
});
test('should return the extensions for the given placement', () => {
@ -43,7 +52,7 @@ describe('getPluginExtensions()', () => {
type: PluginExtensionTypes.link,
title: link1.title,
description: link1.description,
path: link1.path,
path: expect.stringContaining(link1.path!),
})
);
});
@ -60,7 +69,7 @@ describe('getPluginExtensions()', () => {
type: PluginExtensionTypes.link,
title: link1.title,
description: link1.description,
path: link1.path,
path: expect.stringContaining(link1.path!),
})
);
});
@ -89,7 +98,7 @@ describe('getPluginExtensions()', () => {
type: PluginExtensionTypes.link,
title: link1.title,
description: link1.description,
path: link1.path,
path: expect.stringContaining(link1.path!),
})
);
});
@ -129,11 +138,32 @@ describe('getPluginExtensions()', () => {
expect(link2.configure).toHaveBeenCalledTimes(1);
expect(extension.title).toBe('Updated title');
expect(extension.description).toBe('Updated description');
expect(extension.path).toBe(`/a/${pluginId}/updated-path`);
expect(extension.path?.startsWith(`/a/${pluginId}/updated-path`)).toBeTruthy();
expect(extension.icon).toBe('search');
expect(extension.category).toBe('Machine Learning');
});
test('should append link tracking to path when running configure() function', () => {
link2.configure = jest.fn().mockImplementation(() => ({
title: 'Updated title',
description: 'Updated description',
path: `/a/${pluginId}/updated-path`,
icon: 'search',
category: 'Machine Learning',
}));
const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]);
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 });
const [extension] = extensions;
assertPluginExtensionLink(extension);
expect(link2.configure).toHaveBeenCalledTimes(1);
expect(extension.path).toBe(
`/a/${pluginId}/updated-path?uel_pid=grafana-basic-app&uel_epid=plugins%2Fmyorg-basic-app%2Fstart`
);
});
test('should ignore restricted properties passed via the configure() function', () => {
link2.configure = jest.fn().mockImplementation(() => ({
// The following props are not allowed to override
@ -332,7 +362,7 @@ describe('getPluginExtensions()', () => {
}).toThrow();
});
test('should should not make original context read only', () => {
test('should not make original context read only', () => {
const context = {
title: 'New title from the context!',
nested: { title: 'title' },
@ -348,4 +378,35 @@ describe('getPluginExtensions()', () => {
context.array.push('b');
}).not.toThrow();
});
test('should report interaction when onClick is triggered', () => {
const reportInteractionMock = jest.mocked(reportInteraction);
const registry = createPluginExtensionRegistry([
{
pluginId,
extensionConfigs: [
{
...link1,
path: undefined,
onClick: jest.fn(),
},
],
},
]);
const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint1 });
const [extension] = extensions;
assertPluginExtensionLink(extension);
extension.onClick?.();
expect(reportInteractionMock).toBeCalledTimes(1);
expect(reportInteractionMock).toBeCalledWith('ui_extension_link_clicked', {
pluginId: extension.pluginId,
extensionPointId: extensionPoint1,
title: extension.title,
category: extension.category,
});
});
});

View File

@ -1,10 +1,14 @@
import { isString } from 'lodash';
import {
type PluginExtension,
PluginExtensionTypes,
type PluginExtensionLink,
type PluginExtensionLinkConfig,
type PluginExtensionComponent,
urlUtil,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import type { PluginExtensionRegistry } from './types';
import {
@ -60,24 +64,25 @@ export const getPluginExtensions: GetExtensions = ({ context, extensionPointId,
// LINK
if (isPluginExtensionLinkConfig(extensionConfig)) {
// Run the configure() function with the current context, and apply the ovverides
const overrides = getLinkExtensionOverrides(registryItem.pluginId, extensionConfig, frozenContext);
const overrides = getLinkExtensionOverrides(pluginId, extensionConfig, frozenContext);
// configure() returned an `undefined` -> hide the extension
if (extensionConfig.configure && overrides === undefined) {
continue;
}
const path = overrides?.path || extensionConfig.path;
const extension: PluginExtensionLink = {
id: generateExtensionId(registryItem.pluginId, extensionConfig),
id: generateExtensionId(pluginId, extensionConfig),
type: PluginExtensionTypes.link,
pluginId: registryItem.pluginId,
onClick: getLinkExtensionOnClick(extensionConfig, frozenContext),
pluginId: pluginId,
onClick: getLinkExtensionOnClick(pluginId, extensionConfig, frozenContext),
// Configurable properties
icon: overrides?.icon || extensionConfig.icon,
title: overrides?.title || extensionConfig.title,
description: overrides?.description || extensionConfig.description,
path: overrides?.path || extensionConfig.path,
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionConfig) : undefined,
category: overrides?.category || extensionConfig.category,
};
@ -165,6 +170,7 @@ function getLinkExtensionOverrides(pluginId: string, config: PluginExtensionLink
}
function getLinkExtensionOnClick(
pluginId: string,
config: PluginExtensionLinkConfig,
context?: object
): ((event?: React.MouseEvent) => void) | undefined {
@ -176,6 +182,13 @@ function getLinkExtensionOnClick(
return function onClickExtensionLink(event?: React.MouseEvent) {
try {
reportInteraction('ui_extension_link_clicked', {
pluginId: pluginId,
extensionPointId: config.extensionPointId,
title: config.title,
category: config.category,
});
const result = onClick(event, getEventHelpers(context));
if (isPromise(result)) {
@ -192,3 +205,13 @@ function getLinkExtensionOnClick(
}
};
}
function getLinkExtensionPathWithTracking(pluginId: string, path: string, config: PluginExtensionLinkConfig): string {
return urlUtil.appendQueryToUrl(
path,
urlUtil.toUrlParams({
uel_pid: pluginId,
uel_epid: config.extensionPointId,
})
);
}