Skip to content

Commit a25295e

Browse files
ArthurKnausisabellaenriquez
authored andcommitted
feat(ai-generations): Add generations table (#102389)
<img width="1205" height="305" alt="Screenshot 2025-10-30 at 12 47 31" src="https://github.com/user-attachments/assets/3b601055-3848-44e8-b669-a5f7458c144f" /> - closes [TET-1286: AI Generations Page - Table](https://linear.app/getsentry/issue/TET-1286/ai-generations-page-table) - closes [TET-1287: AI Generations Page - Input/Output Columns](https://linear.app/getsentry/issue/TET-1287/ai-generations-page-inputoutput-columns)
1 parent 648061d commit a25295e

File tree

5 files changed

+232
-23
lines changed

5 files changed

+232
-23
lines changed

static/app/views/insights/agents/components/drawer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function useTraceViewDrawer({onClose}: UseTraceViewDrawerProps = {}) {
110110
const {openDrawer, isDrawerOpen, drawerUrlState, closeDrawer} = useUrlTraceDrawer();
111111

112112
const openTraceViewDrawer = useCallback(
113-
(traceSlug: string) => {
113+
(traceSlug: string, spanId?: string) => {
114114
trackAnalytics('agent-monitoring.drawer.open', {
115115
organization,
116116
});
@@ -124,6 +124,7 @@ export function useTraceViewDrawer({onClose}: UseTraceViewDrawerProps = {}) {
124124
drawerWidth: `${DRAWER_WIDTH}px`,
125125
resizable: true,
126126
traceSlug,
127+
spanId,
127128
drawerKey: 'abbreviated-trace-view-drawer',
128129
}
129130
);

static/app/views/insights/agents/hooks/useUrlTraceDrawer.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export function useUrlTraceDrawer() {
1717
parseAsString.withOptions({history: 'replace'})
1818
);
1919

20+
const [_, setSelectedSpan] = useQueryState(
21+
DrawerUrlParams.SELECTED_SPAN,
22+
parseAsString.withOptions({history: 'replace'})
23+
);
24+
2025
const removeQueryParams = useCallback(() => {
2126
setSelectedTrace(null);
2227
}, [setSelectedTrace]);
@@ -29,13 +34,25 @@ export function useUrlTraceDrawer() {
2934
const openDrawer = useCallback(
3035
(
3136
renderer: Parameters<typeof baseOpenDrawer>[0],
32-
options?: Parameters<typeof baseOpenDrawer>[1] & {traceSlug?: string}
37+
options?: Parameters<typeof baseOpenDrawer>[1] & {
38+
spanId?: string;
39+
traceSlug?: string;
40+
}
3341
) => {
34-
const {traceSlug: optionsTraceSlug, onClose, ariaLabel, ...rest} = options || {};
42+
const {
43+
traceSlug: optionsTraceSlug,
44+
spanId: optionsSpanId,
45+
onClose,
46+
ariaLabel,
47+
...rest
48+
} = options || {};
3549

3650
if (optionsTraceSlug) {
3751
setSelectedTrace(optionsTraceSlug);
3852
}
53+
if (optionsSpanId) {
54+
setSelectedSpan(optionsSpanId);
55+
}
3956

4057
return baseOpenDrawer(renderer, {
4158
...rest,
@@ -49,7 +66,7 @@ export function useUrlTraceDrawer() {
4966
},
5067
});
5168
},
52-
[baseOpenDrawer, setSelectedTrace, removeQueryParams]
69+
[baseOpenDrawer, setSelectedTrace, setSelectedSpan, removeQueryParams]
5370
);
5471

5572
return {
Lines changed: 203 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,208 @@
1-
import {PanelTable} from 'sentry/components/panels/panelTable';
1+
import {useCallback} from 'react';
2+
3+
import {Button} from '@sentry/scraps/button';
4+
import {Container, Grid} from '@sentry/scraps/layout';
5+
import {Text} from '@sentry/scraps/text';
6+
import {Tooltip} from '@sentry/scraps/tooltip';
7+
8+
import {
9+
COL_WIDTH_UNDEFINED,
10+
type GridColumnOrder,
11+
} from 'sentry/components/tables/gridEditable';
12+
import TimeSince from 'sentry/components/timeSince';
13+
import {getShortEventId} from 'sentry/utils/events';
14+
import {useTraceViewDrawer} from 'sentry/views/insights/agents/components/drawer';
15+
import {HeadSortCell} from 'sentry/views/insights/agents/components/headSortCell';
16+
import {ModelName} from 'sentry/views/insights/agents/components/modelName';
17+
import {useCombinedQuery} from 'sentry/views/insights/agents/hooks/useCombinedQuery';
18+
import {useTableCursor} from 'sentry/views/insights/agents/hooks/useTableCursor';
19+
import {getAIGenerationsFilter} from 'sentry/views/insights/agents/utils/query';
20+
import {Referrer} from 'sentry/views/insights/aiGenerations/views/utils/referrer';
21+
import {TextAlignRight} from 'sentry/views/insights/common/components/textAlign';
22+
import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';
23+
import {PlatformInsightsTable} from 'sentry/views/insights/pages/platform/shared/table';
24+
import {SpanFields} from 'sentry/views/insights/types';
25+
26+
const INITIAL_COLUMN_ORDER = [
27+
{key: SpanFields.SPAN_ID, name: 'Span ID', width: 100},
28+
{
29+
key: SpanFields.GEN_AI_REQUEST_MESSAGES,
30+
name: 'Input / Output',
31+
width: COL_WIDTH_UNDEFINED,
32+
},
33+
{
34+
key: SpanFields.GEN_AI_REQUEST_MODEL,
35+
name: 'Model',
36+
width: 200,
37+
},
38+
{key: SpanFields.TIMESTAMP, name: 'Timestamp'},
39+
] as const;
40+
41+
function getLastUserMessage(messages?: string) {
42+
if (!messages) {
43+
return null;
44+
}
45+
try {
46+
const messagesArray = JSON.parse(messages);
47+
const lastUserMessage = messagesArray.findLast(
48+
(message: any) => message.role === 'user'
49+
)?.content;
50+
return typeof lastUserMessage === 'string'
51+
? lastUserMessage
52+
: lastUserMessage[0].text.toString();
53+
} catch (error) {
54+
return messages;
55+
}
56+
}
257

358
export function GenerationsTable() {
59+
const {openTraceViewDrawer} = useTraceViewDrawer({});
60+
const query = useCombinedQuery(
61+
// Only show generation spans that result in a response to the user's message
62+
`${getAIGenerationsFilter()} AND !span.op:gen_ai.embeddings`
63+
);
64+
65+
const {cursor} = useTableCursor();
66+
67+
const {data, isLoading, error, pageLinks, isPlaceholderData} = useSpans(
68+
{
69+
search: query,
70+
fields: [
71+
SpanFields.TRACE,
72+
SpanFields.SPAN_ID,
73+
SpanFields.SPAN_STATUS,
74+
SpanFields.SPAN_DESCRIPTION,
75+
SpanFields.GEN_AI_REQUEST_MESSAGES,
76+
SpanFields.GEN_AI_RESPONSE_TEXT,
77+
SpanFields.GEN_AI_RESPONSE_OBJECT,
78+
SpanFields.GEN_AI_REQUEST_MODEL,
79+
SpanFields.TIMESTAMP,
80+
],
81+
sorts: [{field: SpanFields.TIMESTAMP, kind: 'desc'}],
82+
cursor,
83+
keepPreviousData: true,
84+
limit: 20,
85+
},
86+
Referrer.GENERATIONS_TABLE
87+
);
88+
89+
type TableData = (typeof data)[number];
90+
91+
const renderBodyCell = useCallback(
92+
(column: GridColumnOrder<keyof TableData>, dataRow: TableData) => {
93+
if (column.key === SpanFields.SPAN_ID) {
94+
return (
95+
<div>
96+
<Button
97+
priority="link"
98+
onClick={() => {
99+
openTraceViewDrawer(dataRow.trace, dataRow.span_id);
100+
}}
101+
>
102+
{getShortEventId(dataRow.span_id)}
103+
</Button>
104+
</div>
105+
);
106+
}
107+
108+
if (column.key === SpanFields.GEN_AI_REQUEST_MESSAGES) {
109+
const noValueFallback = <Text variant="muted"></Text>;
110+
const statusValue = dataRow[SpanFields.SPAN_STATUS];
111+
const isError = statusValue && statusValue !== 'ok' && statusValue !== 'unknown';
112+
113+
const userMessage = getLastUserMessage(
114+
dataRow[SpanFields.GEN_AI_REQUEST_MESSAGES]
115+
);
116+
const outputValue =
117+
dataRow[SpanFields.GEN_AI_RESPONSE_TEXT] ||
118+
dataRow[SpanFields.GEN_AI_RESPONSE_OBJECT];
119+
return (
120+
<div>
121+
<Grid
122+
areas={`
123+
"inputLabel inputValue"
124+
"outputLabel outputValue"
125+
`}
126+
columns="min-content 1fr"
127+
gap="2xs md"
128+
>
129+
<Container area="inputLabel">
130+
<Text variant="muted">Input</Text>
131+
</Container>
132+
<Container area="inputValue" minWidth="0px">
133+
<Tooltip
134+
title={userMessage}
135+
disabled={!userMessage}
136+
showOnlyOnOverflow
137+
maxWidth={800}
138+
isHoverable
139+
>
140+
<Text ellipsis>{userMessage || noValueFallback}</Text>
141+
</Tooltip>
142+
</Container>
143+
<Container area="outputLabel">
144+
<Text variant="muted">Output</Text>
145+
</Container>
146+
<Container area="outputValue" minWidth="0px">
147+
<Tooltip
148+
title={outputValue}
149+
disabled={!outputValue}
150+
showOnlyOnOverflow
151+
maxWidth={800}
152+
isHoverable
153+
>
154+
{isError ? (
155+
<Text variant="danger">{statusValue}</Text>
156+
) : (
157+
<Text ellipsis>{outputValue || noValueFallback}</Text>
158+
)}
159+
</Tooltip>
160+
</Container>
161+
</Grid>
162+
</div>
163+
);
164+
}
165+
166+
if (column.key === SpanFields.GEN_AI_REQUEST_MODEL) {
167+
return <ModelName modelId={dataRow[column.key] || '(no value)'} />;
168+
}
169+
if (column.key === SpanFields.TIMESTAMP) {
170+
return (
171+
<TextAlignRight>
172+
<TimeSince unitStyle="extraShort" date={new Date(dataRow.timestamp)} />
173+
</TextAlignRight>
174+
);
175+
}
176+
return <div>{dataRow[column.key]}</div>;
177+
},
178+
[openTraceViewDrawer]
179+
);
180+
181+
const renderHeadCell = useCallback((column: GridColumnOrder<keyof TableData>) => {
182+
return (
183+
<HeadSortCell
184+
align={column.key === SpanFields.TIMESTAMP ? 'right' : 'left'}
185+
sortKey={column.key}
186+
forceCellGrow={column.key === SpanFields.GEN_AI_REQUEST_MESSAGES}
187+
>
188+
{column.name}
189+
</HeadSortCell>
190+
);
191+
}, []);
192+
4193
return (
5-
<PanelTable headers={['id', 'input/output', 'model', 'cost', 'timestamp']}>
6-
<div>1244</div>
7-
<div>
8-
<div>User Input</div>
9-
<div>Some AI response</div>
10-
</div>
11-
<div>gpt-4o</div>
12-
<div>1244$</div>
13-
<div>2025-01-01 12:00:00</div>
14-
<div>1245</div>
15-
<div>
16-
<div>Another user query</div>
17-
<div>Short AI answer</div>
18-
</div>
19-
<div>gpt-4o</div>
20-
<div>8$</div>
21-
<div>2025-01-01 8:00:00</div>
22-
</PanelTable>
194+
<PlatformInsightsTable
195+
data={data}
196+
stickyHeader
197+
isLoading={isLoading}
198+
error={error}
199+
initialColumnOrder={INITIAL_COLUMN_ORDER as any}
200+
pageLinks={pageLinks}
201+
grid={{
202+
renderBodyCell,
203+
renderHeadCell,
204+
}}
205+
isPlaceholderData={isPlaceholderData}
206+
/>
23207
);
24208
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export enum Referrer {
22
GENERATIONS_CHART = 'api.insights.ai-generations.generations-chart',
3+
GENERATIONS_TABLE = 'api.insights.ai-generations.generations-table',
34
}

static/app/views/insights/types.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ export enum SpanFields {
9595
GEN_AI_AGENT_NAME = 'gen_ai.agent.name',
9696
GEN_AI_FUNCTION_ID = 'gen_ai.function_id',
9797
GEN_AI_REQUEST_MODEL = 'gen_ai.request.model',
98+
GEN_AI_REQUEST_MESSAGES = 'gen_ai.request.messages',
99+
GEN_AI_RESPONSE_TEXT = 'gen_ai.response.text',
100+
GEN_AI_RESPONSE_OBJECT = 'gen_ai.response.object',
98101
GEN_AI_RESPONSE_MODEL = 'gen_ai.response.model',
99102
GEN_AI_TOOL_NAME = 'gen_ai.tool.name',
100103
GEN_AI_COST_INPUT_TOKENS = 'gen_ai.cost.input_tokens',
@@ -251,6 +254,9 @@ export type SpanStringFields =
251254
| SpanFields.STATUS_MESSAGE
252255
| SpanFields.GEN_AI_AGENT_NAME
253256
| SpanFields.GEN_AI_REQUEST_MODEL
257+
| SpanFields.GEN_AI_REQUEST_MESSAGES
258+
| SpanFields.GEN_AI_RESPONSE_TEXT
259+
| SpanFields.GEN_AI_RESPONSE_OBJECT
254260
| SpanFields.GEN_AI_RESPONSE_MODEL
255261
| SpanFields.GEN_AI_TOOL_NAME
256262
| SpanFields.MCP_CLIENT_NAME

0 commit comments

Comments
 (0)