From 2f965a07ab3c5a33efa6b5b0e40cbe719e0971f8 Mon Sep 17 00:00:00 2001 From: Syerikjan Kh Date: Thu, 24 Oct 2024 12:03:17 -0400 Subject: [PATCH] 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 --- .../tabs/ConnectData/ConnectData.tsx | 7 +++- .../panel-edit/PanelVizTypePicker.tsx | 36 ++++++++++++++++--- .../components/DataSourceAddButton.tsx | 9 ++++- .../components/DataSourceCategories.tsx | 13 ++++++- .../components/DataSourcesListHeader.tsx | 22 ++++++++++-- .../components/picker/DataSourcePicker.tsx | 32 +++++++++++++++-- .../datasources/state/actions.test.ts | 2 +- .../app/features/datasources/state/actions.ts | 2 +- public/app/features/datasources/tracking.ts | 16 +++++++++ .../VisualizationSuggestions.tsx | 12 +++++-- .../VizTypePicker/VizTypePicker.tsx | 14 +++++--- .../GetStartedWithApp.tsx | 16 ++++++++- .../InstallControls/InstallControlsButton.tsx | 4 ++- .../admin/components/PluginListItem.tsx | 6 +++- .../admin/components/UpdateAllModal.tsx | 7 +++- .../features/plugins/admin/state/selectors.ts | 22 +++++++++--- .../hooks/useSearchKeyboardSelection.ts | 1 + 17 files changed, 191 insertions(+), 30 deletions(-) diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx index bc21502dfcb..4cd292c298a 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx @@ -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', + }); } }; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx index aee54b2f90e..88c3c1540b0 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx @@ -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) { {listMode === VisualizationSelectPaneTab.Visualizations && ( - + )} {listMode === VisualizationSelectPaneTab.Suggestions && ( - + )} diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx index 3d5d9fead3f..7e216028917 100644 --- a/public/app/features/datasources/components/DataSourceAddButton.tsx +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -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 ? ( - + Add new data source ) : null; diff --git a/public/app/features/datasources/components/DataSourceCategories.tsx b/public/app/features/datasources/components/DataSourceCategories.tsx index 89ef44ff3c7..939e28541db 100644 --- a/public/app/features/datasources/components/DataSourceCategories.tsx +++ b/public/app/features/datasources/components/DataSourceCategories.tsx @@ -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 */}
- + Find more data source plugins
diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx index 413d9284045..cf0a97883d1 100644 --- a/public/app/features/datasources/components/DataSourcesListHeader.tsx +++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx @@ -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( diff --git a/public/app/features/datasources/components/picker/DataSourcePicker.tsx b/public/app/features/datasources/components/picker/DataSourcePicker.tsx index 14dd2676050..332d5ec020a 100644 --- a/public/app/features/datasources/components/picker/DataSourcePicker.tsx +++ b/public/app/features/datasources/components/picker/DataSourcePicker.tsx @@ -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(''); const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); const ref = useRef(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(); @@ -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) {
{/* 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 */} -
+
{ + openDropdown(); + reportInteraction(INTERACTION_EVENT_NAME, { + item: INTERACTION_ITEM.OPEN_DROPDOWN, + creator_team: 'grafana_plugins_catalog', + schema_version: '1.0.0', + }); + }} + > { openDropdown(); setFilterTerm(e.currentTarget.value); + if (e.currentTarget.value) { + debouncedTrackSearch(e.currentTarget.value); + } }} ref={handleReference} disabled={disabled} diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index eb67937e089..3c3f120ce42 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -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, }); }); }); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 24ab46c15b7..f1b2fdd4ffb 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -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); diff --git a/public/app/features/datasources/tracking.ts b/public/app/features/datasources/tracking.ts index c0b39029aa2..b9d0aaa8949 100644 --- a/public/app/features/datasources/tracking.ts +++ b/public/app/features/datasources/tracking.ts @@ -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', + }); +}; diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx index 43ed82cbb30..212e97d24c8 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @@ -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 diff --git a/public/app/features/panel/components/VizTypePicker/VizTypePicker.tsx b/public/app/features/panel/components/VizTypePicker/VizTypePicker.tsx index 1249ec14308..5954872f520 100644 --- a/public/app/features/panel/components/VizTypePicker/VizTypePicker.tsx +++ b/public/app/features/panel/components/VizTypePicker/VizTypePicker.tsx @@ -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 Could not find anything matching your query; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx index 8518161a9a9..bdb80b54d4f 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx @@ -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, diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index fa82e9ce628..1ebcfddec13 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -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) { diff --git a/public/app/features/plugins/admin/components/PluginListItem.tsx b/public/app/features/plugins/admin/components/PluginListItem.tsx index 252ab24be13..d6b53c446b6 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.tsx @@ -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 ( diff --git a/public/app/features/plugins/admin/components/UpdateAllModal.tsx b/public/app/features/plugins/admin/components/UpdateAllModal.tsx index d0c07295f59..5006d8096b8 100644 --- a/public/app/features/plugins/admin/components/UpdateAllModal.tsx +++ b/public/app/features/plugins/admin/components/UpdateAllModal.tsx @@ -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); diff --git a/public/app/features/plugins/admin/state/selectors.ts b/public/app/features/plugins/admin/state/selectors.ts index d0af21fa41c..128823e1c88 100644 --- a/public/app/features/plugins/admin/state/selectors.ts +++ b/public/app/features/plugins/admin/state/selectors.ts @@ -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) => diff --git a/public/app/features/search/hooks/useSearchKeyboardSelection.ts b/public/app/features/search/hooks/useSearchKeyboardSelection.ts index 1eb05bc9145..12c0478eddf 100644 --- a/public/app/features/search/hooks/useSearchKeyboardSelection.ts +++ b/public/app/features/search/hooks/useSearchKeyboardSelection.ts @@ -17,6 +17,7 @@ export function useKeyNavigationListener() { case 'ArrowLeft': case 'ArrowRight': case 'Enter': + case 'Escape': eventsRef.current.next(e); default: // ignore