Skip to content

Commit 8c4d6bd

Browse files
Zylphrexk-fish
andauthored
feat(trace-explorer): Update trace explorer designs (#68827)
This updates the trace explorer to the current designs to render a list of traces with collapsible list of spans. --------- Co-authored-by: Kev <kevan.fisher@sentry.io>
1 parent f17db21 commit 8c4d6bd

File tree

4 files changed

+318
-268
lines changed

4 files changed

+318
-268
lines changed

static/app/components/idBadge/projectBadge.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export interface ProjectBadgeProps
2020
*/
2121
disableLink?: boolean;
2222
displayPlatformName?: boolean;
23+
/**
24+
* Hide project name and only display badge.
25+
*/
26+
hideName?: boolean;
2327
/**
2428
* If true, will use default max-width, or specify one as a string
2529
*/
@@ -34,6 +38,7 @@ function ProjectBadge({
3438
project,
3539
to,
3640
hideOverflow = true,
41+
hideName = false,
3742
disableLink = false,
3843
displayPlatformName = false,
3944
className,
@@ -44,6 +49,7 @@ function ProjectBadge({
4449

4550
const badge = (
4651
<BaseBadge
52+
hideName={hideName}
4753
displayName={
4854
<BadgeDisplayName hideOverflow={hideOverflow}>
4955
{displayPlatformName && project.platform
Lines changed: 271 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,48 @@
1-
import {useCallback, useMemo} from 'react';
1+
import {Fragment, useCallback, useMemo, useState} from 'react';
22
import {browserHistory} from 'react-router';
33
import styled from '@emotion/styled';
44

5+
import {Button} from 'sentry/components/button';
6+
import Count from 'sentry/components/count';
7+
import EmptyStateWarning from 'sentry/components/emptyStateWarning';
58
import * as Layout from 'sentry/components/layouts/thirds';
9+
import LoadingIndicator from 'sentry/components/loadingIndicator';
610
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
711
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
812
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
13+
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
914
import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
10-
import type {CursorHandler} from 'sentry/components/pagination';
11-
import Pagination from 'sentry/components/pagination';
15+
import Panel from 'sentry/components/panels/panel';
16+
import PanelHeader from 'sentry/components/panels/panelHeader';
17+
import PanelItem from 'sentry/components/panels/panelItem';
18+
import PerformanceDuration from 'sentry/components/performanceDuration';
1219
import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
20+
import {IconChevron} from 'sentry/icons/iconChevron';
21+
import {t} from 'sentry/locale';
1322
import {space} from 'sentry/styles/space';
14-
import {defined} from 'sentry/utils';
23+
import type {PageFilters} from 'sentry/types';
24+
import {useApiQuery} from 'sentry/utils/queryClient';
1525
import {decodeInteger, decodeScalar} from 'sentry/utils/queryString';
16-
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
1726
import {useLocation} from 'sentry/utils/useLocation';
18-
import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans';
27+
import useOrganization from 'sentry/utils/useOrganization';
28+
import usePageFilters from 'sentry/utils/usePageFilters';
1929

20-
import {fields} from './data';
21-
import {TraceRow} from './traceRow';
30+
import {ProjectRenderer, SpanIdRenderer, TraceIdRenderer} from './fieldRenderers';
2231
import {TracesSearchBar} from './tracesSearchBar';
2332

2433
const DEFAULT_PER_PAGE = 20;
2534

35+
const FIELDS = [
36+
'project',
37+
'transaction.id',
38+
'id',
39+
'timestamp',
40+
'span.op',
41+
'span.description',
42+
'span.duration',
43+
];
44+
type Field = (typeof FIELDS)[number];
45+
2646
export function Content() {
2747
const location = useLocation();
2848

@@ -48,43 +68,16 @@ export function Content() {
4868
[location]
4969
);
5070

51-
const handleCursor: CursorHandler = useCallback((newCursor, pathname, newQuery) => {
52-
browserHistory.push({
53-
pathname,
54-
query: {...newQuery, cursor: newCursor},
55-
});
56-
}, []);
57-
58-
const filters = useMemo(() => new MutableSearch(query ?? '').filters, [query]);
59-
60-
const spansQuery = useIndexedSpans({
61-
fields,
62-
filters,
71+
const traces = useTraces<Field>({
72+
fields: FIELDS,
6373
limit,
64-
sorts: [],
65-
referrer: 'api.trace-explorer.table',
74+
query,
6675
});
6776

68-
const traces = useMemo(() => {
69-
const data = (spansQuery.data ?? []).reduce((acc, span) => {
70-
const traceId = span.trace;
71-
if (!defined(traceId)) {
72-
// TODO: warn missing trace id
73-
return acc;
74-
}
75-
76-
let spansList = acc.get(traceId);
77-
if (!defined(spansList)) {
78-
spansList = [];
79-
acc.set(traceId, spansList);
80-
}
81-
82-
spansList.push(span);
83-
return acc;
84-
}, new Map());
85-
86-
return Array.from(data);
87-
}, [spansQuery.data]);
77+
const isLoading = traces.isFetching;
78+
const isError = !isLoading && traces.isError;
79+
const isEmpty = !isLoading && !isError && (traces?.data?.data?.length ?? 0) === 0;
80+
const data = !isLoading && !isError ? traces?.data?.data : undefined;
8881

8982
return (
9083
<LayoutMain fullWidth>
@@ -94,20 +87,250 @@ export function Content() {
9487
<DatePageFilter />
9588
</PageFilterBar>
9689
<TracesSearchBar query={query} handleSearch={handleSearch} />
97-
{traces.map(([traceId, spans]) => (
98-
<TraceRow key={traceId} traceId={traceId} spans={spans} />
99-
))}
100-
<StyledPagination pageLinks={spansQuery.pageLinks} onCursor={handleCursor} />
90+
<StyledPanel>
91+
<TracePanelContent>
92+
<StyledPanelHeader align="right" lightText>
93+
{t('Trace ID')}
94+
</StyledPanelHeader>
95+
<StyledPanelHeader align="left" lightText>
96+
{t('Trace Root Name')}
97+
</StyledPanelHeader>
98+
<StyledPanelHeader align="right" lightText>
99+
{t('Spans')}
100+
</StyledPanelHeader>
101+
<StyledPanelHeader align="right" lightText>
102+
{t('Breakdown')}
103+
</StyledPanelHeader>
104+
<StyledPanelHeader align="right" lightText>
105+
{t('Trace Duration')}
106+
</StyledPanelHeader>
107+
<StyledPanelHeader align="right" lightText>
108+
{t('Issues')}
109+
</StyledPanelHeader>
110+
{isLoading && (
111+
<StyledPanelItem span={6}>
112+
<LoadingIndicator />
113+
</StyledPanelItem>
114+
)}
115+
{isError && ( // TODO: need an error state
116+
<StyledPanelItem span={6}>
117+
<EmptyStateWarning withIcon />
118+
</StyledPanelItem>
119+
)}
120+
{isEmpty && (
121+
<StyledPanelItem span={6}>
122+
<EmptyStateWarning withIcon />
123+
</StyledPanelItem>
124+
)}
125+
{data?.map(trace => <TraceRow key={trace.trace} trace={trace} />)}
126+
</TracePanelContent>
127+
</StyledPanel>
101128
</LayoutMain>
102129
);
103130
}
104131

132+
function TraceRow({trace}: {trace: TraceResult<Field>}) {
133+
const [expanded, setExpanded] = useState<boolean>(false);
134+
return (
135+
<Fragment>
136+
<StyledPanelItem align="center" center>
137+
<Button
138+
icon={<IconChevron size="xs" direction={expanded ? 'down' : 'right'} />}
139+
aria-label={t('Toggle trace details')}
140+
aria-expanded={expanded}
141+
size="zero"
142+
borderless
143+
onClick={() => setExpanded(e => !e)}
144+
/>
145+
<TraceIdRenderer traceId={trace.trace} timestamp={trace.spans[0].timestamp} />
146+
</StyledPanelItem>
147+
<StyledPanelItem align="left">
148+
{trace.name ? (
149+
trace.name
150+
) : (
151+
<EmptyValueContainer>{t('No Name Available')}</EmptyValueContainer>
152+
)}
153+
</StyledPanelItem>
154+
<StyledPanelItem align="right">
155+
<Count value={trace.numSpans} />
156+
</StyledPanelItem>
157+
<StyledPanelItem align="right">
158+
<EmptyValueContainer>{'\u2014'}</EmptyValueContainer>
159+
</StyledPanelItem>
160+
<StyledPanelItem align="right">
161+
<PerformanceDuration milliseconds={trace.duration} abbreviation />
162+
</StyledPanelItem>
163+
<StyledPanelItem align="right">
164+
<EmptyValueContainer>{'\u2014'}</EmptyValueContainer>
165+
</StyledPanelItem>
166+
{expanded && (
167+
<StyledPanelItem span={6}>
168+
<StyledPanel>
169+
<SpanPanelContent>
170+
<StyledPanelHeader align="left" lightText>
171+
{t('Span ID')}
172+
</StyledPanelHeader>
173+
<StyledPanelHeader align="left" lightText>
174+
{t('Span Description')}
175+
</StyledPanelHeader>
176+
<StyledPanelHeader align="right" lightText />
177+
<StyledPanelHeader align="right" lightText>
178+
{t('Span Duration')}
179+
</StyledPanelHeader>
180+
<StyledPanelHeader align="right" lightText>
181+
{t('Issues')}
182+
</StyledPanelHeader>
183+
{trace.spans.map(span => (
184+
<SpanRow key={span.id} span={span} trace={trace.trace} />
185+
))}
186+
</SpanPanelContent>
187+
</StyledPanel>
188+
</StyledPanelItem>
189+
)}
190+
</Fragment>
191+
);
192+
}
193+
194+
function SpanRow({span, trace}: {span: SpanResult<Field>; trace: string}) {
195+
return (
196+
<Fragment>
197+
<StyledPanelItem align="right">
198+
<SpanIdRenderer
199+
projectSlug={span.project}
200+
transactionId={span['transaction.id']}
201+
spanId={span.id}
202+
trace={trace}
203+
timestamp={span.timestamp}
204+
/>
205+
</StyledPanelItem>
206+
<StyledPanelItem align="left">
207+
<Description>
208+
<ProjectRenderer projectSlug={span.project} hideName />
209+
<strong>{span['span.op']}</strong>
210+
<em>{'\u2014'}</em>
211+
{span['span.description']}
212+
</Description>
213+
</StyledPanelItem>
214+
<StyledPanelItem align="right">
215+
<EmptyValueContainer>{'\u2014'}</EmptyValueContainer>
216+
</StyledPanelItem>
217+
<StyledPanelItem align="right">
218+
<PerformanceDuration milliseconds={span['span.duration']} abbreviation />
219+
</StyledPanelItem>
220+
<StyledPanelItem align="right">
221+
<EmptyValueContainer>{'\u2014'}</EmptyValueContainer>
222+
</StyledPanelItem>
223+
</Fragment>
224+
);
225+
}
226+
227+
type SpanResult<F extends string> = Record<F, any>;
228+
229+
interface TraceResult<F extends string> {
230+
duration: number;
231+
name: string | null;
232+
numSpans: number;
233+
spans: SpanResult<F>[];
234+
trace: string;
235+
}
236+
237+
interface TraceResults<F extends string> {
238+
data: TraceResult<F>[];
239+
meta: any;
240+
}
241+
242+
interface UseTracesOptions<F extends string> {
243+
fields: F[];
244+
datetime?: PageFilters['datetime'];
245+
enabled?: boolean;
246+
limit?: number;
247+
query?: string;
248+
}
249+
250+
function useTraces<F extends string>({
251+
fields,
252+
datetime,
253+
enabled,
254+
limit,
255+
query,
256+
}: UseTracesOptions<F>) {
257+
const organization = useOrganization();
258+
const {selection} = usePageFilters();
259+
260+
const path = `/organizations/${organization.slug}/traces/`;
261+
262+
const endpointOptions = {
263+
query: {
264+
project: selection.projects,
265+
environment: selection.environments,
266+
...(datetime ?? normalizeDateTimeParams(selection.datetime)),
267+
field: fields,
268+
query,
269+
per_page: limit,
270+
maxSpansPerTrace: 10,
271+
},
272+
};
273+
274+
return useApiQuery<TraceResults<F>>([path, endpointOptions], {
275+
staleTime: 0,
276+
refetchOnWindowFocus: false,
277+
retry: false,
278+
enabled,
279+
});
280+
}
281+
105282
const LayoutMain = styled(Layout.Main)`
106283
display: flex;
107284
flex-direction: column;
108285
gap: ${space(2)};
109286
`;
110287

111-
const StyledPagination = styled(Pagination)`
112-
margin: 0px;
288+
const StyledPanel = styled(Panel)`
289+
margin-bottom: 0px;
290+
`;
291+
292+
const TracePanelContent = styled('div')`
293+
width: 100%;
294+
display: grid;
295+
grid-template-columns: repeat(1, min-content) auto repeat(4, min-content);
296+
`;
297+
298+
const SpanPanelContent = styled('div')`
299+
width: 100%;
300+
display: grid;
301+
grid-template-columns: repeat(1, min-content) auto repeat(3, min-content);
302+
`;
303+
304+
const StyledPanelHeader = styled(PanelHeader)<{align: 'left' | 'right'}>`
305+
white-space: nowrap;
306+
justify-content: ${p => (p.align === 'left' ? 'flex-start' : 'flex-end')};
307+
padding: ${space(2)} ${space(1)};
308+
`;
309+
310+
const Description = styled('div')`
311+
${p => p.theme.overflowEllipsis};
312+
display: flex;
313+
flex-direction: row;
314+
align-items: center;
315+
gap: ${space(1)};
316+
`;
317+
318+
const StyledPanelItem = styled(PanelItem)<{
319+
align?: 'left' | 'center' | 'right';
320+
span?: number;
321+
}>`
322+
padding: ${space(1)};
323+
${p => p.theme.overflowEllipsis};
324+
${p =>
325+
p.align === 'center'
326+
? `
327+
justify-content: space-around;`
328+
: p.align === 'left' || p.align === 'right'
329+
? `text-align: ${p.align};`
330+
: undefined}
331+
${p => p.span && `grid-column: auto / span ${p.span}`}
332+
`;
333+
334+
const EmptyValueContainer = styled('span')`
335+
color: ${p => p.theme.gray300};
113336
`;

0 commit comments

Comments
 (0)