From 7b0acd554b4f06b1b547f3e580b97406e78a428f Mon Sep 17 00:00:00 2001 From: Arthur Knaus Date: Wed, 26 Nov 2025 23:01:52 +0100 Subject: [PATCH 1/4] feat(agents): Show agent names in traces table --- .../pages/agents/components/tracesTable.tsx | 85 ++++++++++++++++--- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index fb23ea909dc2fc..4590e3f5155b89 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -1,9 +1,14 @@ import {Fragment, memo, useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; +import {Tag} from '@sentry/scraps/badge/tag'; +import {Flex} from '@sentry/scraps/layout/flex'; + import {Button} from 'sentry/components/core/button'; +import {Text} from 'sentry/components/core/text'; import {Tooltip} from 'sentry/components/core/tooltip'; import Pagination from 'sentry/components/pagination'; +import Placeholder from 'sentry/components/placeholder'; import GridEditable, { COL_WIDTH_UNDEFINED, type GridColumnHeader, @@ -18,10 +23,7 @@ import usePageFilters from 'sentry/utils/usePageFilters'; import {SAMPLING_MODE} from 'sentry/views/explore/hooks/useProgressiveQuery'; import {useTraces} from 'sentry/views/explore/hooks/useTraces'; import {getExploreUrl} from 'sentry/views/explore/utils'; -import { - OverflowEllipsisTextContainer, - TextAlignRight, -} from 'sentry/views/insights/common/components/textAlign'; +import {TextAlignRight} from 'sentry/views/insights/common/components/textAlign'; import {useSpans} from 'sentry/views/insights/common/queries/useDiscover'; import {useTraceViewDrawer} from 'sentry/views/insights/pages/agents/components/drawer'; import {LLMCosts} from 'sentry/views/insights/pages/agents/components/llmCosts'; @@ -40,6 +42,7 @@ import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/Du import {NumberCell} from 'sentry/views/insights/pages/platform/shared/table/NumberCell'; interface TableData { + agents: string[]; duration: number; errors: number; llmCalls: number; @@ -49,6 +52,7 @@ interface TableData { totalTokens: number; traceId: string; transaction: string; + isAgentDataLoading?: boolean; isSpanDataLoading?: boolean; } @@ -56,7 +60,7 @@ const EMPTY_ARRAY: never[] = []; const defaultColumnOrder: Array> = [ {key: 'traceId', name: t('Trace ID'), width: 110}, - {key: 'transaction', name: t('Trace Root'), width: COL_WIDTH_UNDEFINED}, + {key: 'agents', name: t('Agents'), width: COL_WIDTH_UNDEFINED}, {key: 'duration', name: t('Root Duration'), width: 130}, {key: 'errors', name: t('Errors'), width: 100}, {key: 'llmCalls', name: t('LLM Calls'), width: 110}, @@ -112,9 +116,32 @@ export function TracesTable() { Referrer.TRACES_TABLE ); + const agentsRequest = useSpans( + { + search: `span.op:gen_ai.invoke_agent has:gen_ai.agent.name trace:[${tracesRequest.data?.data.map(span => `"${span.trace}"`).join(',')}]`, + fields: ['trace', 'gen_ai.agent.name', 'timestamp'], + sorts: [{field: 'timestamp', kind: 'asc'}], + samplingMode: SAMPLING_MODE.HIGH_ACCURACY, + enabled: Boolean(tracesRequest.data && tracesRequest.data.data.length > 0), + limit: 100, + }, + Referrer.TRACES_TABLE + ); + + const traceAgents = useMemo>>(() => { + if (!agentsRequest.data) { + return new Map(); + } + return agentsRequest.data.reduce((acc, span) => { + const agentsSet = acc.get(span.trace) ?? new Set(); + agentsSet.add(span['gen_ai.agent.name']); + acc.set(span.trace, agentsSet); + return acc; + }, new Map>()); + }, [agentsRequest.data]); + const traceErrorRequest = useSpans( { - // Get all spans with error status search: `span.status:internal_error trace:[${tracesRequest.data?.data.map(span => span.trace).join(',')}]`, fields: ['trace', 'count(span.duration)'], limit: tracesRequest.data?.data.length ?? 0, @@ -175,6 +202,8 @@ export function TracesTable() { totalTokens: spanDataMap[span.trace]?.totalTokens ?? 0, totalCost: spanDataMap[span.trace]?.totalCost ?? null, timestamp: span.start, + agents: Array.from(traceAgents.get(span.trace) ?? []), + isAgentDataLoading: agentsRequest.isLoading, isSpanDataLoading: spansRequest.isLoading || traceErrorRequest.isLoading, })); }, [ @@ -182,6 +211,8 @@ export function TracesTable() { spanDataMap, spansRequest.isLoading, traceErrorRequest.isLoading, + traceAgents, + agentsRequest.isLoading, ]); const renderHeadCell = useCallback((column: GridColumnHeader) => { @@ -189,7 +220,7 @@ export function TracesTable() { {column.name} {column.key === 'timestamp' && } - {column.key === 'transaction' && } + {column.key === 'agents' && } ); }, []); @@ -208,9 +239,9 @@ export function TracesTable() { isLoading={tracesRequest.isPending} error={tracesRequest.error} data={tableData} + stickyHeader columnOrder={columnOrder} columnSortBy={EMPTY_ARRAY} - stickyHeader grid={{ renderBodyCell, renderHeadCell, @@ -237,6 +268,8 @@ const BodyCell = memo(function BodyCell({ const {selection} = usePageFilters(); const {openTraceViewDrawer} = useTraceViewDrawer(); + const agentFlow = dataRow.agents.join(', '); + switch (column.key) { case 'traceId': return ( @@ -251,13 +284,37 @@ const BodyCell = memo(function BodyCell({ ); - case 'transaction': - return ( - - - {dataRow.transaction} - + case 'agents': + if (dataRow.isAgentDataLoading) { + return ; + } + return agentFlow.length > 0 ? ( + + {dataRow.agents.map(agent => ( + + {agent} + + ))} + + } + maxWidth={500} + showOnlyOnOverflow + skipWrapper + > + + {dataRow.agents.map(agent => ( + + {agent} + + ))} + + ) : ( + + {t('(no value)')} + ); case 'duration': return ; From db8817033509c7f0610c06f428414736e781099d Mon Sep 17 00:00:00 2001 From: Arthur Knaus Date: Wed, 26 Nov 2025 23:32:48 +0100 Subject: [PATCH 2/4] Remove limit --- .../app/views/insights/pages/agents/components/tracesTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index 4590e3f5155b89..015110304af13f 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -123,7 +123,6 @@ export function TracesTable() { sorts: [{field: 'timestamp', kind: 'asc'}], samplingMode: SAMPLING_MODE.HIGH_ACCURACY, enabled: Boolean(tracesRequest.data && tracesRequest.data.data.length > 0), - limit: 100, }, Referrer.TRACES_TABLE ); From 7fbe4effde9f85700b95d04e35421ffc2604cbbc Mon Sep 17 00:00:00 2001 From: Arthur Knaus Date: Thu, 27 Nov 2025 12:24:38 +0100 Subject: [PATCH 3/4] Implement feedback --- static/app/utils/useHoverOverlay.tsx | 2 +- .../pages/agents/components/tracesTable.tsx | 126 +++++++++++++----- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/static/app/utils/useHoverOverlay.tsx b/static/app/utils/useHoverOverlay.tsx index 95f2d8ff821ed4..42904bbf7b2214 100644 --- a/static/app/utils/useHoverOverlay.tsx +++ b/static/app/utils/useHoverOverlay.tsx @@ -140,7 +140,7 @@ interface UseHoverOverlayProps { underlineColor?: ColorOrAlias; } -function isOverflown(el: Element): boolean { +export function isOverflown(el: Element): boolean { // Safari seems to calculate scrollWidth incorrectly, causing isOverflown to always return true in some cases. // Adding a 2 pixel tolerance seems to account for this discrepancy. const tolerance = diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index 015110304af13f..98823a55f660e2 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -1,14 +1,18 @@ -import {Fragment, memo, useCallback, useMemo} from 'react'; +import {Fragment, memo, useCallback, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {parseAsString, useQueryState} from 'nuqs'; import {Tag} from '@sentry/scraps/badge/tag'; +import {Container} from '@sentry/scraps/layout'; import {Flex} from '@sentry/scraps/layout/flex'; +import {Link} from '@sentry/scraps/link'; import {Button} from 'sentry/components/core/button'; import {Text} from 'sentry/components/core/text'; import {Tooltip} from 'sentry/components/core/tooltip'; import Pagination from 'sentry/components/pagination'; import Placeholder from 'sentry/components/placeholder'; +import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch'; import GridEditable, { COL_WIDTH_UNDEFINED, type GridColumnHeader, @@ -18,6 +22,8 @@ import useStateBasedColumnResize from 'sentry/components/tables/gridEditable/use import TimeSince from 'sentry/components/timeSince'; import {IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; +import {isOverflown} from 'sentry/utils/useHoverOverlay'; +import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {SAMPLING_MODE} from 'sentry/views/explore/hooks/useProgressiveQuery'; @@ -60,7 +66,7 @@ const EMPTY_ARRAY: never[] = []; const defaultColumnOrder: Array> = [ {key: 'traceId', name: t('Trace ID'), width: 110}, - {key: 'agents', name: t('Agents'), width: COL_WIDTH_UNDEFINED}, + {key: 'agents', name: t('Agents / Trace Root'), width: COL_WIDTH_UNDEFINED}, {key: 'duration', name: t('Root Duration'), width: 130}, {key: 'errors', name: t('Errors'), width: 100}, {key: 'llmCalls', name: t('LLM Calls'), width: 110}, @@ -267,8 +273,6 @@ const BodyCell = memo(function BodyCell({ const {selection} = usePageFilters(); const {openTraceViewDrawer} = useTraceViewDrawer(); - const agentFlow = dataRow.agents.join(', '); - switch (column.key) { case 'traceId': return ( @@ -287,33 +291,19 @@ const BodyCell = memo(function BodyCell({ if (dataRow.isAgentDataLoading) { return ; } - return agentFlow.length > 0 ? ( - - {dataRow.agents.map(agent => ( - - {agent} - - ))} - - } - maxWidth={500} - showOnlyOnOverflow - skipWrapper - > - - {dataRow.agents.map(agent => ( - - {agent} - - ))} - - + return dataRow.agents.length > 0 ? ( + ) : ( - - {t('(no value)')} - + + + {dataRow.transaction} + + ); case 'duration': return ; @@ -357,6 +347,82 @@ const BodyCell = memo(function BodyCell({ } }); +function AgentTags({agents}: {agents: string[]}) { + const [showAll, setShowAll] = useState(false); + const location = useLocation(); + const [searchQuery] = useQueryState('query', parseAsString.withDefault('')); + const [showToggle, setShowToggle] = useState(false); + const resizeObserverRef = useRef(null); + const containerRef = useRef(null); + + const handleShowAll = useCallback(() => { + setShowAll(!showAll); + + if (!containerRef.current) return; + // While the all tags are visible, observe the container to see if it displays more than one line (22px) + // so we can reset the show all state accordingly + const observer = new ResizeObserver(entries => { + const containerElement = entries[0]?.target; + if (!containerElement || containerElement.clientHeight > 22) return; + setShowToggle(false); + setShowAll(false); + resizeObserverRef.current?.disconnect(); + resizeObserverRef.current = null; + }); + resizeObserverRef.current = observer; + observer.observe(containerRef.current); + }, [showAll]); + + return ( + { + setShowToggle(isOverflown(event.currentTarget)); + }} + onMouseLeave={() => setShowToggle(false)} + > + {agents.map(agent => ( + + + + {agent} + + + + ))} + {/* Placeholder for floating button */} + + + + + + ); +} + const GridEditableContainer = styled('div')` position: relative; margin-bottom: ${p => p.theme.space.md}; From cf99efc6f555ab4d2e36f7ed5dbbc5e956920157 Mon Sep 17 00:00:00 2001 From: Arthur Knaus Date: Thu, 27 Nov 2025 12:30:27 +0100 Subject: [PATCH 4/4] Cleanup ResizeObserver --- .../insights/pages/agents/components/tracesTable.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index 98823a55f660e2..558fe3f4d679e8 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -1,4 +1,4 @@ -import {Fragment, memo, useCallback, useMemo, useRef, useState} from 'react'; +import {Fragment, memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {parseAsString, useQueryState} from 'nuqs'; @@ -373,6 +373,14 @@ function AgentTags({agents}: {agents: string[]}) { observer.observe(containerRef.current); }, [showAll]); + // Cleanup the resize observer when the component unmounts + useEffect(() => { + return () => { + resizeObserverRef.current?.disconnect(); + resizeObserverRef.current = null; + }; + }, []); + return (