Browse Source

feat(traces): Add multiple queries for trace search (#69929)

### Summary
This allows multiple span conditions to be used together to target
specific traces containing all (up to 3) of those conditions.



#### Screenshots
![Screenshot 2024-04-29 at 5 21
36 PM](https://github.com/getsentry/sentry/assets/6111995/6a6af25e-fd41-45bd-bf82-310f8f65fbd7)
Kev 10 months ago
parent
commit
001c56e289

+ 40 - 11
static/app/views/performance/traces/content.tsx

@@ -17,13 +17,12 @@ import Panel from 'sentry/components/panels/panel';
 import PanelHeader from 'sentry/components/panels/panelHeader';
 import PanelItem from 'sentry/components/panels/panelItem';
 import PerformanceDuration from 'sentry/components/performanceDuration';
-import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
 import {IconChevron} from 'sentry/icons/iconChevron';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {PageFilters} from 'sentry/types/core';
 import {useApiQuery} from 'sentry/utils/queryClient';
-import {decodeInteger, decodeScalar} from 'sentry/utils/queryString';
+import {decodeInteger, decodeList} from 'sentry/utils/queryString';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
@@ -46,26 +45,52 @@ const DEFAULT_PER_PAGE = 20;
 export function Content() {
   const location = useLocation();
 
-  const query = useMemo(() => {
-    return decodeScalar(location.query.query, '');
+  const queries = useMemo(() => {
+    return decodeList(location.query.query);
   }, [location.query.query]);
 
   const limit = useMemo(() => {
     return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE);
   }, [location.query.perPage]);
 
-  const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
-    (searchQuery: string) => {
+  const handleSearch = useCallback(
+    (searchIndex: number, searchQuery: string) => {
+      const newQueries = [...queries];
+      if (newQueries.length === 0) {
+        // In the odd case someone wants to add search bars before any query has been made, we add both the default one shown and a new one.
+        newQueries[0] = '';
+      }
+      newQueries[searchIndex] = searchQuery;
       browserHistory.push({
         ...location,
         query: {
           ...location.query,
           cursor: undefined,
-          query: searchQuery || undefined,
+          query: typeof searchQuery === 'string' ? newQueries : queries,
         },
       });
     },
-    [location]
+    [location, queries]
+  );
+
+  const handleClearSearch = useCallback(
+    (searchIndex: number) => {
+      const newQueries = [...queries];
+      if (typeof newQueries[searchIndex] !== undefined) {
+        delete newQueries[searchIndex];
+        browserHistory.push({
+          ...location,
+          query: {
+            ...location.query,
+            cursor: undefined,
+            query: newQueries,
+          },
+        });
+        return true;
+      }
+      return false;
+    },
+    [location, queries]
   );
 
   const traces = useTraces<Field>({
@@ -76,7 +101,7 @@ export function Content() {
       ),
     ],
     limit,
-    query,
+    query: queries,
     sort: SORTS,
   });
 
@@ -92,7 +117,11 @@ export function Content() {
         <EnvironmentPageFilter />
         <DatePageFilter />
       </PageFilterBar>
-      <TracesSearchBar query={query} handleSearch={handleSearch} />
+      <TracesSearchBar
+        queries={queries}
+        handleSearch={handleSearch}
+        handleClearSearch={handleClearSearch}
+      />
       <StyledPanel>
         <TracePanelContent>
           <StyledPanelHeader align="right" lightText>
@@ -292,7 +321,7 @@ interface UseTracesOptions<F extends string> {
   datetime?: PageFilters['datetime'];
   enabled?: boolean;
   limit?: number;
-  query?: string;
+  query?: string | string[];
   sort?: string[];
   suggestedQuery?: string;
 }

+ 87 - 10
static/app/views/performance/traces/tracesSearchBar.tsx

@@ -1,22 +1,99 @@
+import styled from '@emotion/styled';
+
+import {Button} from 'sentry/components/button';
 import SearchBar from 'sentry/components/events/searchBar';
-import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
+import {IconAdd, IconClose} from 'sentry/icons';
 import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import useOrganization from 'sentry/utils/useOrganization';
 
 interface TracesSearchBarProps {
-  handleSearch: SmartSearchBarProps['onSearch'];
-  query: string;
+  handleClearSearch: (index: number) => boolean;
+  handleSearch: (index: number, query: string) => void;
+  queries: string[];
 }
 
-export function TracesSearchBar({query, handleSearch}: TracesSearchBarProps) {
+const getSpanName = (index: number) => {
+  const spanNames = [t('Span A'), t('Span B'), t('Span C')];
+  return spanNames[index];
+};
+
+export function TracesSearchBar({
+  queries,
+  handleSearch,
+  handleClearSearch,
+}: TracesSearchBarProps) {
   // TODO: load tags for autocompletion
   const organization = useOrganization();
+  const canAddMoreQueries = queries.length <= 2;
+  const localQueries = queries.length ? queries : [''];
   return (
-    <SearchBar
-      query={query}
-      onSearch={handleSearch}
-      placeholder={t('Filter by tags')}
-      organization={organization}
-    />
+    <TraceSearchBarsContainer>
+      {localQueries.map((query, index) => (
+        <TraceBar key={index}>
+          <SpanLetter>{getSpanName(index)}</SpanLetter>
+          <StyledSearchBar
+            query={query}
+            onSearch={(queryString: string) => handleSearch(index, queryString)}
+            placeholder={t(
+              'Search for traces containing a span matching these attributes'
+            )}
+            organization={organization}
+          />
+          <StyledButton
+            aria-label={t('Remove span')}
+            icon={<IconClose size="sm" />}
+            size="sm"
+            onClick={() => (queries.length === 0 ? false : handleClearSearch(index))}
+          />
+        </TraceBar>
+      ))}
+
+      {canAddMoreQueries ? (
+        <Button
+          aria-label={t('Add query')}
+          icon={<IconAdd size="xs" isCircled />}
+          size="sm"
+          onClick={() => handleSearch(localQueries.length, '')}
+        >
+          {t('Add Span')}
+        </Button>
+      ) : null}
+    </TraceSearchBarsContainer>
   );
 }
+
+const TraceSearchBarsContainer = styled('div')`
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  justify-content: center;
+  gap: ${space(1)};
+`;
+
+const TraceBar = styled('div')`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-start;
+  width: 100%;
+  gap: ${space(1)};
+`;
+
+const SpanLetter = styled('div')`
+  background-color: ${p => p.theme.purple100};
+  border-radius: ${p => p.theme.borderRadius};
+  padding: ${space(1)} ${space(2)};
+
+  color: ${p => p.theme.purple400};
+  white-space: nowrap;
+  font-weight: 800;
+`;
+
+const StyledSearchBar = styled(SearchBar)`
+  width: 100%;
+`;
+
+const StyledButton = styled(Button)`
+  height: 38px;
+`;