spanDetailsTable.tsx 6.9 KB

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