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:
parent
083273b49b
commit
82822a735b
@ -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'}
|
||||||
|
@ -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} />
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user