Skip to content

feat(trace-view): Add ai spans tab #93803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
401 changes: 401 additions & 0 deletions static/app/views/insights/agentMonitoring/components/aiSpanList.tsx

Large diffs are not rendered by default.

670 changes: 68 additions & 602 deletions static/app/views/insights/agentMonitoring/components/drawer.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
AI_TOKEN_USAGE_ATTRIBUTE_SUM,
getAITracesFilter,
} from 'sentry/views/insights/agentMonitoring/utils/query';
import {Referrer} from 'sentry/views/insights/agentMonitoring/utils/referrers';
import {TextAlignRight} from 'sentry/views/insights/common/components/textAlign';
import {useEAPSpans} from 'sentry/views/insights/common/queries/useDiscover';
import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/DurationCell';
Expand Down Expand Up @@ -92,9 +93,9 @@ export function TracesTable() {
AI_TOKEN_USAGE_ATTRIBUTE_SUM,
],
limit: tracesRequest.data?.data.length ?? 0,
enabled: tracesRequest.data && tracesRequest.data.data.length > 0,
enabled: Boolean(tracesRequest.data && tracesRequest.data.data.length > 0),
},
'test-traces-table'
Referrer.TRACES_TABLE
);

const spanDataMap = useMemo(() => {
Expand Down
109 changes: 109 additions & 0 deletions static/app/views/insights/agentMonitoring/hooks/useAITrace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {useEffect, useState} from 'react';

import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import {getIsAiNode} from 'sentry/views/insights/agentMonitoring/utils/highlightedSpanAttributes';
import type {AITraceSpanNode} from 'sentry/views/insights/agentMonitoring/utils/types';
import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
import {useTraceMeta} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceMeta';
import {
isEAPSpanNode,
isSpanNode,
isTransactionNode,
isTransactionNodeEquivalent,
} from 'sentry/views/performance/newTraceDetails/traceGuards';
import {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
import type {TraceTreeNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode';
import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
import {useTraceQueryParams} from 'sentry/views/performance/newTraceDetails/useTraceQueryParams';

interface UseAITraceResult {
error: boolean;
isLoading: boolean;
nodes: AITraceSpanNode[];
}

export function useAITrace(traceSlug: string): UseAITraceResult {
const [nodes, setNodes] = useState<AITraceSpanNode[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);

const api = useApi();
const organization = useOrganization();
const queryParams = useTraceQueryParams();

const meta = useTraceMeta([{traceSlug, timestamp: queryParams.timestamp}]);
const trace = useTrace({traceSlug, timestamp: queryParams.timestamp});

useEffect(() => {
if (trace.status !== 'success' || !trace.data || !meta.data) {
setError(trace.status === 'error' || meta.status === 'error');
setIsLoading(trace.status === 'pending' || meta.status === 'pending' || !meta.data);
return;
}

const loadAllSpans = async () => {
setIsLoading(true);
setError(false);
setNodes([]);

try {
const tree = TraceTree.FromTrace(trace.data, {
meta: meta.data,
replay: null,
preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
});

tree.build();

const fetchableTransactions = TraceTree.FindAll(tree.root, node => {
return isTransactionNode(node) && node.canFetch && node.value !== null;
}).filter((node): node is TraceTreeNode<TraceTree.Transaction> =>
isTransactionNode(node)
);

const uniqueTransactions = fetchableTransactions.filter(
(node, index, array) =>
index === array.findIndex(tx => tx.value.event_id === node.value.event_id)
);

const zoomPromises = uniqueTransactions.map(node =>
tree.zoom(node, true, {
api,
organization,
preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
})
);

await Promise.all(zoomPromises);

// Keep only transactions that include AI spans and the AI spans themselves
const flattenedNodes = TraceTree.FindAll(tree.root, node => {
if (!isTransactionNode(node) && !isSpanNode(node) && !isEAPSpanNode(node)) {
return false;
}

if (isTransactionNodeEquivalent(node)) {
return TraceTree.Find(node, child => getIsAiNode(child)) !== null;
}

return getIsAiNode(node);
}) as AITraceSpanNode[];

setNodes(flattenedNodes);
setIsLoading(false);
} catch (err) {
setError(true);
setIsLoading(false);
}
};

loadAllSpans();
}, [trace.status, trace.data, meta.data, meta.status, organization, api]);

return {
nodes,
isLoading,
error,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import type {AITraceSpanNode} from 'sentry/views/insights/agentMonitoring/utils/types';
import {
isEAPSpanNode,
isSpanNode,
isTransactionNode,
} from 'sentry/views/performance/newTraceDetails/traceGuards';
import type {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';

export function useNodeDetailsLink({
node,
traceSlug,
source,
}: {
node: AITraceSpanNode | undefined;
source: TraceViewSources;
traceSlug: string;
}) {
const organization = useOrganization();
const {selection} = usePageFilters();
const location = useLocation();

let spanId: string | undefined;
let targetId: string | undefined;
let timestamp: number | undefined;

if (node) {
if (isEAPSpanNode(node)) {
spanId = node.value.event_id;
targetId = node.value.transaction_id;
timestamp = node.value.start_timestamp;
}
if (isTransactionNode(node)) {
spanId = node.value.event_id;
targetId = node.value.event_id;
timestamp = node.value.start_timestamp;
}
if (isSpanNode(node)) {
spanId = node.value.span_id;
timestamp = node.value.start_timestamp;
// Find parent transaction
let parent = node.parent;
while (parent && !isTransactionNode(parent)) {
parent = parent.parent;
}
if (parent) {
targetId = parent.value.event_id;
spanId = node.value.span_id;
}
}
}

return getTraceDetailsUrl({
source,
organization,
location,
traceSlug,
spanId,
targetId,
timestamp,
dateSelection: normalizeDateTimeParams(selection),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum Referrer {
MODELS_TABLE = 'api.performance.agent-monitoring.models-table',
TOOLS_TABLE = 'api.performance.agent-monitoring.tools-table',
TRACE_DRAWER = 'api.performance.agent-monitoring.trace-drawer',
TRACES_TABLE = 'api.performance.agent-monitoring.traces-table',
}
6 changes: 6 additions & 0 deletions static/app/views/insights/agentMonitoring/utils/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
import type {TraceTreeNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode';

export type AITraceSpanNode = TraceTreeNode<
TraceTree.Transaction | TraceTree.EAPSpan | TraceTree.Span
>;
7 changes: 7 additions & 0 deletions static/app/views/performance/newTraceDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {useLogsPageDataQueryResult} from 'sentry/views/explore/contexts/logs/logsPageData';
import {TraceContextTags} from 'sentry/views/performance/newTraceDetails/traceContextTags';
import TraceAiSpans from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceAiSpans';
import {TraceProfiles} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles';
import {
TraceViewLogsDataProvider,
Expand Down Expand Up @@ -163,6 +164,12 @@ function TraceViewImpl({traceSlug}: {traceSlug: string}) {
{currentTab === TraceLayoutTabKeys.SUMMARY ? (
<TraceSummarySection traceSlug={traceSlug} />
) : null}
{currentTab === TraceLayoutTabKeys.AI_SPANS ? (
<TraceAiSpans
traceSlug={traceSlug}
viewManager={traceWaterfallModels.viewManager}
/>
) : null}
</TraceInnerLayout>
</TraceExternalLayout>
</NoProjectMessage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ function EAPSpanNodeDetails({
location,
traceId,
theme,
hideNodeActions,
}: EAPSpanNodeDetailsProps) {
const {
data: traceItemData,
Expand Down Expand Up @@ -424,6 +425,7 @@ function EAPSpanNodeDetails({
node={node}
organization={organization}
onTabScrollToNode={onTabScrollToNode}
hideNodeActions={hideNodeActions}
/>
<TraceDrawerComponents.BodyContainer>
<ProfilesProvider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {useMemo, useState} from 'react';
import styled from '@emotion/styled';

import EmptyMessage from 'sentry/components/emptyMessage';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import useOrganization from 'sentry/utils/useOrganization';
import {AISpanList} from 'sentry/views/insights/agentMonitoring/components/aiSpanList';
import {useAITrace} from 'sentry/views/insights/agentMonitoring/hooks/useAITrace';
import {TraceTreeNodeDetails} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager';

function TraceAiSpans({
traceSlug,
}: {
traceSlug: string;
viewManager: VirtualizedViewManager;
}) {
const organization = useOrganization();
const {nodes, isLoading, error} = useAITrace(traceSlug);
const [selectedNodeKey, setSelectedNodeKey] = useState<string | null>(null);
const selectedNode = useMemo(() => {
return nodes.find(node => node.metadata.event_id === selectedNodeKey) || nodes[0];
}, [nodes, selectedNodeKey]);

if (isLoading) {
return <LoadingIndicator />;
}

if (nodes.length === 0) {
return <EmptyMessage>{t('No AI spans found')}</EmptyMessage>;
}

if (error) {
return <div>{t('Failed to load trace')}</div>;
}

return (
<Wrapper>
<HeaderCell>{t('Abbreviated Trace')}</HeaderCell>
<HeaderCell>{/* TODO: tabs for spans */}</HeaderCell>
<LeftPanel>
<SpansHeader>{t('AI Spans')}</SpansHeader>
<AISpanList
nodes={nodes}
onSelectNode={node => setSelectedNodeKey(node.metadata.event_id as string)}
selectedNodeKey={selectedNode?.metadata.event_id ?? null}
/>
</LeftPanel>
<RightPanel>
{selectedNode && (
<TraceTreeNodeDetails
node={selectedNode}
manager={null}
onParentClick={() => {}}
onTabScrollToNode={() => {}}
organization={organization}
replay={null}
traceId={traceSlug}
hideNodeActions
/>
)}
</RightPanel>
</Wrapper>
);
}

export default TraceAiSpans;

const Wrapper = styled('div')`
display: grid;
grid-template-columns: minmax(300px, 400px) 1fr;
grid-template-rows: 38px 1fr;
flex: 1 1 100%;
min-height: 0;
background-color: ${p => p.theme.background};
border-radius: ${p => p.theme.borderRadius};
border: 1px solid ${p => p.theme.border};
`;

const SpansHeader = styled('h6')`
font-size: ${p => p.theme.fontSizeExtraLarge};
font-weight: bold;
margin-bottom: ${space(2)};
margin-left: ${space(1)};
`;

const HeaderCell = styled('div')`
padding: 0 ${space(2)};
font-size: ${p => p.theme.fontSizeSmall};
color: ${p => p.theme.subText};
border-bottom: 1px solid ${p => p.theme.border};
display: flex;
align-items: center;
`;

const LeftPanel = styled('div')`
flex: 1;
min-width: 300px;
min-height: 0;
padding: ${space(2)};
border-right: 1px solid ${p => p.theme.border};
overflow-y: auto;
overflow-x: hidden;
max-width: 400px;
`;

const RightPanel = styled('div')`
min-width: 400px;
padding-top: ${space(1)};
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export function isPageloadTransactionNode(
);
}

export function isTransactionNodeEquivalent(
node: TraceTreeNode<TraceTree.NodeValue>
): node is TraceTreeNode<TraceTree.Transaction | TraceTree.EAPSpan> {
return isTransactionNode(node) || isEAPTransaction(node.value);
}

export function isServerRequestHandlerTransactionNode(
node: TraceTreeNode<TraceTree.NodeValue>
): boolean {
Expand Down
Loading
Loading