PipelinesTable.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import type {Location} from 'history';
  2. import GridEditable, {
  3. COL_WIDTH_UNDEFINED,
  4. type GridColumnHeader,
  5. } from 'sentry/components/gridEditable';
  6. import Link from 'sentry/components/links/link';
  7. import type {CursorHandler} from 'sentry/components/pagination';
  8. import Pagination from 'sentry/components/pagination';
  9. import {t} from 'sentry/locale';
  10. import type {Organization} from 'sentry/types/organization';
  11. import {browserHistory} from 'sentry/utils/browserHistory';
  12. import type {EventsMetaType} from 'sentry/utils/discover/eventView';
  13. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  14. import type {Sort} from 'sentry/utils/discover/fields';
  15. import {RATE_UNIT_TITLE, RateUnit} from 'sentry/utils/discover/fields';
  16. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  17. import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
  18. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  22. import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
  23. import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
  24. import type {MetricsResponse} from 'sentry/views/starfish/types';
  25. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  26. import {DataTitles} from 'sentry/views/starfish/views/spans/types';
  27. type Row = Pick<
  28. MetricsResponse,
  29. | 'project.id'
  30. | 'span.description'
  31. | 'span.group'
  32. | 'spm()'
  33. | 'avg(span.duration)'
  34. | 'sum(span.duration)'
  35. | 'ai_total_tokens_used()'
  36. >;
  37. type Column = GridColumnHeader<
  38. 'span.description' | 'spm()' | 'avg(span.duration)' | 'ai_total_tokens_used()'
  39. >;
  40. const COLUMN_ORDER: Column[] = [
  41. {
  42. key: 'span.description',
  43. name: t('AI Pipeline Name'),
  44. width: COL_WIDTH_UNDEFINED,
  45. },
  46. {
  47. key: 'spm()',
  48. name: `${t('Times')} ${RATE_UNIT_TITLE[RateUnit.PER_MINUTE]}`,
  49. width: COL_WIDTH_UNDEFINED,
  50. },
  51. {
  52. key: 'ai_total_tokens_used()',
  53. name: t('Total tokens used'),
  54. width: 180,
  55. },
  56. {
  57. key: `avg(span.duration)`,
  58. name: DataTitles.avg,
  59. width: COL_WIDTH_UNDEFINED,
  60. },
  61. ];
  62. const SORTABLE_FIELDS = ['avg(span.duration)', 'spm()'];
  63. type ValidSort = Sort & {
  64. field: 'spm()' | 'avg(span.duration)';
  65. };
  66. export function isAValidSort(sort: Sort): sort is ValidSort {
  67. return (SORTABLE_FIELDS as unknown as string[]).includes(sort.field);
  68. }
  69. export function PipelinesTable() {
  70. const location = useLocation();
  71. const organization = useOrganization();
  72. const cursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]);
  73. const sortField = decodeScalar(location.query?.[QueryParameterNames.SPANS_SORT]);
  74. let sort = decodeSorts(sortField).filter(isAValidSort)[0];
  75. if (!sort) {
  76. sort = {field: 'spm()', kind: 'desc'};
  77. }
  78. const {data, isLoading, meta, pageLinks, error} = useSpanMetrics({
  79. search: new MutableSearch('span.category:ai.pipeline'),
  80. fields: [
  81. 'project.id',
  82. 'span.group',
  83. 'span.description',
  84. 'spm()',
  85. 'avg(span.duration)',
  86. 'sum(span.duration)',
  87. 'ai_total_tokens_used()', // this is zero initially and overwritten below.
  88. ],
  89. sorts: [sort],
  90. limit: 25,
  91. cursor,
  92. referrer: 'api.ai-pipelines.view',
  93. });
  94. const {
  95. data: tokensUsedData,
  96. isLoading: tokensUsedLoading,
  97. error: tokensUsedError,
  98. } = useSpanMetrics({
  99. search: new MutableSearch(
  100. `span.ai.pipeline.group:[${(data as Row[])?.map(x => x['span.group']).join(',')}] span.category:ai`
  101. ),
  102. fields: ['span.ai.pipeline.group', 'ai_total_tokens_used()'],
  103. });
  104. if (!tokensUsedLoading) {
  105. for (const tokenUsedRow of tokensUsedData) {
  106. const groupId = tokenUsedRow['span.ai.pipeline.group'];
  107. const tokensUsed = tokenUsedRow['ai_total_tokens_used()'];
  108. data
  109. .filter(x => x['span.group'] === groupId)
  110. .forEach(x => (x['ai_total_tokens_used()'] = tokensUsed));
  111. }
  112. }
  113. const handleCursor: CursorHandler = (newCursor, pathname, query) => {
  114. browserHistory.push({
  115. pathname,
  116. query: {...query, [QueryParameterNames.SPANS_CURSOR]: newCursor},
  117. });
  118. };
  119. return (
  120. <VisuallyCompleteWithData
  121. id="PipelinesTable"
  122. hasData={data.length > 0}
  123. isLoading={isLoading}
  124. >
  125. <GridEditable
  126. isLoading={isLoading}
  127. error={error ?? tokensUsedError}
  128. data={data}
  129. columnOrder={COLUMN_ORDER}
  130. columnSortBy={[
  131. {
  132. key: sort.field,
  133. order: sort.kind,
  134. },
  135. ]}
  136. grid={{
  137. renderHeadCell: column =>
  138. renderHeadCell({
  139. column,
  140. sort,
  141. location,
  142. sortParameterName: QueryParameterNames.SPANS_SORT,
  143. }),
  144. renderBodyCell: (column, row) =>
  145. renderBodyCell(column, row, meta, location, organization),
  146. }}
  147. location={location}
  148. />
  149. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  150. </VisuallyCompleteWithData>
  151. );
  152. }
  153. function renderBodyCell(
  154. column: Column,
  155. row: Row,
  156. meta: EventsMetaType | undefined,
  157. location: Location,
  158. organization: Organization
  159. ) {
  160. if (column.key === 'span.description') {
  161. if (!row['span.description']) {
  162. return <span>(unknown)</span>;
  163. }
  164. if (!row['span.group']) {
  165. return <span>{row['span.description']}</span>;
  166. }
  167. return (
  168. <Link
  169. to={normalizeUrl(
  170. `/organizations/${organization.slug}/ai-monitoring/pipeline-type/${row['span.group']}`
  171. )}
  172. >
  173. {row['span.description']}
  174. </Link>
  175. );
  176. }
  177. if (!meta || !meta?.fields) {
  178. return row[column.key];
  179. }
  180. const renderer = getFieldRenderer(column.key, meta.fields, false);
  181. const rendered = renderer(row, {
  182. location,
  183. organization,
  184. unit: meta.units?.[column.key],
  185. });
  186. return rendered;
  187. }