Skip to content

Commit 2a1a7bf

Browse files
authored
feat(agents): Show agent names in traces table (#104079)
### Problem The name of the trace root is often not helpful when debugging agents, especially when working with distributed traces. ### Solution Display agent names instead of the trace root if available. Allow clicking agents to filter the page by them. https://github.com/user-attachments/assets/78564b87-76e8-400a-b0a8-13f2d613874c
1 parent 3d977e9 commit 2a1a7bf

File tree

2 files changed

+147
-17
lines changed

2 files changed

+147
-17
lines changed

static/app/utils/useHoverOverlay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ interface UseHoverOverlayProps {
140140
underlineColor?: ColorOrAlias;
141141
}
142142

143-
function isOverflown(el: Element): boolean {
143+
export function isOverflown(el: Element): boolean {
144144
// Safari seems to calculate scrollWidth incorrectly, causing isOverflown to always return true in some cases.
145145
// Adding a 2 pixel tolerance seems to account for this discrepancy.
146146
const tolerance =

static/app/views/insights/pages/agents/components/tracesTable.tsx

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import {Fragment, memo, useCallback, useMemo} from 'react';
1+
import {Fragment, memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
22
import styled from '@emotion/styled';
3+
import {parseAsString, useQueryState} from 'nuqs';
4+
5+
import {Tag} from '@sentry/scraps/badge/tag';
6+
import {Container} from '@sentry/scraps/layout';
7+
import {Flex} from '@sentry/scraps/layout/flex';
8+
import {Link} from '@sentry/scraps/link';
39

410
import {Button} from 'sentry/components/core/button';
11+
import {Text} from 'sentry/components/core/text';
512
import {Tooltip} from 'sentry/components/core/tooltip';
613
import Pagination from 'sentry/components/pagination';
14+
import Placeholder from 'sentry/components/placeholder';
15+
import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch';
716
import GridEditable, {
817
COL_WIDTH_UNDEFINED,
918
type GridColumnHeader,
@@ -13,15 +22,14 @@ import useStateBasedColumnResize from 'sentry/components/tables/gridEditable/use
1322
import TimeSince from 'sentry/components/timeSince';
1423
import {IconArrow} from 'sentry/icons';
1524
import {t} from 'sentry/locale';
25+
import {isOverflown} from 'sentry/utils/useHoverOverlay';
26+
import {useLocation} from 'sentry/utils/useLocation';
1627
import useOrganization from 'sentry/utils/useOrganization';
1728
import usePageFilters from 'sentry/utils/usePageFilters';
1829
import {SAMPLING_MODE} from 'sentry/views/explore/hooks/useProgressiveQuery';
1930
import {useTraces} from 'sentry/views/explore/hooks/useTraces';
2031
import {getExploreUrl} from 'sentry/views/explore/utils';
21-
import {
22-
OverflowEllipsisTextContainer,
23-
TextAlignRight,
24-
} from 'sentry/views/insights/common/components/textAlign';
32+
import {TextAlignRight} from 'sentry/views/insights/common/components/textAlign';
2533
import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';
2634
import {useTraceViewDrawer} from 'sentry/views/insights/pages/agents/components/drawer';
2735
import {LLMCosts} from 'sentry/views/insights/pages/agents/components/llmCosts';
@@ -40,6 +48,7 @@ import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/Du
4048
import {NumberCell} from 'sentry/views/insights/pages/platform/shared/table/NumberCell';
4149

4250
interface TableData {
51+
agents: string[];
4352
duration: number;
4453
errors: number;
4554
llmCalls: number;
@@ -49,14 +58,15 @@ interface TableData {
4958
totalTokens: number;
5059
traceId: string;
5160
transaction: string;
61+
isAgentDataLoading?: boolean;
5262
isSpanDataLoading?: boolean;
5363
}
5464

5565
const EMPTY_ARRAY: never[] = [];
5666

5767
const defaultColumnOrder: Array<GridColumnOrder<string>> = [
5868
{key: 'traceId', name: t('Trace ID'), width: 110},
59-
{key: 'transaction', name: t('Trace Root'), width: COL_WIDTH_UNDEFINED},
69+
{key: 'agents', name: t('Agents / Trace Root'), width: COL_WIDTH_UNDEFINED},
6070
{key: 'duration', name: t('Root Duration'), width: 130},
6171
{key: 'errors', name: t('Errors'), width: 100},
6272
{key: 'llmCalls', name: t('LLM Calls'), width: 110},
@@ -112,9 +122,31 @@ export function TracesTable() {
112122
Referrer.TRACES_TABLE
113123
);
114124

125+
const agentsRequest = useSpans(
126+
{
127+
search: `span.op:gen_ai.invoke_agent has:gen_ai.agent.name trace:[${tracesRequest.data?.data.map(span => `"${span.trace}"`).join(',')}]`,
128+
fields: ['trace', 'gen_ai.agent.name', 'timestamp'],
129+
sorts: [{field: 'timestamp', kind: 'asc'}],
130+
samplingMode: SAMPLING_MODE.HIGH_ACCURACY,
131+
enabled: Boolean(tracesRequest.data && tracesRequest.data.data.length > 0),
132+
},
133+
Referrer.TRACES_TABLE
134+
);
135+
136+
const traceAgents = useMemo<Map<string, Set<string>>>(() => {
137+
if (!agentsRequest.data) {
138+
return new Map();
139+
}
140+
return agentsRequest.data.reduce((acc, span) => {
141+
const agentsSet = acc.get(span.trace) ?? new Set();
142+
agentsSet.add(span['gen_ai.agent.name']);
143+
acc.set(span.trace, agentsSet);
144+
return acc;
145+
}, new Map<string, Set<string>>());
146+
}, [agentsRequest.data]);
147+
115148
const traceErrorRequest = useSpans(
116149
{
117-
// Get all spans with error status
118150
search: `span.status:internal_error trace:[${tracesRequest.data?.data.map(span => span.trace).join(',')}]`,
119151
fields: ['trace', 'count(span.duration)'],
120152
limit: tracesRequest.data?.data.length ?? 0,
@@ -175,21 +207,25 @@ export function TracesTable() {
175207
totalTokens: spanDataMap[span.trace]?.totalTokens ?? 0,
176208
totalCost: spanDataMap[span.trace]?.totalCost ?? null,
177209
timestamp: span.start,
210+
agents: Array.from(traceAgents.get(span.trace) ?? []),
211+
isAgentDataLoading: agentsRequest.isLoading,
178212
isSpanDataLoading: spansRequest.isLoading || traceErrorRequest.isLoading,
179213
}));
180214
}, [
181215
tracesRequest.data,
182216
spanDataMap,
183217
spansRequest.isLoading,
184218
traceErrorRequest.isLoading,
219+
traceAgents,
220+
agentsRequest.isLoading,
185221
]);
186222

187223
const renderHeadCell = useCallback((column: GridColumnHeader<string>) => {
188224
return (
189225
<HeadCell align={rightAlignColumns.has(column.key) ? 'right' : 'left'}>
190226
{column.name}
191227
{column.key === 'timestamp' && <IconArrow direction="down" size="xs" />}
192-
{column.key === 'transaction' && <CellExpander />}
228+
{column.key === 'agents' && <CellExpander />}
193229
</HeadCell>
194230
);
195231
}, []);
@@ -208,9 +244,9 @@ export function TracesTable() {
208244
isLoading={tracesRequest.isPending}
209245
error={tracesRequest.error}
210246
data={tableData}
247+
stickyHeader
211248
columnOrder={columnOrder}
212249
columnSortBy={EMPTY_ARRAY}
213-
stickyHeader
214250
grid={{
215251
renderBodyCell,
216252
renderHeadCell,
@@ -251,13 +287,23 @@ const BodyCell = memo(function BodyCell({
251287
</TraceIdButton>
252288
</span>
253289
);
254-
case 'transaction':
255-
return (
256-
<Tooltip title={dataRow.transaction} showOnlyOnOverflow skipWrapper>
257-
<OverflowEllipsisTextContainer>
258-
{dataRow.transaction}
259-
</OverflowEllipsisTextContainer>
260-
</Tooltip>
290+
case 'agents':
291+
if (dataRow.isAgentDataLoading) {
292+
return <Placeholder width="100%" height="16px" />;
293+
}
294+
return dataRow.agents.length > 0 ? (
295+
<AgentTags agents={dataRow.agents} />
296+
) : (
297+
<Container paddingLeft="xs">
298+
<Tooltip
299+
title={dataRow.transaction}
300+
maxWidth={500}
301+
showOnlyOnOverflow
302+
skipWrapper
303+
>
304+
<Text ellipsis>{dataRow.transaction}</Text>
305+
</Tooltip>
306+
</Container>
261307
);
262308
case 'duration':
263309
return <DurationCell milliseconds={dataRow.duration} />;
@@ -301,6 +347,90 @@ const BodyCell = memo(function BodyCell({
301347
}
302348
});
303349

350+
function AgentTags({agents}: {agents: string[]}) {
351+
const [showAll, setShowAll] = useState(false);
352+
const location = useLocation();
353+
const [searchQuery] = useQueryState('query', parseAsString.withDefault(''));
354+
const [showToggle, setShowToggle] = useState(false);
355+
const resizeObserverRef = useRef<ResizeObserver | null>(null);
356+
const containerRef = useRef<HTMLDivElement>(null);
357+
358+
const handleShowAll = useCallback(() => {
359+
setShowAll(!showAll);
360+
361+
if (!containerRef.current) return;
362+
// While the all tags are visible, observe the container to see if it displays more than one line (22px)
363+
// so we can reset the show all state accordingly
364+
const observer = new ResizeObserver(entries => {
365+
const containerElement = entries[0]?.target;
366+
if (!containerElement || containerElement.clientHeight > 22) return;
367+
setShowToggle(false);
368+
setShowAll(false);
369+
resizeObserverRef.current?.disconnect();
370+
resizeObserverRef.current = null;
371+
});
372+
resizeObserverRef.current = observer;
373+
observer.observe(containerRef.current);
374+
}, [showAll]);
375+
376+
// Cleanup the resize observer when the component unmounts
377+
useEffect(() => {
378+
return () => {
379+
resizeObserverRef.current?.disconnect();
380+
resizeObserverRef.current = null;
381+
};
382+
}, []);
383+
384+
return (
385+
<Flex
386+
align="start"
387+
direction="row"
388+
gap="sm"
389+
wrap={showAll ? 'wrap' : 'nowrap'}
390+
overflow="hidden"
391+
position="relative"
392+
ref={containerRef}
393+
onMouseEnter={event => {
394+
setShowToggle(isOverflown(event.currentTarget));
395+
}}
396+
onMouseLeave={() => setShowToggle(false)}
397+
>
398+
{agents.map(agent => (
399+
<Tooltip key={agent} title={t('Add to filter')} maxWidth={500} skipWrapper>
400+
<Link
401+
to={{
402+
query: {
403+
...location.query,
404+
query: new MutableSearch(searchQuery)
405+
.removeFilter('gen_ai.agent.name')
406+
.addFilterValues('gen_ai.agent.name', [agent])
407+
.formatString(),
408+
},
409+
}}
410+
>
411+
<Tag key={agent} type="default">
412+
{agent}
413+
</Tag>
414+
</Link>
415+
</Tooltip>
416+
))}
417+
{/* Placeholder for floating button */}
418+
<Container width="100px" height="20px" flexShrink={0} />
419+
<Container
420+
display={showToggle || showAll ? 'block' : 'none'}
421+
position="absolute"
422+
background="primary"
423+
padding="2xs xs 0 xl"
424+
style={{bottom: '0', right: '0'}}
425+
>
426+
<Button priority="link" size="xs" onClick={handleShowAll}>
427+
{showAll ? t('Show less') : t('Show all')}
428+
</Button>
429+
</Container>
430+
</Flex>
431+
);
432+
}
433+
304434
const GridEditableContainer = styled('div')`
305435
position: relative;
306436
margin-bottom: ${p => p.theme.space.md};

0 commit comments

Comments
 (0)