spanDetailsTable.tsx 7.8 KB

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