Skip to content

Commit 99c3e1c

Browse files
Abdkhan14Abdullah Khan
andauthored
feat(trace-eap-waterfall): Adding search to attrs section in drawer (#93919)
<img width="1431" alt="Screenshot 2025-06-19 at 3 36 07 PM" src="https://github.com/user-attachments/assets/32fe35a6-20ae-45f3-93a0-36e553264957" /> <img width="1434" alt="Screenshot 2025-06-19 at 3 35 50 PM" src="https://github.com/user-attachments/assets/e4a9e703-fadd-4df9-9df0-82071cdfa763" /> Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
1 parent df335b5 commit 99c3e1c

File tree

2 files changed

+194
-104
lines changed

2 files changed

+194
-104
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {useMemo, useState} from 'react';
2+
import type {Theme} from '@emotion/react';
3+
import styled from '@emotion/styled';
4+
import type {Location, LocationDescriptorObject} from 'history';
5+
6+
import Link from 'sentry/components/links/link';
7+
import BaseSearchBar from 'sentry/components/searchBar';
8+
import {t} from 'sentry/locale';
9+
import {space} from 'sentry/styles/space';
10+
import type {Organization} from 'sentry/types/organization';
11+
import type {Project} from 'sentry/types/project';
12+
import {trackAnalytics} from 'sentry/utils/analytics';
13+
import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
14+
import {FieldKey} from 'sentry/utils/fields';
15+
import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
16+
import type {AttributesFieldRendererProps} from 'sentry/views/explore/components/traceItemAttributes/attributesTree';
17+
import {AttributesTree} from 'sentry/views/explore/components/traceItemAttributes/attributesTree';
18+
import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
19+
import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
20+
import {FoldSection} from 'sentry/views/issueDetails/streamline/foldSection';
21+
import {SectionTitleWithQuestionTooltip} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/span';
22+
import {
23+
findSpanAttributeValue,
24+
getTraceAttributesTreeActions,
25+
sortAttributes,
26+
} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils';
27+
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
28+
import type {TraceTreeNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode';
29+
import {useTraceState} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
30+
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
31+
32+
type CustomRenderersProps = AttributesFieldRendererProps<RenderFunctionBaggage>;
33+
34+
export function Attributes({
35+
node,
36+
attributes,
37+
theme,
38+
location,
39+
organization,
40+
project,
41+
}: {
42+
attributes: TraceItemResponseAttribute[];
43+
location: Location;
44+
node: TraceTreeNode<TraceTree.EAPSpan>;
45+
organization: Organization;
46+
project: Project | undefined;
47+
theme: Theme;
48+
}) {
49+
const [searchQuery, setSearchQuery] = useState('');
50+
const traceState = useTraceState();
51+
const columnCount =
52+
traceState.preferences.layout === 'drawer left' ||
53+
traceState.preferences.layout === 'drawer right'
54+
? 1
55+
: undefined;
56+
57+
const sortedAndFilteredAttributes = useMemo(() => {
58+
const sorted = sortAttributes(attributes);
59+
if (!searchQuery.trim()) {
60+
return sorted;
61+
}
62+
63+
return sorted.filter(attribute =>
64+
attribute.name.toLowerCase().trim().includes(searchQuery.toLowerCase().trim())
65+
);
66+
}, [attributes, searchQuery]);
67+
68+
const customRenderers = {
69+
[FieldKey.PROFILE_ID]: (props: CustomRenderersProps) => {
70+
const target = generateProfileFlamechartRoute({
71+
organization,
72+
projectSlug: project?.slug ?? '',
73+
profileId: String(props.item.value),
74+
});
75+
76+
return (
77+
<StyledLink
78+
data-test-id="view-profile"
79+
to={{
80+
pathname: target,
81+
query: {
82+
spanId: node.value.event_id,
83+
},
84+
}}
85+
onClick={() =>
86+
trackAnalytics('profiling_views.go_to_flamegraph', {
87+
organization,
88+
source: 'performance.trace_view.details',
89+
})
90+
}
91+
>
92+
{props.item.value}
93+
</StyledLink>
94+
);
95+
},
96+
[FieldKey.REPLAY_ID]: (props: CustomRenderersProps) => {
97+
const target: LocationDescriptorObject = {
98+
pathname: makeReplaysPathname({
99+
path: `/${props.item.value}/`,
100+
organization,
101+
}),
102+
query: {
103+
event_t: node.value.start_timestamp,
104+
referrer: 'performance.trace_view.details',
105+
},
106+
};
107+
return <StyledLink to={target}>{props.item.value}</StyledLink>;
108+
},
109+
};
110+
111+
return (
112+
<FoldSection
113+
sectionKey={SectionKey.SPAN_ATTRIBUTES}
114+
title={
115+
<SectionTitleWithQuestionTooltip
116+
title={t('Attributes')}
117+
tooltipText={t(
118+
'These attributes are indexed and can be queried in the Trace Explorer.'
119+
)}
120+
/>
121+
}
122+
disableCollapsePersistence
123+
>
124+
<ContentWrapper>
125+
<BaseSearchBar
126+
placeholder={t('Search')}
127+
onChange={query => setSearchQuery(query)}
128+
query={searchQuery}
129+
size="sm"
130+
/>
131+
{sortedAndFilteredAttributes.length > 0 ? (
132+
<AttributesTreeWrapper>
133+
<AttributesTree
134+
columnCount={columnCount}
135+
attributes={sortedAndFilteredAttributes}
136+
renderers={customRenderers}
137+
rendererExtra={{
138+
theme,
139+
location,
140+
organization,
141+
}}
142+
getCustomActions={getTraceAttributesTreeActions({
143+
location,
144+
organization,
145+
projectIds: findSpanAttributeValue(attributes, 'project_id'),
146+
})}
147+
/>
148+
</AttributesTreeWrapper>
149+
) : (
150+
<NoAttributesMessage>
151+
<p>{t('No matching attributes found')}</p>
152+
</NoAttributesMessage>
153+
)}
154+
</ContentWrapper>
155+
</FoldSection>
156+
);
157+
}
158+
159+
const StyledLink = styled(Link)`
160+
& div {
161+
display: inline;
162+
}
163+
`;
164+
165+
const ContentWrapper = styled('div')`
166+
display: flex;
167+
flex-direction: column;
168+
max-width: 100%;
169+
gap: ${space(1.5)};
170+
`;
171+
172+
const AttributesTreeWrapper = styled('div')`
173+
padding-left: ${space(1)};
174+
`;
175+
176+
const NoAttributesMessage = styled('div')`
177+
display: flex;
178+
justify-content: center;
179+
align-items: center;
180+
margin-top: ${space(4)};
181+
color: ${p => p.theme.subText};
182+
`;

static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx

Lines changed: 12 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Fragment, useMemo, useRef} from 'react';
22
import {type Theme, useTheme} from '@emotion/react';
33
import styled from '@emotion/styled';
4-
import type {Location, LocationDescriptorObject} from 'history';
4+
import type {Location} from 'history';
55

66
import {EventAttachments} from 'sentry/components/events/eventAttachments';
77
import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
@@ -11,7 +11,6 @@ import {
1111
useSpanProfileDetails,
1212
} from 'sentry/components/events/interfaces/spans/spanProfileDetails';
1313
import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration';
14-
import Link from 'sentry/components/links/link';
1514
import LoadingError from 'sentry/components/loadingError';
1615
import LoadingIndicator from 'sentry/components/loadingIndicator';
1716
import QuestionTooltip from 'sentry/components/questionTooltip';
@@ -21,18 +20,12 @@ import {EntryType, type EventTransaction} from 'sentry/types/event';
2120
import type {NewQuery, Organization} from 'sentry/types/organization';
2221
import type {Project} from 'sentry/types/project';
2322
import {defined} from 'sentry/utils';
24-
import {trackAnalytics} from 'sentry/utils/analytics';
2523
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
2624
import EventView from 'sentry/utils/discover/eventView';
27-
import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
28-
import {FieldKey} from 'sentry/utils/fields';
29-
import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
3025
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
3126
import {useLocation} from 'sentry/utils/useLocation';
3227
import useOrganization from 'sentry/utils/useOrganization';
3328
import useProjects from 'sentry/utils/useProjects';
34-
import type {AttributesFieldRendererProps} from 'sentry/views/explore/components/traceItemAttributes/attributesTree';
35-
import {AttributesTree} from 'sentry/views/explore/components/traceItemAttributes/attributesTree';
3629
import {
3730
LogsPageDataProvider,
3831
useLogsPageDataQueryResult,
@@ -51,27 +44,21 @@ import {useTransaction} from 'sentry/views/performance/newTraceDetails/traceApi/
5144
import {IssueList} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/issues/issues';
5245
import {AIInputSection} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/aiInput';
5346
import {AIOutputSection} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/aiOutput';
47+
import {Attributes} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes';
5448
import {TraceDrawerComponents} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/styles';
5549
import {BreadCrumbs} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/breadCrumbs';
5650
import ReplayPreview from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/replayPreview';
5751
import {Request} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/request';
58-
import {
59-
findSpanAttributeValue,
60-
getProfileMeta,
61-
getTraceAttributesTreeActions,
62-
sortAttributes,
63-
} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils';
52+
import {getProfileMeta} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils';
6453
import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
6554
import {
6655
isEAPSpanNode,
6756
isEAPTransactionNode,
6857
} from 'sentry/views/performance/newTraceDetails/traceGuards';
6958
import {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
7059
import type {TraceTreeNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode';
71-
import {useTraceState} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
7260
import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
7361
import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
74-
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
7562

7663
import {SpanDescription as EAPSpanDescription} from './eapSections/description';
7764
import Alerts from './sections/alerts';
@@ -356,8 +343,6 @@ function useAvgSpanDuration(
356343
return result.data?.[0]?.['avg(span.duration)'];
357344
}
358345

359-
type CustomRenderersProps = AttributesFieldRendererProps<RenderFunctionBaggage>;
360-
361346
type EAPSpanNodeDetailsProps = TraceTreeNodeDetailsProps<
362347
TraceTreeNode<TraceTree.EAPSpan>
363348
> & {
@@ -407,8 +392,6 @@ function EAPSpanNodeDetails({
407392

408393
const avgSpanDuration = useAvgSpanDuration(node.value, location);
409394

410-
const traceState = useTraceState();
411-
412395
if (isTraceItemPending || isEventTransactionLoading) {
413396
return <LoadingIndicator />;
414397
}
@@ -418,12 +401,6 @@ function EAPSpanNodeDetails({
418401
}
419402

420403
const attributes = traceItemData?.attributes;
421-
const columnCount =
422-
traceState.preferences.layout === 'drawer left' ||
423-
traceState.preferences.layout === 'drawer right'
424-
? 1
425-
: undefined;
426-
427404
const isTransaction = isEAPTransactionNode(node) && !!eventTransaction;
428405
const profileMeta = eventTransaction ? getProfileMeta(eventTransaction) || '' : '';
429406
const profileId =
@@ -433,49 +410,6 @@ function EAPSpanNodeDetails({
433410
entry => entry.type === EntryType.REQUEST
434411
);
435412

436-
const customRenderers = {
437-
[FieldKey.PROFILE_ID]: (props: CustomRenderersProps) => {
438-
const target = generateProfileFlamechartRoute({
439-
organization,
440-
projectSlug: project?.slug ?? '',
441-
profileId: String(props.item.value),
442-
});
443-
444-
return (
445-
<StyledLink
446-
data-test-id="view-profile"
447-
to={{
448-
pathname: target,
449-
query: {
450-
spanId: node.value.event_id,
451-
},
452-
}}
453-
onClick={() =>
454-
trackAnalytics('profiling_views.go_to_flamegraph', {
455-
organization,
456-
source: 'performance.trace_view.details',
457-
})
458-
}
459-
>
460-
{props.item.value}
461-
</StyledLink>
462-
);
463-
},
464-
[FieldKey.REPLAY_ID]: (props: CustomRenderersProps) => {
465-
const target: LocationDescriptorObject = {
466-
pathname: makeReplaysPathname({
467-
path: `/${props.item.value}/`,
468-
organization,
469-
}),
470-
query: {
471-
event_t: node.value.start_timestamp,
472-
referrer: 'performance.trace_view.details',
473-
},
474-
};
475-
return <StyledLink to={target}>{props.item.value}</StyledLink>;
476-
},
477-
};
478-
479413
return (
480414
<TraceDrawerComponents.DetailContainer>
481415
<SpanNodeDetailHeader
@@ -521,34 +455,14 @@ function EAPSpanNodeDetails({
521455
/>
522456
<AIInputSection node={node} attributes={attributes} />
523457
<AIOutputSection node={node} attributes={attributes} />
524-
<FoldSection
525-
sectionKey={SectionKey.SPAN_ATTRIBUTES}
526-
title={
527-
<SectionTitleWithQuestionTooltip
528-
title={t('Attributes')}
529-
tooltipText={t(
530-
'These attributes are indexed and can be queried in the Trace Explorer.'
531-
)}
532-
/>
533-
}
534-
disableCollapsePersistence
535-
>
536-
<AttributesTree
537-
columnCount={columnCount}
538-
attributes={sortAttributes(attributes)}
539-
renderers={customRenderers}
540-
rendererExtra={{
541-
theme,
542-
location,
543-
organization,
544-
}}
545-
getCustomActions={getTraceAttributesTreeActions({
546-
location,
547-
organization,
548-
projectIds: findSpanAttributeValue(attributes, 'project_id'),
549-
})}
550-
/>
551-
</FoldSection>
458+
<Attributes
459+
node={node}
460+
attributes={attributes}
461+
theme={theme}
462+
location={location}
463+
organization={organization}
464+
project={project}
465+
/>
552466

553467
{isTransaction && eventHasRequestEntry ? (
554468
<FoldSection
@@ -628,7 +542,7 @@ function EAPSpanNodeDetails({
628542
);
629543
}
630544

631-
function SectionTitleWithQuestionTooltip({
545+
export function SectionTitleWithQuestionTooltip({
632546
title,
633547
tooltipText,
634548
}: {
@@ -648,9 +562,3 @@ const Flex = styled('div')`
648562
align-items: center;
649563
gap: ${space(0.5)};
650564
`;
651-
652-
const StyledLink = styled(Link)`
653-
& div {
654-
display: inline;
655-
}
656-
`;

0 commit comments

Comments
 (0)