spanDetailsTable.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumnOrder,
  7. } from 'sentry/components/gridEditable';
  8. import SortLink from 'sentry/components/gridEditable/sortLink';
  9. import Link from 'sentry/components/links/link';
  10. import Pagination from 'sentry/components/pagination';
  11. import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
  12. import {pickBarColor, toPercent} from 'sentry/components/performance/waterfall/utils';
  13. import PerformanceDuration from 'sentry/components/performanceDuration';
  14. import Tooltip from 'sentry/components/tooltip';
  15. import {t, tct} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {Organization, Project} from 'sentry/types';
  18. import {defined} from 'sentry/utils';
  19. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  20. import {ColumnType, fieldAlignment} from 'sentry/utils/discover/fields';
  21. import {formatPercentage} from 'sentry/utils/formatters';
  22. import {
  23. ExampleTransaction,
  24. SuspectSpan,
  25. } from 'sentry/utils/performance/suspectSpans/types';
  26. import {generateTransactionLink} from '../../utils';
  27. type TableColumnKeys =
  28. | 'id'
  29. | 'timestamp'
  30. | 'transactionDuration'
  31. | 'spanDuration'
  32. | 'occurrences'
  33. | 'cumulativeDuration'
  34. | 'spans';
  35. type TableColumn = GridColumnOrder<TableColumnKeys>;
  36. type TableDataRow = Record<TableColumnKeys, any>;
  37. type Props = {
  38. examples: ExampleTransaction[];
  39. isLoading: boolean;
  40. location: Location;
  41. organization: Organization;
  42. transactionName: string;
  43. pageLinks?: string | null;
  44. project?: Project;
  45. suspectSpan?: SuspectSpan;
  46. };
  47. export default function SpanTable(props: Props) {
  48. const {
  49. location,
  50. organization,
  51. project,
  52. examples,
  53. suspectSpan,
  54. transactionName,
  55. isLoading,
  56. pageLinks,
  57. } = props;
  58. if (!defined(examples)) {
  59. return null;
  60. }
  61. const data = examples
  62. // we assume that the span appears in each example at least once,
  63. // if this assumption is broken, nothing onwards will work so
  64. // filter out such examples
  65. .filter(example => example.spans.length > 0)
  66. .map(example => ({
  67. id: example.id,
  68. project: project?.slug,
  69. // timestamps are in seconds but want them in milliseconds
  70. timestamp: example.finishTimestamp * 1000,
  71. transactionDuration: (example.finishTimestamp - example.startTimestamp) * 1000,
  72. spanDuration: example.nonOverlappingExclusiveTime,
  73. occurrences: example.spans.length,
  74. cumulativeDuration: example.spans.reduce(
  75. (duration, span) => duration + span.exclusiveTime,
  76. 0
  77. ),
  78. spans: example.spans,
  79. }));
  80. return (
  81. <Fragment>
  82. <GridEditable
  83. isLoading={isLoading}
  84. data={data}
  85. columnOrder={SPANS_TABLE_COLUMN_ORDER}
  86. columnSortBy={[]}
  87. grid={{
  88. renderHeadCell,
  89. renderBodyCell: renderBodyCellWithMeta(
  90. location,
  91. organization,
  92. transactionName,
  93. suspectSpan
  94. ),
  95. }}
  96. location={location}
  97. />
  98. <Pagination pageLinks={pageLinks ?? null} />
  99. </Fragment>
  100. );
  101. }
  102. function renderHeadCell(column: TableColumn, _index: number): React.ReactNode {
  103. const align = fieldAlignment(column.key, COLUMN_TYPE[column.key]);
  104. return (
  105. <SortLink
  106. title={column.name}
  107. align={align}
  108. direction={undefined}
  109. canSort={false}
  110. generateSortLink={() => undefined}
  111. />
  112. );
  113. }
  114. function renderBodyCellWithMeta(
  115. location: Location,
  116. organization: Organization,
  117. transactionName: string,
  118. suspectSpan?: SuspectSpan
  119. ) {
  120. return (column: TableColumn, dataRow: TableDataRow): React.ReactNode => {
  121. // if the transaction duration is falsey, then just render the span duration on its own
  122. if (column.key === 'spanDuration' && dataRow.transactionDuration) {
  123. return (
  124. <SpanDurationBar
  125. spanOp={suspectSpan?.op ?? ''}
  126. spanDuration={dataRow.spanDuration}
  127. transactionDuration={dataRow.transactionDuration}
  128. />
  129. );
  130. }
  131. const fieldRenderer = getFieldRenderer(column.key, COLUMN_TYPE);
  132. let rendered = fieldRenderer(dataRow, {location, organization});
  133. if (column.key === 'id') {
  134. const worstSpan = dataRow.spans.length
  135. ? dataRow.spans.reduce((worst, span) =>
  136. worst.exclusiveTime >= span.exclusiveTime ? worst : span
  137. )
  138. : null;
  139. const target = generateTransactionLink(transactionName)(
  140. organization,
  141. dataRow,
  142. location.query,
  143. worstSpan.id
  144. );
  145. rendered = <Link to={target}>{rendered}</Link>;
  146. }
  147. return rendered;
  148. };
  149. }
  150. const COLUMN_TYPE: Omit<
  151. Record<TableColumnKeys, ColumnType>,
  152. 'spans' | 'transactionDuration'
  153. > = {
  154. id: 'string',
  155. timestamp: 'date',
  156. spanDuration: 'duration',
  157. occurrences: 'integer',
  158. cumulativeDuration: 'duration',
  159. };
  160. const SPANS_TABLE_COLUMN_ORDER: TableColumn[] = [
  161. {
  162. key: 'id',
  163. name: t('Event ID'),
  164. width: COL_WIDTH_UNDEFINED,
  165. },
  166. {
  167. key: 'timestamp',
  168. name: t('Timestamp'),
  169. width: COL_WIDTH_UNDEFINED,
  170. },
  171. {
  172. key: 'spanDuration',
  173. name: t('Span Duration'),
  174. width: COL_WIDTH_UNDEFINED,
  175. },
  176. {
  177. key: 'occurrences',
  178. name: t('Count'),
  179. width: COL_WIDTH_UNDEFINED,
  180. },
  181. {
  182. key: 'cumulativeDuration',
  183. name: t('Cumulative Duration'),
  184. width: COL_WIDTH_UNDEFINED,
  185. },
  186. ];
  187. const DurationBar = styled('div')`
  188. position: relative;
  189. display: flex;
  190. top: ${space(0.5)};
  191. background-color: ${p => p.theme.gray100};
  192. `;
  193. const DurationBarSection = styled(RowRectangle)`
  194. position: relative;
  195. width: 100%;
  196. top: 0;
  197. `;
  198. type SpanDurationBarProps = {
  199. spanDuration: number;
  200. spanOp: string;
  201. transactionDuration: number;
  202. };
  203. function SpanDurationBar(props: SpanDurationBarProps) {
  204. const {spanOp, spanDuration, transactionDuration} = props;
  205. const widthPercentage = spanDuration / transactionDuration;
  206. const position = widthPercentage < 0.7 ? 'right' : 'inset';
  207. return (
  208. <DurationBar>
  209. <div style={{width: toPercent(widthPercentage)}}>
  210. <Tooltip
  211. title={tct('[percentage] of the transaction', {
  212. percentage: formatPercentage(widthPercentage),
  213. })}
  214. containerDisplayMode="block"
  215. >
  216. <DurationBarSection style={{backgroundColor: pickBarColor(spanOp)}}>
  217. <DurationPill durationDisplay={position} showDetail={false}>
  218. <PerformanceDuration abbreviation milliseconds={spanDuration} />
  219. </DurationPill>
  220. </DurationBarSection>
  221. </Tooltip>
  222. </div>
  223. </DurationBar>
  224. );
  225. }