spanMetricsTable.tsx 6.6 KB

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