Просмотр исходного кода

feat(analytics): update search query analytics (#72138)

Adds an `isMultiProject` field to `search.searched` analytics and a new
Amplitude event to track individual key/value filters.
Kevin Liu 9 месяцев назад
Родитель
Сommit
61cee6e385

+ 1 - 0
static/app/components/events/searchBar.tsx

@@ -310,6 +310,7 @@ function SearchBar(props: SearchBarProps) {
         <SmartSearchBar
           hasRecentSearches
           savedSearchType={SavedSearchType.EVENT}
+          projectIds={projectIds}
           onGetTagValues={getEventFieldValues}
           prepareQuery={query => {
             // Prepare query string (e.g. strip special characters like negation operator)

+ 4 - 3
static/app/components/feedback/feedbackSearch.tsx

@@ -70,7 +70,7 @@ interface Props {
 }
 
 export default function FeedbackSearch({className, style}: Props) {
-  const projectIdStrings = usePageFilters().selection.projects?.map(String);
+  const projectIds = usePageFilters().selection.projects;
   const {pathname, query} = useLocation();
   const organization = useOrganization();
   const tags = useTags();
@@ -89,7 +89,7 @@ export default function FeedbackSearch({className, style}: Props) {
         orgSlug: organization.slug,
         tagKey: tag.key,
         search: searchQuery,
-        projectIds: projectIdStrings,
+        projectIds: projectIds?.map(String),
       }).then(
         tagValues => (tagValues as TagValue[]).map(({value}) => value),
         () => {
@@ -97,13 +97,14 @@ export default function FeedbackSearch({className, style}: Props) {
         }
       );
     },
-    [api, organization.slug, projectIdStrings]
+    [api, organization.slug, projectIds]
   );
 
   return (
     <SearchContainer className={className} style={style}>
       <SmartSearchBar
         hasRecentSearches
+        projectIds={projectIds}
         placeholder={t('Search Feedback')}
         organization={organization}
         onGetTagValues={getTagValues}

+ 1 - 1
static/app/components/metrics/metricSearchBar.tsx

@@ -16,7 +16,7 @@ import usePageFilters from 'sentry/utils/usePageFilters';
 import {ensureQuotedTextFilters} from 'sentry/views/metrics/utils';
 import {useSelectedProjects} from 'sentry/views/metrics/utils/useSelectedProjects';
 
-interface MetricSearchBarProps extends Partial<SmartSearchBarProps> {
+interface MetricSearchBarProps extends Partial<Omit<SmartSearchBarProps, 'projectIds'>> {
   onChange: (value: string) => void;
   blockedTags?: string[];
   disabled?: boolean;

+ 64 - 11
static/app/components/smartSearchBar/index.tsx

@@ -46,6 +46,7 @@ import {defined} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import type {FieldDefinition} from 'sentry/utils/fields';
 import {FieldKind, FieldValueType, getFieldDefinition} from 'sentry/utils/fields';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
 // eslint-disable-next-line no-restricted-imports
@@ -100,6 +101,21 @@ const generateOpAutocompleteGroup = (
   };
 };
 
+function isMultiProject(projectIds: number[] | Readonly<number[] | undefined>) {
+  /**
+   * Returns true if projectIds is:
+   * - [] (My Projects)
+   * - [-1] (All Projects)
+   * - [a, b, ...] (two or more projects)
+   */
+  if (projectIds === undefined) return false;
+  return (
+    projectIds.length === 0 ||
+    (projectIds.length === 1 && projectIds[0] === -1) ||
+    projectIds.length >= 2
+  );
+}
+
 function maybeFocusInput(input: HTMLTextAreaElement | null) {
   // Cannot focus if there is no input or if the input is already focused
   if (!input || document.activeElement === input) {
@@ -350,6 +366,10 @@ type Props = WithRouterProps &
      * Prepare query value before filtering dropdown items
      */
     prepareQuery?: (query: string) => string;
+    /**
+     * Projects that the search bar queries over
+     */
+    projectIds?: number[] | Readonly<number[]>;
     /**
      * Indicates the usage of the search bar for analytics
      */
@@ -555,13 +575,14 @@ class SmartSearchBar extends Component<DefaultProps & Props, State> {
     this.blur();
 
     const query = removeSpace(this.state.query);
-    const {organization, savedSearchType, searchSource} = this.props;
+    const {organization, savedSearchType, searchSource, projectIds} = this.props;
+    const searchType = savedSearchType === 0 ? 'issues' : 'events';
 
     if (!this.hasValidSearch) {
       trackAnalytics('search.search_with_invalid', {
         organization,
         query,
-        search_type: savedSearchType === 0 ? 'issues' : 'events',
+        search_type: searchType,
         search_source: searchSource,
       });
       return;
@@ -571,10 +592,24 @@ class SmartSearchBar extends Component<DefaultProps & Props, State> {
     trackAnalytics('search.searched', {
       organization,
       query,
-      search_type: savedSearchType === 0 ? 'issues' : 'events',
+      is_multi_project: isMultiProject(projectIds),
+      search_type: searchType,
       search_source: searchSource,
     });
 
+    // track the individual key-values filters in the search query
+    Object.entries(new MutableSearch(query).filters).forEach(([key, values]) => {
+      trackAnalytics('search.searched_filter', {
+        organization,
+        query,
+        key,
+        values,
+        is_multi_project: isMultiProject(projectIds),
+        search_type: searchType,
+        search_source: searchSource,
+      });
+    });
+
     onSearch?.(query);
 
     // Only save recent search query if we have a savedSearchType (also 0 is a valid value)
@@ -1858,15 +1893,33 @@ class SmartSearchBar extends Component<DefaultProps & Props, State> {
   };
 
   onAutoComplete = (replaceText: string, item: SearchItem) => {
+    const {organization, savedSearchType, searchSource, projectIds} = this.props;
+    const searchType = savedSearchType === 0 ? 'issues' : 'events';
+    const query = replaceText;
+
     if (item.type === ItemType.RECENT_SEARCH) {
       trackAnalytics('search.searched', {
-        organization: this.props.organization,
-        query: replaceText,
-        search_type: this.props.savedSearchType === 0 ? 'issues' : 'events',
+        organization,
+        query,
+        is_multi_project: isMultiProject(projectIds),
+        search_type: searchType,
         search_source: 'recent_search',
       });
 
-      this.setState(this.makeQueryState(replaceText), () => {
+      // track the individual key-values filters in the search query
+      Object.entries(new MutableSearch(query).filters).forEach(([key, values]) => {
+        trackAnalytics('search.searched_filter', {
+          organization,
+          query,
+          key,
+          values,
+          is_multi_project: isMultiProject(projectIds),
+          search_type: searchType,
+          search_source: 'recent_search',
+        });
+      });
+
+      this.setState(this.makeQueryState(query), () => {
         // Propagate onSearch and save to recent searches
         this.doSearch();
       });
@@ -1880,13 +1933,13 @@ class SmartSearchBar extends Component<DefaultProps & Props, State> {
       item.type === ItemType.RECOMMENDED
     ) {
       trackAnalytics('search.key_autocompleted', {
-        organization: this.props.organization,
-        search_operator: replaceText,
-        search_source: this.props.searchSource,
+        organization,
+        search_operator: query,
+        search_source: searchSource,
         item_name: item.title ?? item.value?.split(':')[0],
         item_kind: item.kind,
         item_type: item.type,
-        search_type: this.props.savedSearchType === 0 ? 'issues' : 'events',
+        search_type: searchType,
       });
     }
 

+ 7 - 2
static/app/utils/analytics/searchAnalyticsEvents.tsx

@@ -3,6 +3,7 @@ import type {ShortcutType} from 'sentry/components/smartSearchBar/types';
 type SearchEventBase = {
   query: string;
   search_type: string;
+  is_multi_project?: boolean;
   search_source?: string;
 };
 
@@ -60,11 +61,14 @@ export type SearchEventParameters = {
   'search.saved_search_open_create_modal': OpenEvent;
   'search.saved_search_sidebar_toggle_clicked': {open: boolean};
   'search.search_with_invalid': SearchEventBase;
-  'search.searched': SearchEventBase & {search_source?: string};
+  'search.searched': SearchEventBase;
+  'search.searched_filter': SearchEventBase & {
+    key: string;
+    values: string[];
+  };
   'search.shortcut_used': SearchEventBase & {
     shortcut_method: 'hotkey' | 'click';
     shortcut_type: ShortcutType;
-    search_source?: string;
   };
   'settings_search.open': OpenEvent;
   'settings_search.query': QueryEvent;
@@ -78,6 +82,7 @@ export type SearchEventKey = keyof SearchEventParameters;
 
 export const searchEventMap: Record<SearchEventKey, string | null> = {
   'search.searched': 'Search: Performed search',
+  'search.searched_filter': 'Search: Performed search filter',
   'search.key_autocompleted': 'Search: Key Autocompleted',
   'search.shortcut_used': 'Search: Shortcut Used',
   'search.docs_opened': 'Search: Docs Opened',

+ 1 - 0
static/app/views/issueList/searchBar.tsx

@@ -166,6 +166,7 @@ function IssueListSearchBar({organization, tags, ...props}: Props) {
   return (
     <SmartSearchBar
       hasRecentSearches
+      projectIds={pageFilters.projects}
       savedSearchType={SavedSearchType.ISSUE}
       onGetTagValues={getTagValues}
       excludedTags={EXCLUDED_TAGS}

+ 1 - 0
static/app/views/performance/transactionSummary/transactionProfiles/index.tsx

@@ -106,6 +106,7 @@ function Profiles(): React.ReactElement {
               </PageFilterBar>
               <SmartSearchBar
                 organization={organization}
+                projectIds={projects.projects.map(p => parseInt(p.id, 10))}
                 hasRecentSearches
                 searchSource="profile_landing"
                 supportedTags={profileFilters}

+ 1 - 0
static/app/views/profiling/content.tsx

@@ -189,6 +189,7 @@ function ProfilingContent({location}: ProfilingContentProps) {
                   <SmartSearchBar
                     organization={organization}
                     hasRecentSearches
+                    projectIds={projects.map(p => parseInt(p.id, 10))}
                     searchSource="profile_landing"
                     supportedTags={profileFilters}
                     query={query}

+ 1 - 0
static/app/views/profiling/profileSummary/index.tsx

@@ -260,6 +260,7 @@ function ProfileFilters(props: ProfileFiltersProps) {
         <SmartSearchBar
           organization={props.organization}
           hasRecentSearches
+          projectIds={props.projectIds}
           searchSource="profile_summary"
           supportedTags={profileFilters}
           query={props.query}

+ 4 - 3
static/app/views/replays/list/replaySearchBar.tsx

@@ -72,7 +72,7 @@ type Props = React.ComponentProps<typeof SmartSearchBar> & {
 function ReplaySearchBar(props: Props) {
   const {organization, pageFilters, placeholder} = props;
   const api = useApi();
-  const projectIdStrings = pageFilters.projects?.map(String);
+  const projectIds = pageFilters.projects;
   const tags = useTags();
   useEffect(() => {
     loadOrganizationTags(api, organization.slug, pageFilters);
@@ -91,7 +91,7 @@ function ReplaySearchBar(props: Props) {
         orgSlug: organization.slug,
         tagKey: tag.key,
         search: searchQuery,
-        projectIds: projectIdStrings,
+        projectIds: projectIds?.map(String),
         includeReplays: true,
       }).then(
         tagValues => (tagValues as TagValue[]).map(({value}) => value),
@@ -100,7 +100,7 @@ function ReplaySearchBar(props: Props) {
         }
       );
     },
-    [api, organization.slug, projectIdStrings]
+    [api, organization.slug, projectIds]
   );
 
   return (
@@ -118,6 +118,7 @@ function ReplaySearchBar(props: Props) {
       savedSearchType={SavedSearchType.REPLAY}
       maxMenuHeight={500}
       hasRecentSearches
+      projectIds={projectIds}
       fieldDefinitionGetter={getReplayFieldDefinition}
       mergeSearchGroupWith={{
         click: {