spanDetail.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import DateTime from 'app/components/dateTime';
  4. import {SpanDetailContainer} from 'app/components/events/interfaces/spans/spanDetail';
  5. import {rawSpanKeys, SpanType} from 'app/components/events/interfaces/spans/types';
  6. import {getHumanDuration} from 'app/components/performance/waterfall/utils';
  7. import Pill from 'app/components/pill';
  8. import Pills from 'app/components/pills';
  9. import {t} from 'app/locale';
  10. import space from 'app/styles/space';
  11. import getDynamicText from 'app/utils/getDynamicText';
  12. import theme from 'app/utils/theme';
  13. import SpanDetailContent from './spanDetailContent';
  14. import {SpanBarRectangle} from './styles';
  15. import {
  16. DiffSpanType,
  17. generateCSSWidth,
  18. getSpanDuration,
  19. SpanGeneratedBoundsType,
  20. SpanWidths,
  21. } from './utils';
  22. type DurationDisplay = 'right' | 'inset';
  23. const getDurationDisplay = (width: SpanWidths | undefined): DurationDisplay => {
  24. if (!width) {
  25. return 'right';
  26. }
  27. switch (width.type) {
  28. case 'WIDTH_PIXEL': {
  29. return 'right';
  30. }
  31. case 'WIDTH_PERCENTAGE': {
  32. const spaceNeeded = 0.3;
  33. if (width.width < 1 - spaceNeeded) {
  34. return 'right';
  35. }
  36. return 'inset';
  37. }
  38. default: {
  39. const _exhaustiveCheck: never = width;
  40. return _exhaustiveCheck;
  41. }
  42. }
  43. };
  44. type Props = {
  45. span: Readonly<DiffSpanType>;
  46. bounds: SpanGeneratedBoundsType;
  47. };
  48. class SpanDetail extends React.Component<Props> {
  49. renderContent() {
  50. const {span, bounds} = this.props;
  51. switch (span.comparisonResult) {
  52. case 'matched': {
  53. return (
  54. <MatchedSpanDetailsContent
  55. baselineSpan={span.baselineSpan}
  56. regressionSpan={span.regressionSpan}
  57. bounds={bounds}
  58. />
  59. );
  60. }
  61. case 'regression': {
  62. return <SpanDetailContent span={span.regressionSpan} />;
  63. }
  64. case 'baseline': {
  65. return <SpanDetailContent span={span.baselineSpan} />;
  66. }
  67. default: {
  68. const _exhaustiveCheck: never = span;
  69. return _exhaustiveCheck;
  70. }
  71. }
  72. }
  73. render() {
  74. return (
  75. <SpanDetailContainer
  76. onClick={event => {
  77. // prevent toggling the span detail
  78. event.stopPropagation();
  79. }}
  80. >
  81. {this.renderContent()}
  82. </SpanDetailContainer>
  83. );
  84. }
  85. }
  86. const MatchedSpanDetailsContent = (props: {
  87. baselineSpan: SpanType;
  88. regressionSpan: SpanType;
  89. bounds: SpanGeneratedBoundsType;
  90. }) => {
  91. const {baselineSpan, regressionSpan, bounds} = props;
  92. const dataKeys = new Set([
  93. ...Object.keys(baselineSpan?.data ?? {}),
  94. ...Object.keys(regressionSpan?.data ?? {}),
  95. ]);
  96. const unknownKeys = new Set([
  97. ...Object.keys(baselineSpan).filter(key => {
  98. return !rawSpanKeys.has(key as any);
  99. }),
  100. ...Object.keys(regressionSpan).filter(key => {
  101. return !rawSpanKeys.has(key as any);
  102. }),
  103. ]);
  104. return (
  105. <div>
  106. <SpanBars
  107. bounds={bounds}
  108. baselineSpan={baselineSpan}
  109. regressionSpan={regressionSpan}
  110. />
  111. <Row
  112. baselineTitle={t('Baseline Span ID')}
  113. regressionTitle={t("This Event's Span ID")}
  114. renderBaselineContent={() => baselineSpan.span_id}
  115. renderRegressionContent={() => regressionSpan.span_id}
  116. />
  117. <Row
  118. title={t('Parent Span ID')}
  119. renderBaselineContent={() => baselineSpan.parent_span_id || ''}
  120. renderRegressionContent={() => regressionSpan.parent_span_id || ''}
  121. />
  122. <Row
  123. title={t('Trace ID')}
  124. renderBaselineContent={() => baselineSpan.trace_id}
  125. renderRegressionContent={() => regressionSpan.trace_id}
  126. />
  127. <Row
  128. title={t('Description')}
  129. renderBaselineContent={() => baselineSpan.description ?? ''}
  130. renderRegressionContent={() => regressionSpan.description ?? ''}
  131. />
  132. <Row
  133. title={t('Start Date')}
  134. renderBaselineContent={() =>
  135. getDynamicText({
  136. fixed: 'Mar 16, 2020 9:10:12 AM UTC',
  137. value: (
  138. <React.Fragment>
  139. <DateTime date={baselineSpan.start_timestamp * 1000} />
  140. {` (${baselineSpan.start_timestamp})`}
  141. </React.Fragment>
  142. ),
  143. })
  144. }
  145. renderRegressionContent={() =>
  146. getDynamicText({
  147. fixed: 'Mar 16, 2020 9:10:12 AM UTC',
  148. value: (
  149. <React.Fragment>
  150. <DateTime date={regressionSpan.start_timestamp * 1000} />
  151. {` (${baselineSpan.start_timestamp})`}
  152. </React.Fragment>
  153. ),
  154. })
  155. }
  156. />
  157. <Row
  158. title={t('End Date')}
  159. renderBaselineContent={() =>
  160. getDynamicText({
  161. fixed: 'Mar 16, 2020 9:10:12 AM UTC',
  162. value: (
  163. <React.Fragment>
  164. <DateTime date={baselineSpan.timestamp * 1000} />
  165. {` (${baselineSpan.timestamp})`}
  166. </React.Fragment>
  167. ),
  168. })
  169. }
  170. renderRegressionContent={() =>
  171. getDynamicText({
  172. fixed: 'Mar 16, 2020 9:10:12 AM UTC',
  173. value: (
  174. <React.Fragment>
  175. <DateTime date={regressionSpan.timestamp * 1000} />
  176. {` (${regressionSpan.timestamp})`}
  177. </React.Fragment>
  178. ),
  179. })
  180. }
  181. />
  182. <Row
  183. title={t('Duration')}
  184. renderBaselineContent={() => {
  185. const startTimestamp: number = baselineSpan.start_timestamp;
  186. const endTimestamp: number = baselineSpan.timestamp;
  187. const duration = (endTimestamp - startTimestamp) * 1000;
  188. return `${duration.toFixed(3)}ms`;
  189. }}
  190. renderRegressionContent={() => {
  191. const startTimestamp: number = regressionSpan.start_timestamp;
  192. const endTimestamp: number = regressionSpan.timestamp;
  193. const duration = (endTimestamp - startTimestamp) * 1000;
  194. return `${duration.toFixed(3)}ms`;
  195. }}
  196. />
  197. <Row
  198. title={t('Operation')}
  199. renderBaselineContent={() => baselineSpan.op || ''}
  200. renderRegressionContent={() => regressionSpan.op || ''}
  201. />
  202. <Row
  203. title={t('Same Process as Parent')}
  204. renderBaselineContent={() => String(!!baselineSpan.same_process_as_parent)}
  205. renderRegressionContent={() => String(!!regressionSpan.same_process_as_parent)}
  206. />
  207. <Tags baselineSpan={baselineSpan} regressionSpan={regressionSpan} />
  208. {Array.from(dataKeys).map((dataTitle: string) => (
  209. <Row
  210. key={dataTitle}
  211. title={dataTitle}
  212. renderBaselineContent={() => {
  213. const data = baselineSpan?.data ?? {};
  214. const value: string | undefined = data[dataTitle];
  215. return JSON.stringify(value, null, 4) || '';
  216. }}
  217. renderRegressionContent={() => {
  218. const data = regressionSpan?.data ?? {};
  219. const value: string | undefined = data[dataTitle];
  220. return JSON.stringify(value, null, 4) || '';
  221. }}
  222. />
  223. ))}
  224. {Array.from(unknownKeys).map(key => (
  225. <Row
  226. key={key}
  227. title={key}
  228. renderBaselineContent={() => {
  229. return JSON.stringify(baselineSpan[key], null, 4) || '';
  230. }}
  231. renderRegressionContent={() => {
  232. return JSON.stringify(regressionSpan[key], null, 4) || '';
  233. }}
  234. />
  235. ))}
  236. </div>
  237. );
  238. };
  239. const RowSplitter = styled('div')`
  240. display: flex;
  241. flex-direction: row;
  242. > * + * {
  243. border-left: 1px solid ${p => p.theme.border};
  244. }
  245. `;
  246. const SpanBarContainer = styled('div')`
  247. position: relative;
  248. height: 16px;
  249. margin-top: ${space(3)};
  250. margin-bottom: ${space(2)};
  251. `;
  252. const SpanBars = (props: {
  253. bounds: SpanGeneratedBoundsType;
  254. baselineSpan: SpanType;
  255. regressionSpan: SpanType;
  256. }) => {
  257. const {bounds, baselineSpan, regressionSpan} = props;
  258. const baselineDurationDisplay = getDurationDisplay(bounds.baseline);
  259. const regressionDurationDisplay = getDurationDisplay(bounds.regression);
  260. return (
  261. <RowSplitter>
  262. <RowContainer>
  263. <SpanBarContainer>
  264. <SpanBarRectangle
  265. style={{
  266. backgroundColor: theme.gray500,
  267. width: generateCSSWidth(bounds.baseline),
  268. position: 'absolute',
  269. height: '16px',
  270. }}
  271. >
  272. <DurationPill
  273. durationDisplay={baselineDurationDisplay}
  274. fontColors={{right: theme.gray500, inset: theme.white}}
  275. >
  276. {getHumanDuration(getSpanDuration(baselineSpan))}
  277. </DurationPill>
  278. </SpanBarRectangle>
  279. </SpanBarContainer>
  280. </RowContainer>
  281. <RowContainer>
  282. <SpanBarContainer>
  283. <SpanBarRectangle
  284. style={{
  285. backgroundColor: theme.purple200,
  286. width: generateCSSWidth(bounds.regression),
  287. position: 'absolute',
  288. height: '16px',
  289. }}
  290. >
  291. <DurationPill
  292. durationDisplay={regressionDurationDisplay}
  293. fontColors={{right: theme.gray500, inset: theme.gray500}}
  294. >
  295. {getHumanDuration(getSpanDuration(regressionSpan))}
  296. </DurationPill>
  297. </SpanBarRectangle>
  298. </SpanBarContainer>
  299. </RowContainer>
  300. </RowSplitter>
  301. );
  302. };
  303. const Row = (props: {
  304. title?: string;
  305. baselineTitle?: string;
  306. regressionTitle?: string;
  307. renderBaselineContent: () => React.ReactNode;
  308. renderRegressionContent: () => React.ReactNode;
  309. }) => {
  310. const {title, baselineTitle, regressionTitle} = props;
  311. const baselineContent = props.renderBaselineContent();
  312. const regressionContent = props.renderRegressionContent();
  313. if (!baselineContent && !regressionContent) {
  314. return null;
  315. }
  316. return (
  317. <RowSplitter>
  318. <RowCell title={baselineTitle ?? title ?? ''}>{baselineContent}</RowCell>
  319. <RowCell title={regressionTitle ?? title ?? ''}>{regressionContent}</RowCell>
  320. </RowSplitter>
  321. );
  322. };
  323. const RowContainer = styled('div')`
  324. width: 50%;
  325. min-width: 50%;
  326. max-width: 50%;
  327. flex-basis: 50%;
  328. padding-left: ${space(2)};
  329. padding-right: ${space(2)};
  330. `;
  331. const RowTitle = styled('div')`
  332. font-size: 13px;
  333. font-weight: 600;
  334. `;
  335. const RowCell = ({title, children}: {title: string; children: React.ReactNode}) => {
  336. return (
  337. <RowContainer>
  338. <RowTitle>{title}</RowTitle>
  339. <div>
  340. <pre className="val" style={{marginBottom: space(1)}}>
  341. <span className="val-string">{children}</span>
  342. </pre>
  343. </div>
  344. </RowContainer>
  345. );
  346. };
  347. const getTags = (span: SpanType) => {
  348. const tags: {[tag_name: string]: string} | undefined = span?.tags;
  349. if (!tags) {
  350. return undefined;
  351. }
  352. const keys = Object.keys(tags);
  353. if (keys.length <= 0) {
  354. return undefined;
  355. }
  356. return tags;
  357. };
  358. const TagPills = ({tags}: {tags: {[tag_name: string]: string} | undefined}) => {
  359. if (!tags) {
  360. return null;
  361. }
  362. const keys = Object.keys(tags);
  363. if (keys.length <= 0) {
  364. return null;
  365. }
  366. return (
  367. <Pills>
  368. {keys.map((key, index) => (
  369. <Pill key={index} name={key} value={String(tags[key]) || ''} />
  370. ))}
  371. </Pills>
  372. );
  373. };
  374. const Tags = ({
  375. baselineSpan,
  376. regressionSpan,
  377. }: {
  378. baselineSpan: SpanType;
  379. regressionSpan: SpanType;
  380. }) => {
  381. const baselineTags = getTags(baselineSpan);
  382. const regressionTags = getTags(regressionSpan);
  383. if (!baselineTags && !regressionTags) {
  384. return null;
  385. }
  386. return (
  387. <RowSplitter>
  388. <RowContainer>
  389. <RowTitle>{t('Tags')}</RowTitle>
  390. <div>
  391. <TagPills tags={baselineTags} />
  392. </div>
  393. </RowContainer>
  394. <RowContainer>
  395. <RowTitle>{t('Tags')}</RowTitle>
  396. <div>
  397. <TagPills tags={regressionTags} />
  398. </div>
  399. </RowContainer>
  400. </RowSplitter>
  401. );
  402. };
  403. const DurationPill = styled('div')<{
  404. durationDisplay: DurationDisplay;
  405. fontColors: {right: string; inset: string};
  406. }>`
  407. position: absolute;
  408. top: 50%;
  409. display: flex;
  410. align-items: center;
  411. transform: translateY(-50%);
  412. white-space: nowrap;
  413. font-size: ${p => p.theme.fontSizeExtraSmall};
  414. color: ${p => p.fontColors.right};
  415. ${p => {
  416. switch (p.durationDisplay) {
  417. case 'right':
  418. return `left: calc(100% + ${space(0.75)});`;
  419. default:
  420. return `
  421. right: ${space(0.75)};
  422. color: ${p.fontColors.inset};
  423. `;
  424. }
  425. }};
  426. @media (max-width: ${p => p.theme.breakpoints[1]}) {
  427. font-size: 10px;
  428. }
  429. `;
  430. export default SpanDetail;