pipelineSpansTable.tsx 5.7 KB

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