spanSummaryTable.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import type {GridColumnHeader} from 'sentry/components/gridEditable';
  6. import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  7. import Pagination, {type CursorHandler} from 'sentry/components/pagination';
  8. import {ROW_HEIGHT, ROW_PADDING} from 'sentry/components/performance/waterfall/constants';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Organization, Project} from 'sentry/types';
  12. import EventView, {type MetaType} from 'sentry/utils/discover/eventView';
  13. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  14. import type {ColumnType} from 'sentry/utils/discover/fields';
  15. import {
  16. type DiscoverQueryProps,
  17. useGenericDiscoverQuery,
  18. } from 'sentry/utils/discover/genericDiscoverQuery';
  19. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  20. import {decodeScalar} from 'sentry/utils/queryString';
  21. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import {useParams} from 'sentry/utils/useParams';
  25. import {SpanDurationBar} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/spanDetailsTable';
  26. import {SpanSummaryReferrer} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/referrers';
  27. import {useSpanSummarySort} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/useSpanSummarySort';
  28. import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
  29. import {SpanIdCell} from 'sentry/views/starfish/components/tableCells/spanIdCell';
  30. import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans';
  31. import {
  32. type IndexedResponse,
  33. ModuleName,
  34. SpanIndexedField,
  35. type SpanMetricsQueryFilters,
  36. } from 'sentry/views/starfish/types';
  37. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  38. type DataRowKeys =
  39. | SpanIndexedField.ID
  40. | SpanIndexedField.TIMESTAMP
  41. | SpanIndexedField.SPAN_DURATION
  42. | SpanIndexedField.TRANSACTION_ID
  43. | SpanIndexedField.TRACE
  44. | SpanIndexedField.PROJECT;
  45. type ColumnKeys =
  46. | SpanIndexedField.ID
  47. | SpanIndexedField.TIMESTAMP
  48. | SpanIndexedField.SPAN_DURATION;
  49. type DataRow = Pick<IndexedResponse, DataRowKeys> & {'transaction.duration': number};
  50. type Column = GridColumnHeader<ColumnKeys>;
  51. const COLUMN_ORDER: Column[] = [
  52. {
  53. key: SpanIndexedField.ID,
  54. name: t('Span ID'),
  55. width: COL_WIDTH_UNDEFINED,
  56. },
  57. {
  58. key: SpanIndexedField.TIMESTAMP,
  59. name: t('Timestamp'),
  60. width: COL_WIDTH_UNDEFINED,
  61. },
  62. {
  63. key: SpanIndexedField.SPAN_DURATION,
  64. name: t('Span Duration'),
  65. width: COL_WIDTH_UNDEFINED,
  66. },
  67. ];
  68. const COLUMN_TYPE: Omit<
  69. Record<ColumnKeys, ColumnType>,
  70. 'spans' | 'transactionDuration'
  71. > = {
  72. span_id: 'string',
  73. timestamp: 'date',
  74. 'span.duration': 'duration',
  75. };
  76. const LIMIT = 8;
  77. type Props = {
  78. project: Project | undefined;
  79. };
  80. export default function SpanSummaryTable(props: Props) {
  81. const {project} = props;
  82. const organization = useOrganization();
  83. const {spanSlug} = useParams();
  84. const [spanOp, groupId] = spanSlug.split(':');
  85. const location = useLocation();
  86. const {transaction} = location.query;
  87. const spansCursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]);
  88. const filters: SpanMetricsQueryFilters = {
  89. 'span.group': groupId,
  90. 'span.op': spanOp,
  91. transaction: transaction as string,
  92. };
  93. const sort = useSpanSummarySort();
  94. const {
  95. data: rowData,
  96. pageLinks,
  97. isLoading: isRowDataLoading,
  98. } = useIndexedSpans({
  99. fields: [
  100. SpanIndexedField.ID,
  101. SpanIndexedField.TRANSACTION_ID,
  102. SpanIndexedField.TIMESTAMP,
  103. SpanIndexedField.SPAN_DURATION,
  104. SpanIndexedField.TRACE,
  105. ],
  106. search: MutableSearch.fromQueryObject(filters),
  107. limit: LIMIT,
  108. referrer: SpanSummaryReferrer.SPAN_SUMMARY_TABLE,
  109. sorts: [sort],
  110. cursor: spansCursor,
  111. });
  112. const transactionIds = rowData?.map(row => row[SpanIndexedField.TRANSACTION_ID]);
  113. const eventView = EventView.fromNewQueryWithLocation(
  114. {
  115. name: 'Transaction Durations',
  116. query: MutableSearch.fromQueryObject({
  117. project: project?.slug,
  118. id: `[${transactionIds?.join() ?? ''}]`,
  119. }).formatString(),
  120. fields: ['id', 'transaction.duration'],
  121. version: 2,
  122. },
  123. location
  124. );
  125. const {
  126. isLoading: isTxnDurationDataLoading,
  127. data: txnDurationData,
  128. isError: isTxnDurationError,
  129. } = useGenericDiscoverQuery<
  130. {
  131. data: any[];
  132. meta: MetaType;
  133. },
  134. DiscoverQueryProps
  135. >({
  136. route: 'events',
  137. eventView,
  138. location,
  139. orgSlug: organization.slug,
  140. getRequestPayload: () => ({
  141. ...eventView.getEventsAPIPayload(location),
  142. interval: eventView.interval,
  143. }),
  144. limit: LIMIT,
  145. options: {
  146. refetchOnWindowFocus: false,
  147. enabled: Boolean(rowData && rowData.length > 0),
  148. },
  149. referrer: SpanSummaryReferrer.SPAN_SUMMARY_TABLE,
  150. });
  151. // Restructure the transaction durations into a map for faster lookup
  152. const transactionDurationMap = {};
  153. txnDurationData?.data.forEach(datum => {
  154. transactionDurationMap[datum.id] = datum['transaction.duration'];
  155. });
  156. const mergedData: DataRow[] =
  157. rowData?.map((row: Pick<IndexedResponse, DataRowKeys>) => {
  158. const transactionId = row[SpanIndexedField.TRANSACTION_ID];
  159. const newRow = {
  160. ...row,
  161. 'transaction.duration': transactionDurationMap[transactionId],
  162. };
  163. return newRow;
  164. }) ?? [];
  165. const handleCursor: CursorHandler = (cursor, pathname, query) => {
  166. browserHistory.push({
  167. pathname,
  168. query: {...query, [QueryParameterNames.SPANS_CURSOR]: cursor},
  169. });
  170. };
  171. return (
  172. <Fragment>
  173. <VisuallyCompleteWithData
  174. id="SpanDetails-SpanDetailsTable"
  175. hasData={!!mergedData?.length}
  176. isLoading={isRowDataLoading}
  177. >
  178. <GridEditable
  179. isLoading={isRowDataLoading}
  180. data={mergedData}
  181. columnOrder={COLUMN_ORDER}
  182. columnSortBy={[
  183. {
  184. key: sort.field,
  185. order: sort.kind,
  186. },
  187. ]}
  188. grid={{
  189. renderHeadCell: column =>
  190. renderHeadCell({
  191. column,
  192. location,
  193. sort,
  194. }),
  195. renderBodyCell: renderBodyCell(
  196. location,
  197. organization,
  198. spanOp,
  199. isTxnDurationDataLoading || isTxnDurationError
  200. ),
  201. }}
  202. location={location}
  203. />
  204. </VisuallyCompleteWithData>
  205. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  206. </Fragment>
  207. );
  208. }
  209. function renderBodyCell(
  210. location: Location,
  211. organization: Organization,
  212. spanOp: string = '',
  213. isTxnDurationDataLoading: boolean
  214. ) {
  215. return function (column: Column, dataRow: DataRow): React.ReactNode {
  216. const {timestamp, span_id, trace, project} = dataRow;
  217. const spanDuration = dataRow[SpanIndexedField.SPAN_DURATION];
  218. const transactionId = dataRow[SpanIndexedField.TRANSACTION_ID];
  219. const transactionDuration = dataRow['transaction.duration'];
  220. if (column.key === SpanIndexedField.SPAN_DURATION) {
  221. if (isTxnDurationDataLoading) {
  222. return <SpanDurationBarLoading />;
  223. }
  224. return (
  225. <SpanDurationBar
  226. spanOp={spanOp}
  227. spanDuration={spanDuration}
  228. transactionDuration={transactionDuration}
  229. />
  230. );
  231. }
  232. if (column.key === SpanIndexedField.ID) {
  233. return (
  234. <SpanIdCell
  235. moduleName={ModuleName.OTHER}
  236. projectSlug={project}
  237. spanId={span_id}
  238. timestamp={timestamp}
  239. traceId={trace}
  240. transactionId={transactionId}
  241. />
  242. );
  243. }
  244. const fieldRenderer = getFieldRenderer(column.key, COLUMN_TYPE);
  245. const rendered = fieldRenderer(dataRow, {location, organization});
  246. return rendered;
  247. };
  248. }
  249. const SpanDurationBarLoading = styled('div')`
  250. height: ${ROW_HEIGHT - 2 * ROW_PADDING}px;
  251. width: 100%;
  252. position: relative;
  253. display: flex;
  254. top: ${space(0.5)};
  255. background-color: ${p => p.theme.gray100};
  256. `;