1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

feat: command bar track events (#7469)

Start tracking plausible events

1. Log the search keywords that returned 0 results
2. Track all clicks, based on source(search/recents/pages), type etc.
This commit is contained in:
Jaanus Sellin 2024-06-27 12:48:57 +03:00 committed by GitHub
parent 083273b49b
commit 82822a735b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 117 additions and 8 deletions

View File

@ -25,6 +25,7 @@ import { useRoutes } from 'component/layout/MainLayout/NavigationSidebar/useRout
import { useAsyncDebounce } from 'react-table'; import { useAsyncDebounce } from 'react-table';
import useProjects from 'hooks/api/getters/useProjects/useProjects'; import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { CommandFeatures } from './CommandFeatures'; import { CommandFeatures } from './CommandFeatures';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
export const CommandResultsPaper = styled(Paper)(({ theme }) => ({ export const CommandResultsPaper = styled(Paper)(({ theme }) => ({
position: 'absolute', position: 'absolute',
@ -95,6 +96,7 @@ interface IPageRouteInfo {
} }
export const CommandBar = () => { export const CommandBar = () => {
const { trackEvent } = usePlausibleTracker();
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const searchContainerRef = useRef<HTMLInputElement>(null); const searchContainerRef = useRef<HTMLInputElement>(null);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
@ -105,6 +107,8 @@ export const CommandBar = () => {
const [searchedPages, setSearchedPages] = useState< const [searchedPages, setSearchedPages] = useState<
CommandResultGroupItem[] CommandResultGroupItem[]
>([]); >([]);
const [searchedFlagCount, setSearchedFlagCount] = useState(0);
const [value, setValue] = useState<string>('');
const { lastVisited } = useRecentlyVisited(); const { lastVisited } = useRecentlyVisited();
const { routes } = useRoutes(); const { routes } = useRoutes();
const allRoutes: Record<string, IPageRouteInfo> = {}; const allRoutes: Record<string, IPageRouteInfo> = {};
@ -148,10 +152,25 @@ export const CommandBar = () => {
link: page.path, link: page.path,
})); }));
setSearchedPages(mappedPages); setSearchedPages(mappedPages);
const noResultsFound =
query.length !== 0 &&
mappedProjects.length === 0 &&
mappedPages.length === 0 &&
searchedFlagCount === 0;
if (noResultsFound) {
trackEvent('command-bar', {
props: {
eventType: 'no search results found',
query: query,
},
});
}
}, 200); }, 200);
const onSearchChange = (value: string) => { const onSearchChange = (value: string) => {
debouncedSetSearchState(value); debouncedSetSearchState(value);
setValue(value);
}; };
const hotkey = useKeyboardShortcut( const hotkey = useKeyboardShortcut(
@ -194,7 +213,7 @@ export const CommandBar = () => {
'aria-label': placeholder, 'aria-label': placeholder,
'data-testid': SEARCH_INPUT, 'data-testid': SEARCH_INPUT,
}} }}
value={searchString} value={value}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
onFocus={() => { onFocus={() => {
setShowSuggestions(true); setShowSuggestions(true);
@ -203,13 +222,13 @@ export const CommandBar = () => {
<Box sx={{ width: (theme) => theme.spacing(4) }}> <Box sx={{ width: (theme) => theme.spacing(4) }}>
<ConditionallyRender <ConditionallyRender
condition={Boolean(searchString)} condition={Boolean(value)}
show={ show={
<Tooltip title='Clear search query' arrow> <Tooltip title='Clear search query' arrow>
<IconButton <IconButton
size='small' size='small'
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); // prevent outside click from the lazily added element e.stopPropagation();
onSearchChange(''); onSearchChange('');
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}} }}
@ -226,11 +245,14 @@ export const CommandBar = () => {
</StyledSearch> </StyledSearch>
<ConditionallyRender <ConditionallyRender
condition={Boolean(searchString) && showSuggestions} condition={Boolean(value) && showSuggestions}
show={ show={
<CommandResultsPaper> <CommandResultsPaper>
{searchString !== undefined && ( {searchString !== undefined && (
<CommandFeatures searchString={searchString} /> <CommandFeatures
searchString={searchString}
setSearchedFlagCount={setSearchedFlagCount}
/>
)} )}
<CommandResultGroup <CommandResultGroup
groupName={'Projects'} groupName={'Projects'}

View File

@ -3,11 +3,16 @@ import {
type CommandResultGroupItem, type CommandResultGroupItem,
} from './RecentlyVisited/CommandResultGroup'; } from './RecentlyVisited/CommandResultGroup';
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
import { useEffect } from 'react';
interface ICommandBar { interface ICommandBar {
searchString: string; searchString: string;
setSearchedFlagCount: (count: number) => void;
} }
export const CommandFeatures = ({ searchString }: ICommandBar) => { export const CommandFeatures = ({
searchString,
setSearchedFlagCount,
}: ICommandBar) => {
const { features = [] } = useFeatureSearch( const { features = [] } = useFeatureSearch(
{ {
query: searchString, query: searchString,
@ -24,6 +29,10 @@ export const CommandFeatures = ({ searchString }: ICommandBar) => {
description: feature.description, description: feature.description,
})); }));
useEffect(() => {
setSearchedFlagCount(flags.length);
}, [JSON.stringify(flags)]);
return ( return (
<CommandResultGroup groupName={'Flags'} icon={'flag'} items={flags} /> <CommandResultGroup groupName={'Flags'} icon={'flag'} items={flags} />
); );

View File

@ -9,6 +9,8 @@ import {
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { IconRenderer } from 'component/layout/MainLayout/NavigationSidebar/IconRenderer'; import { IconRenderer } from 'component/layout/MainLayout/NavigationSidebar/IconRenderer';
import type { Theme } from '@mui/material/styles/createTheme'; import type { Theme } from '@mui/material/styles/createTheme';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { JSX } from 'react';
const listItemButtonStyle = (theme: Theme) => ({ const listItemButtonStyle = (theme: Theme) => ({
border: `1px solid transparent`, border: `1px solid transparent`,
@ -42,10 +44,16 @@ const StyledListItemText = styled(ListItemText)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
interface IPageSuggestionItem {
icon: JSX.Element;
name: string;
path: string;
}
const toListItemData = ( const toListItemData = (
items: string[], items: string[],
routes: Record<string, { path: string; route: string; title: string }>, routes: Record<string, { path: string; route: string; title: string }>,
) => { ): IPageSuggestionItem[] => {
return items.map((item) => { return items.map((item) => {
return { return {
name: routes[item]?.title ?? item, name: routes[item]?.title ?? item,
@ -71,8 +79,19 @@ export const PageSuggestions = ({
}: { }: {
routes: Record<string, { path: string; route: string; title: string }>; routes: Record<string, { path: string; route: string; title: string }>;
}) => { }) => {
const { trackEvent } = usePlausibleTracker();
const filtered = pages.filter((page) => routes[page]); const filtered = pages.filter((page) => routes[page]);
const pageItems = toListItemData(filtered, routes); const pageItems = toListItemData(filtered, routes);
const onClick = (item: IPageSuggestionItem) => {
trackEvent('command-bar', {
props: {
eventType: `click`,
source: 'suggestions',
eventTarget: 'Pages',
pageType: item.name,
},
});
};
return ( return (
<StyledContainer> <StyledContainer>
<StyledTypography color='textSecondary'>Pages</StyledTypography> <StyledTypography color='textSecondary'>Pages</StyledTypography>
@ -83,6 +102,9 @@ export const PageSuggestions = ({
dense={true} dense={true}
component={Link} component={Link}
to={item.path} to={item.path}
onClick={() => {
onClick(item);
}}
sx={listItemButtonStyle} sx={listItemButtonStyle}
> >
<StyledListItemIcon <StyledListItemIcon

View File

@ -12,6 +12,7 @@ import type { Theme } from '@mui/material/styles/createTheme';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StyledProjectIcon } from 'component/layout/MainLayout/NavigationSidebar/IconRenderer'; import { StyledProjectIcon } from 'component/layout/MainLayout/NavigationSidebar/IconRenderer';
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver'; import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const listItemButtonStyle = (theme: Theme) => ({ const listItemButtonStyle = (theme: Theme) => ({
borderRadius: theme.spacing(0.5), borderRadius: theme.spacing(0.5),
@ -52,11 +53,23 @@ export const CommandResultGroup = ({
groupName, groupName,
items, items,
}: CommandResultGroupProps) => { }: CommandResultGroupProps) => {
const { trackEvent } = usePlausibleTracker();
const slicedItems = items.slice(0, 3); const slicedItems = items.slice(0, 3);
if (items.length === 0) { if (items.length === 0) {
return null; return null;
} }
const onClick = (item: CommandResultGroupItem) => {
trackEvent('command-bar', {
props: {
eventType: `click`,
source: 'search',
eventTarget: groupName,
...(groupName === 'Pages' && { pageType: item.name }),
},
});
};
return ( return (
<> <>
<StyledTypography color='textSecondary'> <StyledTypography color='textSecondary'>
@ -68,6 +81,9 @@ export const CommandResultGroup = ({
key={`command-result-group-${groupName}-${index}`} key={`command-result-group-${groupName}-${index}`}
dense={true} dense={true}
component={Link} component={Link}
onClick={() => {
onClick(item);
}}
to={item.link} to={item.link}
sx={listItemButtonStyle} sx={listItemButtonStyle}
> >

View File

@ -16,6 +16,7 @@ import type { LastViewedPage } from 'hooks/useRecentlyVisited';
import type { Theme } from '@mui/material/styles/createTheme'; import type { Theme } from '@mui/material/styles/createTheme';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const listItemButtonStyle = (theme: Theme) => ({ const listItemButtonStyle = (theme: Theme) => ({
border: `1px solid transparent`, border: `1px solid transparent`,
@ -90,11 +91,23 @@ const RecentlyVisitedFeatureButton = ({
projectId, projectId,
featureId, featureId,
}: { key: string; projectId: string; featureId: string }) => { }: { key: string; projectId: string; featureId: string }) => {
const { trackEvent } = usePlausibleTracker();
const onClick = () => {
trackEvent('command-bar', {
props: {
eventType: `click`,
source: 'recently-visited',
eventTarget: 'Flags',
},
});
};
return ( return (
<ListItemButton <ListItemButton
key={key} key={key}
dense={true} dense={true}
component={Link} component={Link}
onClick={onClick}
to={`/projects/${projectId}/features/${featureId}`} to={`/projects/${projectId}/features/${featureId}`}
sx={listItemButtonStyle} sx={listItemButtonStyle}
> >
@ -115,12 +128,25 @@ const RecentlyVisitedPathButton = ({
key, key,
name, name,
}: { path: string; key: string; name: string }) => { }: { path: string; key: string; name: string }) => {
const { trackEvent } = usePlausibleTracker();
const onClick = () => {
trackEvent('command-bar', {
props: {
eventType: `click`,
source: 'recently-visited',
eventTarget: 'Pages',
pageType: name,
},
});
};
return ( return (
<ListItemButton <ListItemButton
key={key} key={key}
dense={true} dense={true}
component={Link} component={Link}
to={path} to={path}
onClick={onClick}
sx={listItemButtonStyle} sx={listItemButtonStyle}
> >
<StyledListItemIcon <StyledListItemIcon
@ -145,8 +171,20 @@ const RecentlyVisitedProjectButton = ({
projectId, projectId,
key, key,
}: { projectId: string; key: string }) => { }: { projectId: string; key: string }) => {
const { trackEvent } = usePlausibleTracker();
const { project, loading } = useProjectOverview(projectId); const { project, loading } = useProjectOverview(projectId);
const projectDeleted = !project.name && !loading; const projectDeleted = !project.name && !loading;
const onClick = () => {
trackEvent('command-bar', {
props: {
eventType: `click`,
source: 'recently-visited',
eventTarget: 'Projects',
},
});
};
if (projectDeleted) return null; if (projectDeleted) return null;
return ( return (
<ListItemButton <ListItemButton
@ -154,6 +192,7 @@ const RecentlyVisitedProjectButton = ({
dense={true} dense={true}
component={Link} component={Link}
to={`/projects/${projectId}`} to={`/projects/${projectId}`}
onClick={onClick}
sx={listItemButtonStyle} sx={listItemButtonStyle}
> >
<StyledListItemIcon> <StyledListItemIcon>

View File

@ -61,7 +61,8 @@ export type CustomEvents =
| 'insights-share' | 'insights-share'
| 'many-strategies' | 'many-strategies'
| 'sdk-banner' | 'sdk-banner'
| 'feature-lifecycle'; | 'feature-lifecycle'
| 'command-bar';
export const usePlausibleTracker = () => { export const usePlausibleTracker = () => {
const plausible = useContext(PlausibleContext); const plausible = useContext(PlausibleContext);