spanDetailsTable.tsx 7.0 KB

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