Ref: datasource picker rudderstack events (#95074)

* ref: ds picker events added

* ref: track opendropdown only on click

* ref: update/update all payload added to the event

* ref: configure more ds button event path to from path

* ref: result count fix, enable/disable track

* ref: debounce search tracks

* ref: track connections_plugin_card_clicked

* ref: call tracking from the child comp with result count

* ref: change event names, add creator_team and schema version
This commit is contained in:
Syerikjan Kh 2024-10-24 12:03:17 -04:00 committed by GitHub
parent 59f5c1edfb
commit 2f965a07ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 191 additions and 30 deletions

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useMemo, useState, FormEvent, MouseEvent } from 'react';
import { GrafanaTheme2, PluginType } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { useStyles2, LoadingPlaceholder, EmptyState } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
@ -65,8 +66,12 @@ export function AddNewConnection() {
if (!canCreateDataSources) {
e.preventDefault();
e.stopPropagation();
openModal(item);
reportInteraction('connections_plugin_card_clicked', {
plugin_id: item.id,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}
};

View File

@ -1,4 +1,5 @@
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { useLocalStorage } from 'react-use';
@ -27,11 +28,23 @@ export interface Props {
export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
const styles = useStyles2(getStyles);
const [searchQuery, setSearchQuery] = useState('');
const trackSearch = useMemo(
() =>
debounce((q, count) => {
if (q) {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.SEARCH,
query: q,
result_count: count,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}
}, 300),
[]
);
const handleSearchChange = (value: string) => {
if (value) {
reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SEARCH, query: value });
}
setSearchQuery(value);
};
@ -54,6 +67,8 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CHANGE_TAB,
tab: VisualizationSelectPaneTab[value],
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
setListMode(value);
};
@ -93,10 +108,21 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
</Field>
<CustomScrollbar>
{listMode === VisualizationSelectPaneTab.Visualizations && (
<VizTypePicker pluginId={panel.state.pluginId} searchQuery={searchQuery} onChange={onChange} />
<VizTypePicker
pluginId={panel.state.pluginId}
searchQuery={searchQuery}
trackSearch={trackSearch}
onChange={onChange}
/>
)}
{listMode === VisualizationSelectPaneTab.Suggestions && (
<VisualizationSuggestions onChange={onChange} searchQuery={searchQuery} panel={panelModel} data={data} />
<VisualizationSuggestions
onChange={onChange}
trackSearch={trackSearch}
searchQuery={searchQuery}
panel={panelModel}
data={data}
/>
)}
</CustomScrollbar>
</div>

View File

@ -1,3 +1,5 @@
import { useCallback } from 'react';
import { config } from '@grafana/runtime';
import { LinkButton } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
@ -5,11 +7,16 @@ import { Trans } from 'app/core/internationalization';
import { ROUTES } from 'app/features/connections/constants';
import { AccessControlAction } from 'app/types';
import { trackAddNewDsClicked } from '../tracking';
export function DataSourceAddButton(): JSX.Element | null {
const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
const handleClick = useCallback(() => {
trackAddNewDsClicked({ path: location.pathname });
}, []);
return canCreateDataSource ? (
<LinkButton icon="plus" href={config.appSubUrl + ROUTES.DataSourcesNew}>
<LinkButton icon="plus" href={config.appSubUrl + ROUTES.DataSourcesNew} onClick={handleClick}>
<Trans i18nKey="data-sources.datasource-add-button.label">Add new data source</Trans>
</LinkButton>
) : null;

View File

@ -1,6 +1,8 @@
import { css } from '@emotion/css';
import { useCallback } from 'react';
import { DataSourcePluginMeta, GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { DataSourcePluginCategory } from 'app/types';
@ -20,6 +22,15 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop
const moreDataSourcesLink = `${ROUTES.AddNewConnection}?cat=data-source`;
const styles = useStyles2(getStyles);
const handleClick = useCallback(() => {
reportInteraction('connections_add_datasource_find_more_ds_plugins_clicked', {
targetPath: moreDataSourcesLink,
path: location.pathname,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}, [moreDataSourcesLink]);
return (
<>
{/* Categories */}
@ -34,7 +45,7 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop
{/* Find more */}
<div className={styles.more}>
<LinkButton variant="secondary" href={moreDataSourcesLink} target="_self" rel="noopener">
<LinkButton variant="secondary" href={moreDataSourcesLink} onClick={handleClick} target="_self" rel="noopener">
Find more data source plugins
</LinkButton>
</div>

View File

@ -1,10 +1,12 @@
import { useCallback } from 'react';
import { debounce } from 'lodash';
import { useCallback, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { StoreState, useSelector, useDispatch } from 'app/types';
import { getDataSourcesSearchQuery, getDataSourcesSort, setDataSourcesSearchQuery, setIsSortAscending } from '../state';
import { trackDsSearched } from '../tracking';
const ascendingSortValue = 'alpha-asc';
const descendingSortValue = 'alpha-desc';
@ -19,7 +21,23 @@ const sortOptions = [
export function DataSourcesListHeader() {
const dispatch = useDispatch();
const setSearchQuery = useCallback((q: string) => dispatch(setDataSourcesSearchQuery(q)), [dispatch]);
const debouncedTrackSearch = useMemo(
() =>
debounce((q) => {
trackDsSearched({ query: q });
}, 300),
[]
);
const setSearchQuery = useCallback(
(q: string) => {
dispatch(setDataSourcesSearchQuery(q));
if (q) {
debouncedTrackSearch(q);
}
},
[dispatch, debouncedTrackSearch]
);
const searchQuery = useSelector(({ dataSources }: StoreState) => getDataSourcesSearchQuery(dataSources));
const setSort = useCallback(

View File

@ -3,7 +3,8 @@ import { autoUpdate, flip, offset, shift, size, useFloating } from '@floating-ui
import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus';
import { useOverlay } from '@react-aria/overlays';
import { useCallback, useEffect, useRef, useState } from 'react';
import { debounce } from 'lodash';
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import * as React from 'react';
import { Observable } from 'rxjs';
@ -26,6 +27,7 @@ import { dataSourceLabel, matchDataSourceWithSearch } from './utils';
const INTERACTION_EVENT_NAME = 'dashboards_dspicker_clicked';
const INTERACTION_ITEM = {
SEARCH: 'search',
OPEN_DROPDOWN: 'open_dspicker',
SELECT_DS: 'select_ds',
ADD_FILE: 'add_file',
@ -78,6 +80,18 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
const [filterTerm, setFilterTerm] = useState<string>('');
const { onKeyDown, keyboardEvents } = useKeyNavigationListener();
const ref = useRef<HTMLDivElement>(null);
const debouncedTrackSearch = useMemo(
() =>
debounce((q) => {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.SEARCH,
query: q,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}, 300),
[]
);
// Used to position the popper correctly and to bring back the focus when navigating from footer to input
const [markerElement, setMarkerElement] = useState<HTMLInputElement | null>();
@ -151,7 +165,6 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
);
function openDropdown() {
reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN });
setOpen(true);
markerElement?.focus();
}
@ -214,7 +227,17 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
<div className={styles.container} data-testid={selectors.components.DataSourcePicker.container}>
{/* This clickable div is just extending the clickable area on the input element to include the prefix and suffix. */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div className={styles.trigger} onClick={openDropdown}>
<div
className={styles.trigger}
onClick={() => {
openDropdown();
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.OPEN_DROPDOWN,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}}
>
<Input
id={inputId || 'data-source-picker'}
className={inputHasFocus ? undefined : styles.input}
@ -235,6 +258,9 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
onChange={(e) => {
openDropdown();
setFilterTerm(e.currentTarget.value);
if (e.currentTarget.value) {
debouncedTrackSearch(e.currentTarget.value);
}
}}
ref={handleReference}
disabled={disabled}

View File

@ -394,7 +394,7 @@ describe('addDataSource', () => {
plugin_version: '1.2.3',
datasource_uid: 'azure23',
grafana_version: '1.0',
path: DATASOURCES_ROUTES.Edit.replace(':uid', 'azure23'),
path: location.pathname,
});
});
});

View File

@ -249,7 +249,7 @@ export function addDataSource(
plugin_id: plugin.id,
datasource_uid: result.datasource.uid,
plugin_version: result.meta?.info?.version,
path: editLink,
path: location.pathname,
});
locationService.push(editLink);

View File

@ -86,3 +86,19 @@ export const trackDsConfigClicked = (item: string) => {
export const trackDsConfigUpdated = (props: { item: string; error?: unknown }) => {
reportInteraction('connections_datasources_ds_configured', props);
};
export const trackDsSearched = (props: { query: string }) => {
reportInteraction('connections_datasource_list_searched', {
...props,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
};
export const trackAddNewDsClicked = (props: { path: string }) => {
reportInteraction('connections_datasource_list_add_datasource_clicked', {
...props,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
};

View File

@ -1,4 +1,5 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { useAsync } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
@ -15,12 +16,19 @@ export interface Props {
onChange: (options: VizTypeChangeDetails) => void;
data?: PanelData;
panel?: PanelModel;
trackSearch?: (q: string, count: number) => void;
}
export function VisualizationSuggestions({ searchQuery, onChange, data, panel }: Props) {
export function VisualizationSuggestions({ searchQuery, onChange, data, panel, trackSearch }: Props) {
const styles = useStyles2(getStyles);
const { value: suggestions } = useAsync(() => getAllSuggestions(data, panel), [data, panel]);
const filteredSuggestions = filterSuggestionsBySearch(searchQuery, suggestions);
const filteredSuggestions = useMemo(() => {
const result = filterSuggestionsBySearch(searchQuery, suggestions);
if (trackSearch) {
trackSearch(searchQuery, result.length);
}
return result;
}, [searchQuery, suggestions, trackSearch]);
return (
// This div is needed in some places to make AutoSizer work

View File

@ -15,9 +15,10 @@ export interface Props {
searchQuery: string;
onChange: (options: VizTypeChangeDetails) => void;
isWidget?: boolean;
trackSearch?: (q: string, count: number) => void;
}
export function VizTypePicker({ pluginId, searchQuery, onChange, isWidget = false }: Props) {
export function VizTypePicker({ pluginId, searchQuery, onChange, isWidget = false, trackSearch }: Props) {
const styles = useStyles2(getStyles);
const pluginsList = useMemo(() => {
if (config.featureToggles.vizAndWidgetSplit) {
@ -26,10 +27,13 @@ export function VizTypePicker({ pluginId, searchQuery, onChange, isWidget = fals
return getAllPanelPluginMeta();
}, [isWidget]);
const filteredPluginTypes = useMemo(
() => filterPluginList(pluginsList, searchQuery, pluginId),
[pluginsList, searchQuery, pluginId]
);
const filteredPluginTypes = useMemo(() => {
const result = filterPluginList(pluginsList, searchQuery, pluginId);
if (trackSearch) {
trackSearch(searchQuery, result.length);
}
return result;
}, [pluginsList, searchQuery, pluginId, trackSearch]);
if (filteredPluginTypes.length === 0) {
return <EmptySearchResult>Could not find anything matching your query</EmptySearchResult>;

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import { PluginMeta } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types';
@ -26,14 +27,27 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null
const { enabled, jsonData } = pluginConfig?.meta;
const enable = () =>
const enable = () => {
reportInteraction('plugins_detail_enable_clicked', {
path: location.pathname,
plugin_id: plugin.id,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
updatePluginSettingsAndReload(plugin.id, {
enabled: true,
pinned: true,
jsonData,
});
};
const disable = () => {
reportInteraction('plugins_detail_disable_clicked', {
path: location.pathname,
plugin_id: plugin.id,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
updatePluginSettingsAndReload(plugin.id, {
enabled: false,
pinned: false,

View File

@ -55,6 +55,8 @@ export function InstallControlsButton({
plugin_id: plugin.id,
plugin_type: plugin.type,
path: location.pathname,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
};
useEffect(() => {
@ -109,7 +111,7 @@ export function InstallControlsButton({
};
const onUpdate = async () => {
reportInteraction(PLUGIN_UPDATE_INTERACTION_EVENT_NAME);
reportInteraction(PLUGIN_UPDATE_INTERACTION_EVENT_NAME, trackingProps);
await install(plugin.id, latestCompatibleVersion?.version, true);
if (!errorInstalling) {

View File

@ -23,7 +23,11 @@ function PluginListItemComponent({ plugin, pathName }: Props) {
const reportUserClickInteraction = () => {
if (locationService.getSearchObject()?.q) {
reportInteraction('plugins_search_user_click', {});
reportInteraction('plugins_search_user_click', {
plugin_id: plugin.id,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}
};
return (

View File

@ -87,7 +87,12 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props)
const onConfirm = async () => {
if (!inProgress) {
reportInteraction(PLUGINS_UPDATE_ALL_INTERACTION_EVENT_NAME);
reportInteraction(PLUGINS_UPDATE_ALL_INTERACTION_EVENT_NAME, {
path: location.pathname,
count: selectedPlugins?.size,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
setInProgress(true);

View File

@ -1,4 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { debounce } from 'lodash';
import { PluginError, PluginType, unEscapeStringFromRegex } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
@ -14,6 +15,14 @@ export const selectItems = createSelector(selectRoot, ({ items }) => items);
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
const debouncedTrackSearch = debounce((count) => {
reportInteraction('plugins_search', {
resultsCount: count,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}, 300);
export type PluginFilters = {
// Searches for a string in certain fields (e.g. "name" or "orgName")
// (Note: this will be an escaped regex string as it comes from `FilterInput`)
@ -35,12 +44,11 @@ export type PluginFilters = {
export const selectPlugins = (filters: PluginFilters) =>
createSelector(selectAll, (plugins) => {
const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';
// Fuzzy search does not consider plugin type filter
const filteredPluginIds = keyword !== '' ? filterByKeyword(plugins, keyword) : null;
if (keyword) {
reportInteraction('plugins_search', { resultsCount: filteredPluginIds?.length });
}
return plugins.filter((plugin) => {
// Filters are applied here
const filteredPlugins = plugins.filter((plugin) => {
if (keyword && filteredPluginIds == null) {
return false;
}
@ -67,6 +75,12 @@ export const selectPlugins = (filters: PluginFilters) =>
return true;
});
if (keyword) {
debouncedTrackSearch(filteredPlugins.length);
}
return filteredPlugins;
});
export const selectPluginErrors = (filterByPluginType?: PluginType) =>

View File

@ -17,6 +17,7 @@ export function useKeyNavigationListener() {
case 'ArrowLeft':
case 'ArrowRight':
case 'Enter':
case 'Escape':
eventsRef.current.next(e);
default:
// ignore