From 6e95e9cd2249e0cb209fbabc0679f2f232b1da9c Mon Sep 17 00:00:00 2001 From: Ognjen Bostjancic Date: Tue, 3 Nov 2026 10:30:19 +0100 Subject: [PATCH 1/3] fix(agents): Track dedicated AI span drawer views --- .../details/span/aiSpanDetails.spec.tsx | 34 ++++++++++++++++--- .../details/span/aiSpanDetails.tsx | 12 ++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx index b25442a51bb2..b067f12b0cde 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx @@ -5,6 +5,7 @@ import {act, render, screen} from 'sentry-test/reactTestingLibrary'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {ConversationSpanDetail} from 'sentry/views/explore/conversations/components/conversationSpanDetail'; +import {traceAnalytics} from 'sentry/views/performance/traceDetails/traceAnalytics'; import {EapSpanNode} from 'sentry/views/performance/traceDetails/traceModels/traceTreeNode/eapSpanNode'; import {makeEAPSpan} from 'sentry/views/performance/traceDetails/traceModels/traceTreeTestUtils'; @@ -14,11 +15,11 @@ const organization = OrganizationFixture(); const project = ProjectFixture({id: '1'}); const sizeField = 'avg(gen_ai.tool.call.result.size)'; -function toolNode() { +function toolNode(spanId = 'tool-span') { return new EapSpanNode( null, makeEAPSpan({ - event_id: 'tool-span', + event_id: spanId, project_id: 1, project_slug: project.slug, op: 'gen_ai.execute_tool', @@ -29,11 +30,11 @@ function toolNode() { ); } -function mockAttributes(withSize = true) { +function mockAttributes(withSize = true, spanId = 'tool-span') { MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/trace-items/tool-span/`, + url: `/projects/${organization.slug}/${project.slug}/trace-items/${spanId}/`, body: { - itemId: 'tool-span', + itemId: spanId, timestamp: '2020-01-01T00:00:00Z', meta: {}, attributes: [ @@ -67,6 +68,27 @@ describe('Result Size highlighted attribute', () => { }); }); + it('tracks each selected AI span, but not ordinary rerenders', async () => { + const trackViewed = jest.spyOn(traceAnalytics, 'trackGenAISpanDetailsViewed'); + mockAttributes(); + mockAttributes(true, 'other-tool-span'); + mockAverage(); + const node = toolNode(); + const {rerender} = render(, { + organization, + }); + expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); + expect(trackViewed).toHaveBeenCalledTimes(1); + expect(trackViewed).toHaveBeenCalledWith(organization, 'tool'); + + rerender(); + expect(trackViewed).toHaveBeenCalledTimes(1); + + rerender(); + expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); + expect(trackViewed).toHaveBeenCalledTimes(2); + }); + it('compares size in trace details using a project-scoped tool average', async () => { mockAttributes(); const request = mockAverage(); @@ -87,6 +109,7 @@ describe('Result Size highlighted attribute', () => { }); it('also compares size in standalone conversation details without a parent baseline', async () => { + const trackViewed = jest.spyOn(traceAnalytics, 'trackGenAISpanDetailsViewed'); mockAttributes(); mockAverage(); const node = toolNode(); @@ -102,6 +125,7 @@ describe('Result Size highlighted attribute', () => { ); expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); expect(screen.getByText('Result Size')).toBeInTheDocument(); + expect(trackViewed).not.toHaveBeenCalled(); }); it('retains the calculated size without querying a baseline for legacy spans', async () => { diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx index e2c915cc4194..a785566a1475 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx @@ -1,11 +1,14 @@ -import {useState} from 'react'; +import {useEffect, useState} from 'react'; import {useLocation} from 'sentry/utils/useLocation'; +import {useOrganization} from 'sentry/utils/useOrganization'; import { ConversationSpanDetail, type DetailTab, } from 'sentry/views/explore/conversations/components/conversationSpanDetail'; +import {getGenAiOpType} from 'sentry/views/insights/pages/agents/utils/aiTraceNodes'; import type {AITraceSpanNode} from 'sentry/views/insights/pages/agents/utils/types'; +import {traceAnalytics} from 'sentry/views/performance/traceDetails/traceAnalytics'; import {useAvgSpanDuration} from 'sentry/views/performance/traceDetails/traceDrawer/details/span'; import {isEAPSpanNode} from 'sentry/views/performance/traceDetails/traceGuards'; @@ -16,7 +19,14 @@ interface AiSpanDetailsProps { export function AiSpanDetails({node, traceId}: AiSpanDetailsProps) { const location = useLocation(); + const organization = useOrganization(); const [activeTab, setActiveTab] = useState('input'); + useEffect(() => { + const genAiOperationType = getGenAiOpType(node); + if (typeof genAiOperationType === 'string' && genAiOperationType) { + traceAnalytics.trackGenAISpanDetailsViewed(organization, genAiOperationType); + } + }, [node, organization]); const eapSpan = isEAPSpanNode(node) ? node.value : undefined; const avgSpanDuration = useAvgSpanDuration(eapSpan, location); From 01dac3b74e9dabd967ddf04a1ff1ba1eb1cef41e Mon Sep 17 00:00:00 2001 From: Ognjen Bostjancic Date: Tue, 3 Nov 2026 10:41:11 +0100 Subject: [PATCH 2/3] ref(agents): Remove redundant AI operation type guard --- .../traceDetails/traceDrawer/details/span/aiSpanDetails.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx index a785566a1475..2c5e6c35783c 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx @@ -22,10 +22,7 @@ export function AiSpanDetails({node, traceId}: AiSpanDetailsProps) { const organization = useOrganization(); const [activeTab, setActiveTab] = useState('input'); useEffect(() => { - const genAiOperationType = getGenAiOpType(node); - if (typeof genAiOperationType === 'string' && genAiOperationType) { - traceAnalytics.trackGenAISpanDetailsViewed(organization, genAiOperationType); - } + traceAnalytics.trackGenAISpanDetailsViewed(organization, getGenAiOpType(node)!); }, [node, organization]); const eapSpan = isEAPSpanNode(node) ? node.value : undefined; From 0972fde0e822bf54a03e596cac258d8382deca56 Mon Sep 17 00:00:00 2001 From: Ognjen Bostjancic Date: Tue, 3 Nov 2026 13:06:57 +0100 Subject: [PATCH 3/3] fix(agents): Narrow operation type at the panel routing boundary --- .../traceDrawer/details/nodeDetails.tsx | 14 +++++--- .../details/span/aiSpanDetails.spec.tsx | 36 +++++++++++++++---- .../details/span/aiSpanDetails.tsx | 18 +++++++--- .../tabs/traceTreeNodeDetails.spec.tsx | 6 ++-- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/nodeDetails.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/nodeDetails.tsx index 8166e9cc9893..2a77d2c93f93 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/nodeDetails.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/nodeDetails.tsx @@ -1,7 +1,7 @@ import {lazy} from 'react'; import {LazyLoad} from 'sentry/components/lazyLoad'; -import {getIsAiNode} from 'sentry/views/insights/pages/agents/utils/aiTraceNodes'; +import {getGenAiOpType} from 'sentry/views/insights/pages/agents/utils/aiTraceNodes'; import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails'; import type {BaseNode} from 'sentry/views/performance/traceDetails/traceModels/traceTreeNode/baseNode'; import {EapSpanNode} from 'sentry/views/performance/traceDetails/traceModels/traceTreeNode/eapSpanNode'; @@ -22,7 +22,7 @@ const LazyEAPSpanNodeDetails = lazy(() => ); const LazyAiSpanDetails = lazy(() => import('sentry/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails').then( - module => ({default: module.AiSpanDetails}) + module => ({default: module.AiSpanNodeDetails}) ) ); const LazyErrorNodeDetails = lazy(() => @@ -60,9 +60,15 @@ export function NodeDetails(props: TraceTreeNodeDetailsProps) { const {node} = props; if (node instanceof EapSpanNode) { - if (getIsAiNode(node)) { + const operationType = getGenAiOpType(node); + if (operationType) { return ( - + ); } return ; diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx index b067f12b0cde..c75b676256eb 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.spec.tsx @@ -6,6 +6,7 @@ import {act, render, screen} from 'sentry-test/reactTestingLibrary'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {ConversationSpanDetail} from 'sentry/views/explore/conversations/components/conversationSpanDetail'; import {traceAnalytics} from 'sentry/views/performance/traceDetails/traceAnalytics'; +import {TraceTreeNodeDetails} from 'sentry/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails'; import {EapSpanNode} from 'sentry/views/performance/traceDetails/traceModels/traceTreeNode/eapSpanNode'; import {makeEAPSpan} from 'sentry/views/performance/traceDetails/traceModels/traceTreeTestUtils'; @@ -74,26 +75,49 @@ describe('Result Size highlighted attribute', () => { mockAttributes(true, 'other-tool-span'); mockAverage(); const node = toolNode(); - const {rerender} = render(, { - organization, - }); + const onTabScrollToNode = jest.fn(); + const {rerender} = render( + , + {organization} + ); expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); expect(trackViewed).toHaveBeenCalledTimes(1); expect(trackViewed).toHaveBeenCalledWith(organization, 'tool'); - rerender(); + rerender( + + ); expect(trackViewed).toHaveBeenCalledTimes(1); - rerender(); + rerender( + + ); expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); expect(trackViewed).toHaveBeenCalledTimes(2); }); - it('compares size in trace details using a project-scoped tool average', async () => { + it('compares size in the shared AI panel without tracking a waterfall view', async () => { + const trackViewed = jest.spyOn(traceAnalytics, 'trackGenAISpanDetailsViewed'); mockAttributes(); const request = mockAverage(); render(, {organization}); expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); + expect(trackViewed).not.toHaveBeenCalled(); expect(request).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx index 2c5e6c35783c..21c4e90d41a2 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails.tsx @@ -6,7 +6,6 @@ import { ConversationSpanDetail, type DetailTab, } from 'sentry/views/explore/conversations/components/conversationSpanDetail'; -import {getGenAiOpType} from 'sentry/views/insights/pages/agents/utils/aiTraceNodes'; import type {AITraceSpanNode} from 'sentry/views/insights/pages/agents/utils/types'; import {traceAnalytics} from 'sentry/views/performance/traceDetails/traceAnalytics'; import {useAvgSpanDuration} from 'sentry/views/performance/traceDetails/traceDrawer/details/span'; @@ -17,13 +16,22 @@ interface AiSpanDetailsProps { traceId: string; } +export function AiSpanNodeDetails({ + node, + operationType, + traceId, +}: AiSpanDetailsProps & {operationType: string}) { + const organization = useOrganization(); + useEffect(() => { + traceAnalytics.trackGenAISpanDetailsViewed(organization, operationType); + }, [operationType, organization]); + + return ; +} + export function AiSpanDetails({node, traceId}: AiSpanDetailsProps) { const location = useLocation(); - const organization = useOrganization(); const [activeTab, setActiveTab] = useState('input'); - useEffect(() => { - traceAnalytics.trackGenAISpanDetailsViewed(organization, getGenAiOpType(node)!); - }, [node, organization]); const eapSpan = isEAPSpanNode(node) ? node.value : undefined; const avgSpanDuration = useAvgSpanDuration(eapSpan, location); diff --git a/static/app/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails.spec.tsx b/static/app/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails.spec.tsx index 907e4f4fb5b6..0fd2fc9c28e5 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails.spec.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/tabs/traceTreeNodeDetails.spec.tsx @@ -35,7 +35,9 @@ jest.mock('sentry/views/performance/traceDetails/traceDrawer/details/span', () = jest.mock( 'sentry/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails', () => ({ - AiSpanDetails: () =>
ai span details
, + AiSpanNodeDetails: ({operationType}: {operationType: string}) => ( +
ai span details for {operationType}
+ ), }) ); jest.mock('sentry/views/performance/traceDetails/traceDrawer/details/autogroup', () => ({ @@ -77,7 +79,7 @@ describe('TraceTreeNodeDetails', () => { }); render(renderDetails(new EapSpanNode(null, value, extra))); - expect(await screen.findByText('ai span details')).toBeInTheDocument(); + expect(await screen.findByText('ai span details for ai_client')).toBeInTheDocument(); }); it('renders the autogroup panel for an autogroup node', async () => {