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 b25442a51bb2..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 @@ -5,6 +5,8 @@ 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'; @@ -14,11 +16,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 +31,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,11 +69,55 @@ describe('Result Size highlighted attribute', () => { }); }); - it('compares size in trace details using a project-scoped tool average', async () => { + 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 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( + + ); + expect(trackViewed).toHaveBeenCalledTimes(1); + + rerender( + + ); + expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument(); + expect(trackViewed).toHaveBeenCalledTimes(2); + }); + + 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({ @@ -87,6 +133,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 +149,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..21c4e90d41a2 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,13 @@ -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 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'; @@ -14,6 +16,19 @@ 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 [activeTab, setActiveTab] = useState('input'); 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 () => {