pipelineSpansTable.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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 {t} from 'sentry/locale';
  8. import type {Organization} from 'sentry/types/organization';
  9. import EventView, {type EventsMetaType} from 'sentry/utils/discover/eventView';
  10. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  11. import type {Sort} from 'sentry/utils/discover/fields';
  12. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  13. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  14. import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
  15. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  16. import {useLocation} from 'sentry/utils/useLocation';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {renderHeadCell} from 'sentry/views/insights/common/components/tableCells/renderHeadCell';
  19. import {
  20. useEAPSpans,
  21. useSpansIndexed,
  22. } from 'sentry/views/insights/common/queries/useDiscover';
  23. import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
  24. import {SpanIndexedField} from 'sentry/views/insights/types';
  25. import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
  26. type Column = GridColumnHeader<
  27. | SpanIndexedField.ID
  28. | SpanIndexedField.SPAN_DURATION
  29. | SpanIndexedField.TIMESTAMP
  30. | SpanIndexedField.USER
  31. >;
  32. const COLUMN_ORDER: Column[] = [
  33. {
  34. key: SpanIndexedField.ID,
  35. name: t('Span ID'),
  36. width: COL_WIDTH_UNDEFINED,
  37. },
  38. {
  39. key: SpanIndexedField.USER,
  40. name: t('User'),
  41. width: COL_WIDTH_UNDEFINED,
  42. },
  43. {
  44. key: SpanIndexedField.TIMESTAMP,
  45. name: t('Timestamp'),
  46. width: COL_WIDTH_UNDEFINED,
  47. },
  48. {
  49. key: SpanIndexedField.SPAN_DURATION,
  50. name: t('Total duration'),
  51. width: 150,
  52. },
  53. ];
  54. const SORTABLE_FIELDS = [
  55. SpanIndexedField.ID,
  56. SpanIndexedField.SPAN_DURATION,
  57. SpanIndexedField.TIMESTAMP,
  58. ];
  59. type ValidSort = Sort & {
  60. field:
  61. | SpanIndexedField.ID
  62. | SpanIndexedField.SPAN_DURATION
  63. | SpanIndexedField.TIMESTAMP;
  64. };
  65. export function isAValidSort(sort: Sort): sort is ValidSort {
  66. return (SORTABLE_FIELDS as unknown as string[]).includes(sort.field);
  67. }
  68. interface Props {
  69. groupId: string;
  70. useEAP: boolean;
  71. referrer?: string;
  72. }
  73. export function PipelineSpansTable({groupId, useEAP}: Props) {
  74. const location = useLocation();
  75. const organization = useOrganization();
  76. const sortField = decodeScalar(location.query?.[QueryParameterNames.SPANS_SORT]);
  77. let sort = decodeSorts(sortField).filter(isAValidSort)[0];
  78. if (!sort) {
  79. sort = {field: SpanIndexedField.TIMESTAMP, kind: 'desc'};
  80. }
  81. const {
  82. data: rawData,
  83. meta: rawMeta,
  84. error,
  85. isPending,
  86. } = useSpansIndexed(
  87. {
  88. limit: 30,
  89. sorts: [sort],
  90. fields: [
  91. SpanIndexedField.ID,
  92. SpanIndexedField.TRACE,
  93. SpanIndexedField.SPAN_DURATION,
  94. SpanIndexedField.TRANSACTION_ID,
  95. SpanIndexedField.USER,
  96. SpanIndexedField.TIMESTAMP,
  97. SpanIndexedField.PROJECT,
  98. ],
  99. search: new MutableSearch(`span.category:ai.pipeline span.group:"${groupId}"`),
  100. enabled: !useEAP,
  101. },
  102. 'api.ai-pipelines.view'
  103. );
  104. const {
  105. data: eapData,
  106. meta: eapMeta,
  107. error: eapError,
  108. isPending: eapPending,
  109. } = useEAPSpans(
  110. {
  111. limit: 30,
  112. sorts: [sort],
  113. fields: [
  114. SpanIndexedField.ID,
  115. SpanIndexedField.TRACE,
  116. SpanIndexedField.SPAN_DURATION,
  117. SpanIndexedField.TRANSACTION_ID,
  118. SpanIndexedField.USER,
  119. SpanIndexedField.TIMESTAMP,
  120. SpanIndexedField.PROJECT,
  121. ],
  122. search: new MutableSearch(`span.category:ai.pipeline span.group:"${groupId}"`),
  123. enabled: useEAP,
  124. },
  125. 'api.ai-pipelines.view'
  126. );
  127. const data = (useEAP ? eapData : rawData) ?? [];
  128. const meta = (useEAP ? eapMeta : rawMeta) as EventsMetaType;
  129. return (
  130. <VisuallyCompleteWithData
  131. id="PipelineSpansTable"
  132. hasData={data.length > 0}
  133. isLoading={useEAP ? eapPending : isPending}
  134. >
  135. <GridEditable
  136. isLoading={useEAP ? eapPending : isPending}
  137. error={useEAP ? eapError : error}
  138. data={data}
  139. columnOrder={COLUMN_ORDER}
  140. columnSortBy={[
  141. {
  142. key: sort.field,
  143. order: sort.kind,
  144. },
  145. ]}
  146. grid={{
  147. renderHeadCell: column =>
  148. renderHeadCell({
  149. column,
  150. sort,
  151. location,
  152. sortParameterName: QueryParameterNames.SPANS_SORT,
  153. }),
  154. renderBodyCell: (column, row) =>
  155. renderBodyCell(column, row, meta, location, organization, groupId),
  156. }}
  157. />
  158. </VisuallyCompleteWithData>
  159. );
  160. }
  161. function renderBodyCell(
  162. column: Column,
  163. row: any,
  164. meta: EventsMetaType | undefined,
  165. location: Location,
  166. organization: Organization,
  167. groupId: string
  168. ) {
  169. if (column.key === SpanIndexedField.ID) {
  170. if (!row[SpanIndexedField.ID]) {
  171. return <span>(unknown)</span>;
  172. }
  173. if (!row[SpanIndexedField.TRACE]) {
  174. return <span>{row[SpanIndexedField.ID]}</span>;
  175. }
  176. return (
  177. <Link
  178. to={generateLinkToEventInTraceView({
  179. organization,
  180. eventId: row[SpanIndexedField.TRANSACTION_ID],
  181. projectSlug: row[SpanIndexedField.PROJECT],
  182. traceSlug: row[SpanIndexedField.TRACE],
  183. timestamp: row[SpanIndexedField.TIMESTAMP],
  184. location: {
  185. ...location,
  186. query: {
  187. ...location.query,
  188. groupId,
  189. },
  190. },
  191. eventView: EventView.fromLocation(location),
  192. spanId: row[SpanIndexedField.ID],
  193. source: TraceViewSources.LLM_MODULE,
  194. })}
  195. >
  196. {row[SpanIndexedField.ID]}
  197. </Link>
  198. );
  199. }
  200. if (!meta || !meta?.fields) {
  201. return row[column.key];
  202. }
  203. const renderer = getFieldRenderer(column.key, meta.fields, false);
  204. const rendered = renderer(row, {
  205. location,
  206. organization,
  207. unit: meta.units?.[column.key],
  208. });
  209. return rendered;
  210. }