spanMetricsTable.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {Fragment} from 'react';
  2. import type {Location} from 'history';
  3. import type {GridColumnHeader} from 'sentry/components/gridEditable';
  4. import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  5. import Link from 'sentry/components/links/link';
  6. import Pagination, {type CursorHandler} from 'sentry/components/pagination';
  7. import {t} from 'sentry/locale';
  8. import type {Organization} from 'sentry/types/organization';
  9. import type {Project} from 'sentry/types/project';
  10. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  11. import type {ColumnType} from 'sentry/utils/discover/fields';
  12. import {Container as TableCellContainer} from 'sentry/utils/discover/styles';
  13. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  14. import {decodeScalar} from 'sentry/utils/queryString';
  15. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  16. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import {useNavigate} from 'sentry/utils/useNavigate';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import {renderHeadCell} from 'sentry/views/insights/common/components/tableCells/renderHeadCell';
  21. import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
  22. import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
  23. import {
  24. type DomainView,
  25. useDomainViewFilters,
  26. } from 'sentry/views/insights/pages/useFilters';
  27. import {
  28. SpanMetricsField,
  29. type SpanMetricsQueryFilters,
  30. } from 'sentry/views/insights/types';
  31. import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
  32. import {useSpansTabTableSort} from 'sentry/views/performance/transactionSummary/transactionSpans/useSpansTabTableSort';
  33. type DataRow = {
  34. [SpanMetricsField.SPAN_OP]: string;
  35. [SpanMetricsField.SPAN_DESCRIPTION]: string;
  36. [SpanMetricsField.SPAN_GROUP]: string;
  37. 'avg(span.duration)': number;
  38. 'spm()': number;
  39. 'sum(span.duration)': number;
  40. };
  41. type ColumnKeys =
  42. | SpanMetricsField.SPAN_OP
  43. | SpanMetricsField.SPAN_DESCRIPTION
  44. | 'spm()'
  45. | `avg(${SpanMetricsField.SPAN_DURATION})`
  46. | `sum(${SpanMetricsField.SPAN_DURATION})`;
  47. type Column = GridColumnHeader<ColumnKeys>;
  48. const COLUMN_ORDER: Column[] = [
  49. {
  50. key: SpanMetricsField.SPAN_OP,
  51. name: t('Span Operation'),
  52. width: COL_WIDTH_UNDEFINED,
  53. },
  54. {
  55. key: SpanMetricsField.SPAN_DESCRIPTION,
  56. name: t('Span Description'),
  57. width: COL_WIDTH_UNDEFINED,
  58. },
  59. {
  60. key: 'spm()',
  61. name: t('Throughput'),
  62. width: COL_WIDTH_UNDEFINED,
  63. },
  64. {
  65. key: `avg(${SpanMetricsField.SPAN_DURATION})`,
  66. name: t('Avg Duration'),
  67. width: COL_WIDTH_UNDEFINED,
  68. },
  69. {
  70. key: `sum(${SpanMetricsField.SPAN_DURATION})`,
  71. name: t('Time Spent'),
  72. width: COL_WIDTH_UNDEFINED,
  73. },
  74. ];
  75. const COLUMN_TYPE: Record<ColumnKeys, ColumnType> = {
  76. [SpanMetricsField.SPAN_OP]: 'string',
  77. [SpanMetricsField.SPAN_DESCRIPTION]: 'string',
  78. ['spm()']: 'rate',
  79. [`avg(${SpanMetricsField.SPAN_DURATION})`]: 'duration',
  80. [`sum(${SpanMetricsField.SPAN_DURATION})`]: 'duration',
  81. };
  82. const LIMIT = 12;
  83. type Props = {
  84. project: Project | undefined;
  85. query: string;
  86. transactionName: string;
  87. };
  88. export default function SpanMetricsTable(props: Props) {
  89. const {project, transactionName, query: search} = props;
  90. const organization = useOrganization();
  91. const navigate = useNavigate();
  92. const location = useLocation();
  93. const sort = useSpansTabTableSort();
  94. const domainViewFilters = useDomainViewFilters();
  95. const query = useLocationQuery({
  96. fields: {
  97. spansCursor: decodeScalar,
  98. spanOp: decodeScalar,
  99. },
  100. });
  101. const {spansCursor, spanOp} = query;
  102. const filters: SpanMetricsQueryFilters = {
  103. transaction: transactionName,
  104. ['span.op']: spanOp,
  105. };
  106. const handleCursor: CursorHandler = (cursor, pathname, q) => {
  107. navigate({
  108. pathname,
  109. query: {...q, [QueryParameterNames.SPANS_CURSOR]: cursor},
  110. });
  111. };
  112. const mutableSearch = MutableSearch.fromQueryObject(filters);
  113. mutableSearch.addStringMultiFilter(search);
  114. const {data, isPending, pageLinks} = useSpanMetrics(
  115. {
  116. search: mutableSearch,
  117. fields: [
  118. SpanMetricsField.SPAN_OP,
  119. SpanMetricsField.SPAN_DESCRIPTION,
  120. SpanMetricsField.SPAN_GROUP,
  121. `spm()`,
  122. `avg(${SpanMetricsField.SPAN_DURATION})`,
  123. `sum(${SpanMetricsField.SPAN_DURATION})`,
  124. ],
  125. sorts: [sort],
  126. cursor: spansCursor,
  127. limit: LIMIT,
  128. },
  129. 'api.performance.transaction-spans'
  130. );
  131. return (
  132. <Fragment>
  133. <VisuallyCompleteWithData
  134. id="TransactionSpans-SpanMetricsTable"
  135. hasData={!!data?.length}
  136. isLoading={isPending}
  137. >
  138. <GridEditable
  139. isLoading={isPending}
  140. data={data}
  141. columnOrder={COLUMN_ORDER}
  142. columnSortBy={[
  143. {
  144. key: sort.field,
  145. order: sort.kind,
  146. },
  147. ]}
  148. grid={{
  149. renderHeadCell: column =>
  150. renderHeadCell({
  151. column,
  152. location,
  153. sort,
  154. }),
  155. // This is now caught by noUncheckedIndexedAccess, ignoring for now as
  156. // it seems related to some nasty grid editable generic.
  157. // @ts-expect-error TS(2769): No overload matches this call.
  158. renderBodyCell: renderBodyCell(
  159. location,
  160. organization,
  161. transactionName,
  162. project,
  163. domainViewFilters?.view
  164. ),
  165. }}
  166. />
  167. </VisuallyCompleteWithData>
  168. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  169. </Fragment>
  170. );
  171. }
  172. function renderBodyCell(
  173. location: Location,
  174. organization: Organization,
  175. transactionName: string,
  176. project?: Project,
  177. view?: DomainView
  178. ) {
  179. return function (column: Column, dataRow: DataRow): React.ReactNode {
  180. if (column.key === SpanMetricsField.SPAN_OP) {
  181. const target = spanDetailsRouteWithQuery({
  182. organization,
  183. transaction: transactionName,
  184. query: location.query,
  185. spanSlug: {op: dataRow['span.op'], group: ''},
  186. projectID: project?.id,
  187. view,
  188. });
  189. return (
  190. <TableCellContainer>
  191. <Link to={target}>{dataRow[column.key]}</Link>
  192. </TableCellContainer>
  193. );
  194. }
  195. if (column.key === SpanMetricsField.SPAN_DESCRIPTION) {
  196. if (!dataRow['span.group']) {
  197. return <TableCellContainer>{'\u2014'}</TableCellContainer>;
  198. }
  199. const target = spanDetailsRouteWithQuery({
  200. organization,
  201. transaction: transactionName,
  202. query: location.query,
  203. spanSlug: {op: dataRow['span.op'], group: dataRow['span.group']},
  204. projectID: project?.id,
  205. view,
  206. });
  207. return (
  208. <TableCellContainer>
  209. <Link to={target}>{dataRow[column.key]}</Link>
  210. </TableCellContainer>
  211. );
  212. }
  213. const fieldRenderer = getFieldRenderer(column.key, COLUMN_TYPE, false);
  214. const rendered = fieldRenderer(dataRow, {location, organization});
  215. return rendered;
  216. };
  217. }