|
@@ -9,6 +9,8 @@ import Link from 'sentry/components/links/link';
|
|
|
import type {CursorHandler} from 'sentry/components/pagination';
|
|
|
import Pagination from 'sentry/components/pagination';
|
|
|
import SearchBar from 'sentry/components/searchBar';
|
|
|
+import {Tooltip} from 'sentry/components/tooltip';
|
|
|
+import {IconInfo} from 'sentry/icons';
|
|
|
import {t} from 'sentry/locale';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
import type {Organization} from 'sentry/types/organization';
|
|
@@ -37,10 +39,15 @@ type Row = Pick<
|
|
|
| 'avg(span.duration)'
|
|
|
| 'sum(span.duration)'
|
|
|
| 'ai_total_tokens_used()'
|
|
|
+ | 'ai_total_tokens_used(c:spans/ai.total_cost@none)'
|
|
|
>;
|
|
|
|
|
|
type Column = GridColumnHeader<
|
|
|
- 'span.description' | 'spm()' | 'avg(span.duration)' | 'ai_total_tokens_used()'
|
|
|
+ | 'span.description'
|
|
|
+ | 'spm()'
|
|
|
+ | 'avg(span.duration)'
|
|
|
+ | 'ai_total_tokens_used()'
|
|
|
+ | 'ai_total_tokens_used(c:spans/ai.total_cost@none)'
|
|
|
>;
|
|
|
|
|
|
const COLUMN_ORDER: Column[] = [
|
|
@@ -54,6 +61,11 @@ const COLUMN_ORDER: Column[] = [
|
|
|
name: t('Total tokens used'),
|
|
|
width: 180,
|
|
|
},
|
|
|
+ {
|
|
|
+ key: 'ai_total_tokens_used(c:spans/ai.total_cost@none)',
|
|
|
+ name: t('Total cost'),
|
|
|
+ width: 180,
|
|
|
+ },
|
|
|
{
|
|
|
key: `avg(span.duration)`,
|
|
|
name: t('Pipeline Duration'),
|
|
@@ -87,6 +99,7 @@ export function PipelinesTable() {
|
|
|
if (!sort) {
|
|
|
sort = {field: 'spm()', kind: 'desc'};
|
|
|
}
|
|
|
+
|
|
|
const {data, isLoading, meta, pageLinks, error} = useSpanMetrics({
|
|
|
search: MutableSearch.fromQueryObject({
|
|
|
'span.category': 'ai.pipeline',
|
|
@@ -99,7 +112,6 @@ export function PipelinesTable() {
|
|
|
'spm()',
|
|
|
'avg(span.duration)',
|
|
|
'sum(span.duration)',
|
|
|
- 'ai_total_tokens_used()', // this is zero initially and overwritten below.
|
|
|
],
|
|
|
sorts: [sort],
|
|
|
limit: 25,
|
|
@@ -109,24 +121,37 @@ export function PipelinesTable() {
|
|
|
|
|
|
const {
|
|
|
data: tokensUsedData,
|
|
|
- isLoading: tokensUsedLoading,
|
|
|
error: tokensUsedError,
|
|
|
+ isLoading: tokensUsedLoading,
|
|
|
} = useSpanMetrics({
|
|
|
- search: MutableSearch.fromQueryObject({
|
|
|
- 'span.ai.pipeline.group': (data as Row[])?.map(x => x['span.group']).join(','),
|
|
|
- 'span.category': 'ai',
|
|
|
- }),
|
|
|
- fields: ['span.ai.pipeline.group', 'ai_total_tokens_used()'],
|
|
|
+ search: new MutableSearch(
|
|
|
+ `span.category:ai span.ai.pipeline.group:[${(data as Row[])?.map(x => x['span.group']).join(',')}]`
|
|
|
+ ),
|
|
|
+ fields: [
|
|
|
+ 'span.ai.pipeline.group',
|
|
|
+ 'ai_total_tokens_used()',
|
|
|
+ 'ai_total_tokens_used(c:spans/ai.total_cost@none)',
|
|
|
+ ],
|
|
|
});
|
|
|
- if (!tokensUsedLoading) {
|
|
|
- for (const tokenUsedRow of tokensUsedData) {
|
|
|
- const groupId = tokenUsedRow['span.ai.pipeline.group'];
|
|
|
- const tokensUsed = tokenUsedRow['ai_total_tokens_used()'];
|
|
|
- data
|
|
|
- .filter(x => x['span.group'] === groupId)
|
|
|
- .forEach(x => (x['ai_total_tokens_used()'] = tokensUsed));
|
|
|
+
|
|
|
+ const rows: Row[] = (data as Row[]).map(baseRow => {
|
|
|
+ const row: Row = {
|
|
|
+ ...baseRow,
|
|
|
+ 'ai_total_tokens_used()': 0,
|
|
|
+ 'ai_total_tokens_used(c:spans/ai.total_cost@none)': 0,
|
|
|
+ };
|
|
|
+ if (!tokensUsedLoading) {
|
|
|
+ const tokenUsedDataPoint = tokensUsedData.find(
|
|
|
+ tokenRow => tokenRow['span.ai.pipeline.group'] === row['span.group']
|
|
|
+ );
|
|
|
+ if (tokenUsedDataPoint) {
|
|
|
+ row['ai_total_tokens_used()'] = tokenUsedDataPoint['ai_total_tokens_used()'];
|
|
|
+ row['ai_total_tokens_used(c:spans/ai.total_cost@none)'] =
|
|
|
+ tokenUsedDataPoint['ai_total_tokens_used(c:spans/ai.total_cost@none)'];
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
+ return row;
|
|
|
+ });
|
|
|
|
|
|
const handleCursor: CursorHandler = (newCursor, pathname, query) => {
|
|
|
browserHistory.push({
|
|
@@ -149,7 +174,7 @@ export function PipelinesTable() {
|
|
|
return (
|
|
|
<VisuallyCompleteWithData
|
|
|
id="PipelinesTable"
|
|
|
- hasData={data.length > 0}
|
|
|
+ hasData={rows.length > 0}
|
|
|
isLoading={isLoading}
|
|
|
>
|
|
|
<Container>
|
|
@@ -161,7 +186,7 @@ export function PipelinesTable() {
|
|
|
<GridEditable
|
|
|
isLoading={isLoading}
|
|
|
error={error ?? tokensUsedError}
|
|
|
- data={data}
|
|
|
+ data={rows}
|
|
|
columnOrder={COLUMN_ORDER}
|
|
|
columnSortBy={[
|
|
|
{
|
|
@@ -212,6 +237,26 @@ function renderBodyCell(
|
|
|
</Link>
|
|
|
);
|
|
|
}
|
|
|
+ if (column.key === 'ai_total_tokens_used(c:spans/ai.total_cost@none)') {
|
|
|
+ const cost = row['ai_total_tokens_used(c:spans/ai.total_cost@none)'];
|
|
|
+ if (cost) {
|
|
|
+ if (cost < 0.01) {
|
|
|
+ return <span>US {cost * 100}¢</span>;
|
|
|
+ }
|
|
|
+ return <span>US${cost}</span>;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <span>
|
|
|
+ Unknown{' '}
|
|
|
+ <Tooltip
|
|
|
+ title="Cost can only be calculated for certain OpenAI and Anthropic models, other providers aren't yet supported."
|
|
|
+ isHoverable
|
|
|
+ >
|
|
|
+ <IconInfo size="xs" />
|
|
|
+ </Tooltip>
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
if (!meta || !meta?.fields) {
|
|
|
return row[column.key];
|