Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(() =>
Expand Down Expand Up @@ -60,9 +60,15 @@ export function NodeDetails(props: TraceTreeNodeDetailsProps<BaseNode>) {
const {node} = props;

if (node instanceof EapSpanNode) {
if (getIsAiNode(node)) {
const operationType = getGenAiOpType(node);
if (operationType) {
return (
<LazyLoad LazyComponent={LazyAiSpanDetails} node={node} traceId={props.traceId} />
<LazyLoad
LazyComponent={LazyAiSpanDetails}
node={node}
operationType={operationType}
traceId={props.traceId}
/>
);
}
return <LazyLoad LazyComponent={LazyEAPSpanNodeDetails} {...props} node={node} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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',
Expand All @@ -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: [
Expand Down Expand Up @@ -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(
<TraceTreeNodeDetails
node={node}
organization={organization}
onTabScrollToNode={onTabScrollToNode}
traceId="trace-id"
/>,
{organization}
);
expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument();
expect(trackViewed).toHaveBeenCalledTimes(1);
expect(trackViewed).toHaveBeenCalledWith(organization, 'tool');

rerender(
<TraceTreeNodeDetails
node={node}
organization={organization}
onTabScrollToNode={onTabScrollToNode}
traceId="trace-id"
/>
);
expect(trackViewed).toHaveBeenCalledTimes(1);

rerender(
<TraceTreeNodeDetails
node={toolNode('other-tool-span')}
organization={organization}
onTabScrollToNode={onTabScrollToNode}
traceId="trace-id"
/>
);
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(<AiSpanDetails node={toolNode()} traceId="trace-id" />, {organization});
expect(await screen.findByText('300% larger than avg 1 kB')).toBeInTheDocument();
expect(trackViewed).not.toHaveBeenCalled();
expect(request).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
Expand All @@ -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();
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 <AiSpanDetails node={node} traceId={traceId} />;
}

export function AiSpanDetails({node, traceId}: AiSpanDetailsProps) {
const location = useLocation();
const [activeTab, setActiveTab] = useState<DetailTab>('input');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ jest.mock('sentry/views/performance/traceDetails/traceDrawer/details/span', () =
jest.mock(
'sentry/views/performance/traceDetails/traceDrawer/details/span/aiSpanDetails',
() => ({
AiSpanDetails: () => <div>ai span details</div>,
AiSpanNodeDetails: ({operationType}: {operationType: string}) => (
<div>ai span details for {operationType}</div>
),
})
);
jest.mock('sentry/views/performance/traceDetails/traceDrawer/details/autogroup', () => ({
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading