spanTree.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import isEqual from 'lodash/isEqual';
  4. import {MessageRow} from 'app/components/performance/waterfall/messageRow';
  5. import {pickBarColor} from 'app/components/performance/waterfall/utils';
  6. import {t, tct} from 'app/locale';
  7. import {Organization} from 'app/types';
  8. import {DragManagerChildrenProps} from './dragManager';
  9. import {ScrollbarManagerChildrenProps, withScrollbarManager} from './scrollbarManager';
  10. import SpanBar from './spanBar';
  11. import {EnhancedProcessedSpanType, FilterSpans, ParsedTraceType} from './types';
  12. import {getSpanID, getSpanOperation} from './utils';
  13. import WaterfallModel from './waterfallModel';
  14. type PropType = ScrollbarManagerChildrenProps & {
  15. organization: Organization;
  16. dragProps: DragManagerChildrenProps;
  17. traceViewRef: React.RefObject<HTMLDivElement>;
  18. filterSpans: FilterSpans | undefined;
  19. waterfallModel: WaterfallModel;
  20. spans: EnhancedProcessedSpanType[];
  21. };
  22. class SpanTree extends React.Component<PropType> {
  23. shouldComponentUpdate(nextProps: PropType) {
  24. if (
  25. this.props.dragProps.isDragging !== nextProps.dragProps.isDragging ||
  26. this.props.dragProps.isWindowSelectionDragging !==
  27. nextProps.dragProps.isWindowSelectionDragging
  28. ) {
  29. return true;
  30. }
  31. if (
  32. nextProps.dragProps.isDragging ||
  33. nextProps.dragProps.isWindowSelectionDragging ||
  34. isEqual(this.props.spans, nextProps.spans)
  35. ) {
  36. return false;
  37. }
  38. return true;
  39. }
  40. componentDidUpdate(prevProps: PropType) {
  41. if (
  42. !isEqual(prevProps.filterSpans, this.props.filterSpans) ||
  43. !isEqual(prevProps.spans, this.props.spans)
  44. ) {
  45. // Update horizontal scroll states after a search has been performed or if
  46. // if the spans has changed
  47. this.props.updateScrollState();
  48. }
  49. }
  50. generateInfoMessage(input: {
  51. isCurrentSpanHidden: boolean;
  52. numOfSpansOutOfViewAbove: number;
  53. isCurrentSpanFilteredOut: boolean;
  54. numOfFilteredSpansAbove: number;
  55. }): React.ReactNode {
  56. const {
  57. isCurrentSpanHidden,
  58. numOfSpansOutOfViewAbove,
  59. isCurrentSpanFilteredOut,
  60. numOfFilteredSpansAbove,
  61. } = input;
  62. const messages: React.ReactNode[] = [];
  63. const showHiddenSpansMessage = !isCurrentSpanHidden && numOfSpansOutOfViewAbove > 0;
  64. if (showHiddenSpansMessage) {
  65. messages.push(
  66. <span key="spans-out-of-view">
  67. <strong>{numOfSpansOutOfViewAbove}</strong> {t('spans out of view')}
  68. </span>
  69. );
  70. }
  71. const showFilteredSpansMessage =
  72. !isCurrentSpanFilteredOut && numOfFilteredSpansAbove > 0;
  73. if (showFilteredSpansMessage) {
  74. if (!isCurrentSpanHidden) {
  75. if (numOfFilteredSpansAbove === 1) {
  76. messages.push(
  77. <span key="spans-filtered">
  78. {tct('[numOfSpans] hidden span', {
  79. numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
  80. })}
  81. </span>
  82. );
  83. } else {
  84. messages.push(
  85. <span key="spans-filtered">
  86. {tct('[numOfSpans] hidden spans', {
  87. numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
  88. })}
  89. </span>
  90. );
  91. }
  92. }
  93. }
  94. if (messages.length <= 0) {
  95. return null;
  96. }
  97. return <MessageRow>{messages}</MessageRow>;
  98. }
  99. generateLimitExceededMessage() {
  100. const {waterfallModel} = this.props;
  101. const {parsedTrace} = waterfallModel;
  102. if (hasAllSpans(parsedTrace)) {
  103. return null;
  104. }
  105. return (
  106. <MessageRow>
  107. {t(
  108. 'The next spans are unavailable. You may have exceeded the span limit or need to address missing instrumentation.'
  109. )}
  110. </MessageRow>
  111. );
  112. }
  113. toggleSpanTree = (spanID: string) => () => {
  114. this.props.waterfallModel.toggleSpanGroup(spanID);
  115. // Update horizontal scroll states after this subtree was either hidden or
  116. // revealed.
  117. this.props.updateScrollState();
  118. };
  119. render() {
  120. const {waterfallModel, spans, organization, dragProps} = this.props;
  121. const generateBounds = waterfallModel.generateBounds({
  122. viewStart: dragProps.viewWindowStart,
  123. viewEnd: dragProps.viewWindowEnd,
  124. });
  125. type AccType = {
  126. numOfSpansOutOfViewAbove: number;
  127. numOfFilteredSpansAbove: number;
  128. spanTree: React.ReactNode[];
  129. };
  130. const {spanTree, numOfSpansOutOfViewAbove, numOfFilteredSpansAbove} = spans.reduce(
  131. (acc: AccType, payload: EnhancedProcessedSpanType, index) => {
  132. const {type} = payload;
  133. switch (payload.type) {
  134. case 'filtered_out': {
  135. acc.numOfFilteredSpansAbove += 1;
  136. return acc;
  137. }
  138. case 'out_of_view': {
  139. acc.numOfSpansOutOfViewAbove += 1;
  140. return acc;
  141. }
  142. default: {
  143. break;
  144. }
  145. }
  146. const previousSpanNotDisplayed =
  147. acc.numOfFilteredSpansAbove > 0 || acc.numOfSpansOutOfViewAbove > 0;
  148. if (previousSpanNotDisplayed) {
  149. const infoMessage = this.generateInfoMessage({
  150. isCurrentSpanHidden: false,
  151. numOfSpansOutOfViewAbove: acc.numOfSpansOutOfViewAbove,
  152. isCurrentSpanFilteredOut: false,
  153. numOfFilteredSpansAbove: acc.numOfFilteredSpansAbove,
  154. });
  155. acc.spanTree.push(infoMessage);
  156. }
  157. const {span} = payload;
  158. const key = getSpanID(span, `span-${index}`);
  159. const isLast = payload.isLastSibling;
  160. const isRoot = type === 'root_span';
  161. const spanBarColor: string = pickBarColor(getSpanOperation(span));
  162. const spanNumber = index + 1;
  163. const numOfSpanChildren = payload.numOfSpanChildren;
  164. const treeDepth = payload.treeDepth;
  165. const continuingTreeDepths = payload.continuingTreeDepths;
  166. acc.numOfFilteredSpansAbove = 0;
  167. acc.numOfSpansOutOfViewAbove = 0;
  168. acc.spanTree.push(
  169. <SpanBar
  170. key={key}
  171. organization={organization}
  172. event={waterfallModel.event}
  173. spanBarColor={spanBarColor}
  174. spanBarHatch={type === 'gap'}
  175. span={span}
  176. showSpanTree={!waterfallModel.hiddenSpanGroups.has(getSpanID(span))}
  177. numOfSpanChildren={numOfSpanChildren}
  178. trace={waterfallModel.parsedTrace}
  179. generateBounds={generateBounds}
  180. toggleSpanTree={this.toggleSpanTree(getSpanID(span))}
  181. treeDepth={treeDepth}
  182. continuingTreeDepths={continuingTreeDepths}
  183. spanNumber={spanNumber}
  184. isLast={isLast}
  185. isRoot={isRoot}
  186. showEmbeddedChildren={payload.showEmbeddedChildren}
  187. toggleEmbeddedChildren={payload.toggleEmbeddedChildren}
  188. fetchEmbeddedChildrenState={payload.fetchEmbeddedChildrenState}
  189. />
  190. );
  191. return acc;
  192. },
  193. {
  194. numOfSpansOutOfViewAbove: 0,
  195. numOfFilteredSpansAbove: 0,
  196. spanTree: [],
  197. }
  198. );
  199. const infoMessage = this.generateInfoMessage({
  200. isCurrentSpanHidden: false,
  201. numOfSpansOutOfViewAbove,
  202. isCurrentSpanFilteredOut: false,
  203. numOfFilteredSpansAbove,
  204. });
  205. return (
  206. <TraceViewContainer ref={this.props.traceViewRef}>
  207. {spanTree}
  208. {infoMessage}
  209. {this.generateLimitExceededMessage()}
  210. </TraceViewContainer>
  211. );
  212. }
  213. }
  214. const TraceViewContainer = styled('div')`
  215. overflow-x: hidden;
  216. border-bottom-left-radius: 3px;
  217. border-bottom-right-radius: 3px;
  218. `;
  219. /**
  220. * Checks if a trace contains all of its spans.
  221. *
  222. * The heuristic used here favors false negatives over false positives.
  223. * This is because showing a warning that the trace is not showing all
  224. * spans when it has them all is more misleading than not showing a
  225. * warning when it is missing some spans.
  226. *
  227. * A simple heuristic to determine when there are unrecorded spans
  228. *
  229. * 1. We assume if there are less than 999 spans, then we have all
  230. * the spans for a transaction. 999 was chosen because most SDKs
  231. * have a default limit of 1000 spans per transaction, but the
  232. * python SDK is 999 for historic reasons.
  233. *
  234. * 2. We assume that if there are unrecorded spans, they should be
  235. * at least 100ms in duration.
  236. *
  237. * While not perfect, this simple heuristic is unlikely to report
  238. * false positives.
  239. */
  240. function hasAllSpans(trace: ParsedTraceType): boolean {
  241. const {traceEndTimestamp, spans} = trace;
  242. if (spans.length < 999) {
  243. return true;
  244. }
  245. const lastSpan = spans.reduce((latest, span) =>
  246. latest.timestamp > span.timestamp ? latest : span
  247. );
  248. const missingDuration = traceEndTimestamp - lastSpan.timestamp;
  249. return missingDuration < 0.1;
  250. }
  251. export default withScrollbarManager(SpanTree);