1- import { Fragment , memo , useCallback , useMemo } from 'react' ;
1+ import { Fragment , memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
22import 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
410import { Button } from 'sentry/components/core/button' ;
11+ import { Text } from 'sentry/components/core/text' ;
512import { Tooltip } from 'sentry/components/core/tooltip' ;
613import Pagination from 'sentry/components/pagination' ;
14+ import Placeholder from 'sentry/components/placeholder' ;
15+ import { MutableSearch } from 'sentry/components/searchSyntax/mutableSearch' ;
716import GridEditable , {
817 COL_WIDTH_UNDEFINED ,
918 type GridColumnHeader ,
@@ -13,15 +22,14 @@ import useStateBasedColumnResize from 'sentry/components/tables/gridEditable/use
1322import TimeSince from 'sentry/components/timeSince' ;
1423import { IconArrow } from 'sentry/icons' ;
1524import { t } from 'sentry/locale' ;
25+ import { isOverflown } from 'sentry/utils/useHoverOverlay' ;
26+ import { useLocation } from 'sentry/utils/useLocation' ;
1627import useOrganization from 'sentry/utils/useOrganization' ;
1728import usePageFilters from 'sentry/utils/usePageFilters' ;
1829import { SAMPLING_MODE } from 'sentry/views/explore/hooks/useProgressiveQuery' ;
1930import { useTraces } from 'sentry/views/explore/hooks/useTraces' ;
2031import { 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' ;
2533import { useSpans } from 'sentry/views/insights/common/queries/useDiscover' ;
2634import { useTraceViewDrawer } from 'sentry/views/insights/pages/agents/components/drawer' ;
2735import { LLMCosts } from 'sentry/views/insights/pages/agents/components/llmCosts' ;
@@ -40,6 +48,7 @@ import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/Du
4048import { NumberCell } from 'sentry/views/insights/pages/platform/shared/table/NumberCell' ;
4149
4250interface 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
5565const EMPTY_ARRAY : never [ ] = [ ] ;
5666
5767const 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+
304434const GridEditableContainer = styled ( 'div' ) `
305435 position: relative;
306436 margin-bottom: ${ p => p . theme . space . md } ;
0 commit comments