Skip to content

Commit 001c56e

Browse files
authored
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)
1 parent 8ec16e1 commit 001c56e

File tree

2 files changed

+127
-21
lines changed

2 files changed

+127
-21
lines changed

static/app/views/performance/traces/content.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ import Panel from 'sentry/components/panels/panel';
1717
import PanelHeader from 'sentry/components/panels/panelHeader';
1818
import PanelItem from 'sentry/components/panels/panelItem';
1919
import PerformanceDuration from 'sentry/components/performanceDuration';
20-
import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
2120
import {IconChevron} from 'sentry/icons/iconChevron';
2221
import {t} from 'sentry/locale';
2322
import {space} from 'sentry/styles/space';
2423
import type {PageFilters} from 'sentry/types/core';
2524
import {useApiQuery} from 'sentry/utils/queryClient';
26-
import {decodeInteger, decodeScalar} from 'sentry/utils/queryString';
25+
import {decodeInteger, decodeList} from 'sentry/utils/queryString';
2726
import {useLocation} from 'sentry/utils/useLocation';
2827
import useOrganization from 'sentry/utils/useOrganization';
2928
import usePageFilters from 'sentry/utils/usePageFilters';
@@ -46,26 +45,52 @@ const DEFAULT_PER_PAGE = 20;
4645
export function Content() {
4746
const location = useLocation();
4847

49-
const query = useMemo(() => {
50-
return decodeScalar(location.query.query, '');
48+
const queries = useMemo(() => {
49+
return decodeList(location.query.query);
5150
}, [location.query.query]);
5251

5352
const limit = useMemo(() => {
5453
return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE);
5554
}, [location.query.perPage]);
5655

57-
const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
58-
(searchQuery: string) => {
56+
const handleSearch = useCallback(
57+
(searchIndex: number, searchQuery: string) => {
58+
const newQueries = [...queries];
59+
if (newQueries.length === 0) {
60+
// 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.
61+
newQueries[0] = '';
62+
}
63+
newQueries[searchIndex] = searchQuery;
5964
browserHistory.push({
6065
...location,
6166
query: {
6267
...location.query,
6368
cursor: undefined,
64-
query: searchQuery || undefined,
69+
query: typeof searchQuery === 'string' ? newQueries : queries,
6570
},
6671
});
6772
},
68-
[location]
73+
[location, queries]
74+
);
75+
76+
const handleClearSearch = useCallback(
77+
(searchIndex: number) => {
78+
const newQueries = [...queries];
79+
if (typeof newQueries[searchIndex] !== undefined) {
80+
delete newQueries[searchIndex];
81+
browserHistory.push({
82+
...location,
83+
query: {
84+
...location.query,
85+
cursor: undefined,
86+
query: newQueries,
87+
},
88+
});
89+
return true;
90+
}
91+
return false;
92+
},
93+
[location, queries]
6994
);
7095

7196
const traces = useTraces<Field>({
@@ -76,7 +101,7 @@ export function Content() {
76101
),
77102
],
78103
limit,
79-
query,
104+
query: queries,
80105
sort: SORTS,
81106
});
82107

@@ -92,7 +117,11 @@ export function Content() {
92117
<EnvironmentPageFilter />
93118
<DatePageFilter />
94119
</PageFilterBar>
95-
<TracesSearchBar query={query} handleSearch={handleSearch} />
120+
<TracesSearchBar
121+
queries={queries}
122+
handleSearch={handleSearch}
123+
handleClearSearch={handleClearSearch}
124+
/>
96125
<StyledPanel>
97126
<TracePanelContent>
98127
<StyledPanelHeader align="right" lightText>
@@ -292,7 +321,7 @@ interface UseTracesOptions<F extends string> {
292321
datetime?: PageFilters['datetime'];
293322
enabled?: boolean;
294323
limit?: number;
295-
query?: string;
324+
query?: string | string[];
296325
sort?: string[];
297326
suggestedQuery?: string;
298327
}
Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,99 @@
1+
import styled from '@emotion/styled';
2+
3+
import {Button} from 'sentry/components/button';
14
import SearchBar from 'sentry/components/events/searchBar';
2-
import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
5+
import {IconAdd, IconClose} from 'sentry/icons';
36
import {t} from 'sentry/locale';
7+
import {space} from 'sentry/styles/space';
48
import useOrganization from 'sentry/utils/useOrganization';
59

610
interface TracesSearchBarProps {
7-
handleSearch: SmartSearchBarProps['onSearch'];
8-
query: string;
11+
handleClearSearch: (index: number) => boolean;
12+
handleSearch: (index: number, query: string) => void;
13+
queries: string[];
914
}
1015

11-
export function TracesSearchBar({query, handleSearch}: TracesSearchBarProps) {
16+
const getSpanName = (index: number) => {
17+
const spanNames = [t('Span A'), t('Span B'), t('Span C')];
18+
return spanNames[index];
19+
};
20+
21+
export function TracesSearchBar({
22+
queries,
23+
handleSearch,
24+
handleClearSearch,
25+
}: TracesSearchBarProps) {
1226
// TODO: load tags for autocompletion
1327
const organization = useOrganization();
28+
const canAddMoreQueries = queries.length <= 2;
29+
const localQueries = queries.length ? queries : [''];
1430
return (
15-
<SearchBar
16-
query={query}
17-
onSearch={handleSearch}
18-
placeholder={t('Filter by tags')}
19-
organization={organization}
20-
/>
31+
<TraceSearchBarsContainer>
32+
{localQueries.map((query, index) => (
33+
<TraceBar key={index}>
34+
<SpanLetter>{getSpanName(index)}</SpanLetter>
35+
<StyledSearchBar
36+
query={query}
37+
onSearch={(queryString: string) => handleSearch(index, queryString)}
38+
placeholder={t(
39+
'Search for traces containing a span matching these attributes'
40+
)}
41+
organization={organization}
42+
/>
43+
<StyledButton
44+
aria-label={t('Remove span')}
45+
icon={<IconClose size="sm" />}
46+
size="sm"
47+
onClick={() => (queries.length === 0 ? false : handleClearSearch(index))}
48+
/>
49+
</TraceBar>
50+
))}
51+
52+
{canAddMoreQueries ? (
53+
<Button
54+
aria-label={t('Add query')}
55+
icon={<IconAdd size="xs" isCircled />}
56+
size="sm"
57+
onClick={() => handleSearch(localQueries.length, '')}
58+
>
59+
{t('Add Span')}
60+
</Button>
61+
) : null}
62+
</TraceSearchBarsContainer>
2163
);
2264
}
65+
66+
const TraceSearchBarsContainer = styled('div')`
67+
display: flex;
68+
flex-direction: column;
69+
align-items: flex-start;
70+
justify-content: center;
71+
gap: ${space(1)};
72+
`;
73+
74+
const TraceBar = styled('div')`
75+
display: flex;
76+
flex-direction: row;
77+
align-items: center;
78+
justify-content: flex-start;
79+
width: 100%;
80+
gap: ${space(1)};
81+
`;
82+
83+
const SpanLetter = styled('div')`
84+
background-color: ${p => p.theme.purple100};
85+
border-radius: ${p => p.theme.borderRadius};
86+
padding: ${space(1)} ${space(2)};
87+
88+
color: ${p => p.theme.purple400};
89+
white-space: nowrap;
90+
font-weight: 800;
91+
`;
92+
93+
const StyledSearchBar = styled(SearchBar)`
94+
width: 100%;
95+
`;
96+
97+
const StyledButton = styled(Button)`
98+
height: 38px;
99+
`;

0 commit comments

Comments
 (0)