11import styled from '@emotion/styled' ;
22import * as Sentry from '@sentry/react' ;
33
4+ import { Tag } from '@sentry/scraps/badge' ;
5+ import { Flex } from '@sentry/scraps/layout' ;
6+
47import { Tooltip } from 'sentry/components/core/tooltip' ;
58import Count from 'sentry/components/count' ;
69import { StructuredData } from 'sentry/components/structuredEventData' ;
7- import { t } from 'sentry/locale' ;
10+ import { t , tn } from 'sentry/locale' ;
811import { prettifyAttributeName } from 'sentry/views/explore/components/traceItemAttributes/utils' ;
912import type { TraceItemResponseAttribute } from 'sentry/views/explore/hooks/useTraceItemDetails' ;
1013import { LLMCosts } from 'sentry/views/insights/agents/components/llmCosts' ;
1114import { ModelName } from 'sentry/views/insights/agents/components/modelName' ;
12- import { AI_CREATE_AGENT_OPS , getIsAiSpan } from 'sentry/views/insights/agents/utils/query' ;
15+ import {
16+ AI_CREATE_AGENT_OPS ,
17+ AI_RUN_OPS ,
18+ getIsAiSpan ,
19+ getToolSpansFilter ,
20+ } from 'sentry/views/insights/agents/utils/query' ;
21+ import { Referrer } from 'sentry/views/insights/agents/utils/referrers' ;
22+ import { useSpans } from 'sentry/views/insights/common/queries/useDiscover' ;
23+ import { SpanFields } from 'sentry/views/insights/types' ;
1324
1425type HighlightedAttribute = {
1526 name : string ;
@@ -26,15 +37,17 @@ function tryParseJson(value: string) {
2637
2738export function getHighlightedSpanAttributes ( {
2839 op,
40+ spanId,
2941 attributes = { } ,
3042} : {
3143 attributes : Record < string , string > | undefined | TraceItemResponseAttribute [ ] ;
3244 op : string | undefined ;
45+ spanId : string ;
3346} ) : HighlightedAttribute [ ] {
3447 const attributeObject = ensureAttributeObject ( attributes ) ;
3548
3649 if ( getIsAiSpan ( { op} ) ) {
37- return getAISpanAttributes ( attributeObject , op ) ;
50+ return getAISpanAttributes ( { attributes : attributeObject , op, spanId } ) ;
3851 }
3952
4053 if ( op ?. startsWith ( 'mcp.' ) ) {
@@ -62,10 +75,15 @@ function ensureAttributeObject(
6275 return attributes ;
6376}
6477
65- function getAISpanAttributes (
66- attributes : Record < string , string | number | boolean > ,
67- op ?: string
68- ) {
78+ function getAISpanAttributes ( {
79+ op,
80+ spanId,
81+ attributes = { } ,
82+ } : {
83+ attributes : Record < string , string | number | boolean > ;
84+ op : string | undefined ;
85+ spanId : string ;
86+ } ) {
6987 const highlightedAttributes = [ ] ;
7088
7189 const agentName = attributes [ 'gen_ai.agent.name' ] || attributes [ 'gen_ai.function_id' ] ;
@@ -142,16 +160,16 @@ function getAISpanAttributes(
142160 }
143161
144162 const availableTools = attributes [ 'gen_ai.request.available_tools' ] ;
145- if ( availableTools && AI_CREATE_AGENT_OPS . includes ( op ! ) ) {
163+ const toolsArray = tryParseJson ( availableTools ?. toString ( ) || '' ) ;
164+ if (
165+ toolsArray &&
166+ Array . isArray ( toolsArray ) &&
167+ toolsArray . length > 0 &&
168+ [ ...AI_RUN_OPS , ...AI_CREATE_AGENT_OPS ] . includes ( op ! )
169+ ) {
146170 highlightedAttributes . push ( {
147171 name : t ( 'Available Tools' ) ,
148- value : (
149- < StructuredData
150- value = { tryParseJson ( availableTools . toString ( ) ) }
151- withAnnotatedText
152- maxDefaultDepth = { 0 }
153- />
154- ) ,
172+ value : < HighlightedTools availableTools = { toolsArray } spanId = { spanId } /> ,
155173 } ) ;
156174 }
157175
@@ -196,6 +214,61 @@ function getMCPAttributes(attributes: Record<string, string | number | boolean>)
196214 return highlightedAttributes ;
197215}
198216
217+ function HighlightedTools ( {
218+ availableTools,
219+ spanId,
220+ } : {
221+ availableTools : any [ ] ;
222+ spanId : string ;
223+ } ) {
224+ const toolNames = availableTools . map ( tool => tool . name ) . filter ( Boolean ) ;
225+ const hasToolNames = toolNames . length > 0 ;
226+ const toolSpansQuery = useSpans (
227+ {
228+ search : `parent_span:${ spanId } has:${ SpanFields . GEN_AI_TOOL_NAME } ${ getToolSpansFilter ( ) } ` ,
229+ fields : [ SpanFields . GEN_AI_TOOL_NAME ] ,
230+ enabled : hasToolNames ,
231+ } ,
232+ Referrer . TRACE_DRAWER_TOOL_USAGE
233+ ) ;
234+
235+ const usedTools : Map < string , number > = new Map ( ) ;
236+ toolSpansQuery . data ?. forEach ( span => {
237+ const toolName = span [ SpanFields . GEN_AI_TOOL_NAME ] ;
238+ usedTools . set ( toolName , ( usedTools . get ( toolName ) ?? 0 ) + 1 ) ;
239+ } ) ;
240+
241+ // Fall back to showing formatted JSON if tool names cannot be parsed
242+ if ( ! hasToolNames ) {
243+ return (
244+ < StructuredData value = { availableTools } withAnnotatedText maxDefaultDepth = { 0 } />
245+ ) ;
246+ }
247+
248+ return (
249+ < Flex direction = "row" gap = "xs" wrap = "wrap" >
250+ { toolNames . sort ( ) . map ( tool => {
251+ const usageCount = usedTools . get ( tool ) ?? 0 ;
252+ return (
253+ < Tooltip
254+ key = { tool }
255+ disabled = { toolSpansQuery . isPending }
256+ title = {
257+ usageCount === 0
258+ ? t ( 'Not used by agent' )
259+ : tn ( 'Used %s time' , 'Used %s times' , usageCount )
260+ }
261+ >
262+ < Tag key = { tool } type = { usedTools . has ( tool ) ? 'info' : 'default' } >
263+ { tool }
264+ </ Tag >
265+ </ Tooltip >
266+ ) ;
267+ } ) }
268+ </ Flex >
269+ ) ;
270+ }
271+
199272function HighlightedTokenAttributes ( {
200273 inputTokens,
201274 cachedTokens,
0 commit comments