Explore: Add click tracking to data links (#65221)

* Add click tracking to data links

* Use constants, mock window.open call

* Add comment about possibly missing reporting data

* Remove superfluous if
This commit is contained in:
Kristina 2023-03-30 16:30:20 -05:00 committed by GitHub
parent 940768cf76
commit 55947cee8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 3 deletions

View File

@ -1,7 +1,9 @@
import {
ArrayVector,
CoreApp,
DataFrame,
DataLink,
DataLinkConfigOrigin,
dateTime,
Field,
FieldType,
@ -10,7 +12,7 @@ import {
TimeRange,
toDataFrame,
} from '@grafana/data';
import { setTemplateSrv } from '@grafana/runtime';
import { setTemplateSrv, reportInteraction } from '@grafana/runtime';
import { initTemplateSrv } from '../../../../test/helpers/initTemplateSrv';
import { ContextSrv, setContextSrv } from '../../../core/services/context_srv';
@ -18,6 +20,11 @@ import { setLinkSrv } from '../../panel/panellinks/link_srv';
import { getFieldLinksForExplore, getVariableUsageInfo } from './links';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
}));
describe('explore links utils', () => {
describe('getFieldLinksForExplore', () => {
beforeEach(() => {
@ -28,6 +35,12 @@ describe('explore links utils', () => {
{ type: 'custom', name: 'test', current: { value: 'foo' } },
])
);
jest.spyOn(window, 'open').mockImplementation();
});
afterEach(() => {
jest.resetAllMocks();
});
it('returns correct link model for external link', () => {
@ -44,6 +57,15 @@ describe('explore links utils', () => {
expect(links[0].href).toBe('http://regionalhost');
expect(links[0].title).toBe('external');
expect(links[0].onClick).toBeDefined();
links[0].onClick!({});
expect(reportInteraction).toBeCalledWith('grafana_data_link_clicked', {
app: CoreApp.Explore,
internal: false,
origin: DataLinkConfigOrigin.Datasource,
});
});
it('returns generates title for external link', () => {
@ -106,6 +128,12 @@ describe('explore links utils', () => {
},
},
});
expect(reportInteraction).toBeCalledWith('grafana_data_link_clicked', {
app: CoreApp.Explore,
internal: true,
origin: DataLinkConfigOrigin.Datasource,
});
});
it('returns correct link model for external link when user does not have access to explore', () => {
@ -251,6 +279,7 @@ describe('explore links utils', () => {
const transformationLink: DataLink = {
title: '',
url: '',
origin: DataLinkConfigOrigin.Correlations,
internal: {
query: { query: 'http_requests{app=${application} env=${environment}}' },
datasourceUid: 'uid_1',
@ -281,6 +310,17 @@ describe('explore links utils', () => {
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{app=foo env=dev}"}]}'
)}`
);
if (links[0][0].onClick) {
links[0][0].onClick({});
}
expect(reportInteraction).toBeCalledWith('grafana_data_link_clicked', {
app: CoreApp.Explore,
internal: true,
origin: DataLinkConfigOrigin.Correlations,
});
expect(links[1]).toHaveLength(1);
expect(links[1][0].href).toBe(
`/explore?left=${encodeURIComponent(

View File

@ -12,8 +12,12 @@ import {
SplitOpen,
DataLink,
DisplayValue,
DataLinkConfigOrigin,
CoreApp,
SplitOpenOptions,
} from '@grafana/data';
import { getTemplateSrv, VariableInterpolation } from '@grafana/runtime';
import { getTemplateSrv, reportInteraction, VariableInterpolation } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { getTransformationVars } from 'app/features/correlations/transformations';
@ -42,6 +46,8 @@ export interface ExploreFieldLinkModel extends LinkModel<Field> {
variables?: VariableInterpolation[];
}
const DATA_LINK_USAGE_KEY = 'grafana_data_link_clicked';
/**
* Get links from the field of a dataframe and in addition check if there is associated
* metadata with datasource in which case we will add onClick to open the link in new split window. This assumes
@ -115,6 +121,30 @@ export const getFieldLinksForExplore = (options: {
if (!linkModel.title) {
linkModel.title = getTitleFromHref(linkModel.href);
}
// Take over the onClick to report the click, then either call the original onClick or navigate to the URL
// Note: it is likely that an external link that opens in the same tab will not be reported, as the browser redirect might cancel reporting the interaction
const origOnClick = linkModel.onClick;
linkModel.onClick = (...args) => {
reportInteraction(DATA_LINK_USAGE_KEY, {
origin: link.origin || DataLinkConfigOrigin.Datasource,
app: CoreApp.Explore,
internal: false,
});
if (origOnClick) {
origOnClick?.apply(...args);
} else {
// for external links without an onClick, we want to duplicate default href behavior since onClick stops it
if (linkModel.target === '_blank') {
window.open(linkModel.href);
} else {
window.location.href = linkModel.href;
}
}
};
return linkModel;
} else {
let internalLinkSpecificVars: ScopedVars = {};
@ -147,6 +177,16 @@ export const getFieldLinksForExplore = (options: {
variables = variableData.variables;
}
const splitFnWithTracking = (options?: SplitOpenOptions<DataQuery>) => {
reportInteraction(DATA_LINK_USAGE_KEY, {
origin: link.origin || DataLinkConfigOrigin.Datasource,
app: CoreApp.Explore,
internal: true,
});
splitOpenFn?.(options);
};
if (variableData.allVariablesDefined) {
const internalLink = mapInternalLinkToExplore({
link,
@ -154,7 +194,7 @@ export const getFieldLinksForExplore = (options: {
scopedVars: allVars,
range,
field,
onClickFn: splitOpenFn,
onClickFn: (options) => splitFnWithTracking(options),
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
});
return { ...internalLink, variables: variables };