spanTree.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import isEqual from 'lodash/isEqual';
  4. import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
  5. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  6. import {t, tct} from 'sentry/locale';
  7. import {Organization} from 'sentry/types';
  8. import {DragManagerChildrenProps} from './dragManager';
  9. import {ScrollbarManagerChildrenProps, withScrollbarManager} from './scrollbarManager';
  10. import SpanBar from './spanBar';
  11. import {SpanDescendantGroupBar} from './spanDescendantGroupBar';
  12. import SpanSiblingGroupBar from './spanSiblingGroupBar';
  13. import {
  14. EnhancedProcessedSpanType,
  15. EnhancedSpan,
  16. FilterSpans,
  17. GroupType,
  18. ParsedTraceType,
  19. SpanType,
  20. } from './types';
  21. import {getSpanID, getSpanOperation, setSpansOnTransaction} from './utils';
  22. import WaterfallModel from './waterfallModel';
  23. type PropType = ScrollbarManagerChildrenProps & {
  24. dragProps: DragManagerChildrenProps;
  25. filterSpans: FilterSpans | undefined;
  26. organization: Organization;
  27. spans: EnhancedProcessedSpanType[];
  28. traceViewRef: React.RefObject<HTMLDivElement>;
  29. waterfallModel: WaterfallModel;
  30. };
  31. class SpanTree extends Component<PropType> {
  32. componentDidMount() {
  33. setSpansOnTransaction(this.props.spans.length);
  34. }
  35. shouldComponentUpdate(nextProps: PropType) {
  36. if (
  37. this.props.dragProps.isDragging !== nextProps.dragProps.isDragging ||
  38. this.props.dragProps.isWindowSelectionDragging !==
  39. nextProps.dragProps.isWindowSelectionDragging
  40. ) {
  41. return true;
  42. }
  43. if (
  44. nextProps.dragProps.isDragging ||
  45. nextProps.dragProps.isWindowSelectionDragging ||
  46. isEqual(this.props.spans, nextProps.spans)
  47. ) {
  48. return false;
  49. }
  50. return true;
  51. }
  52. componentDidUpdate(prevProps: PropType) {
  53. if (
  54. !isEqual(prevProps.filterSpans, this.props.filterSpans) ||
  55. !isEqual(prevProps.spans, this.props.spans)
  56. ) {
  57. // Update horizontal scroll states after a search has been performed or if
  58. // if the spans has changed
  59. this.props.updateScrollState();
  60. }
  61. }
  62. generateInfoMessage(input: {
  63. isCurrentSpanFilteredOut: boolean;
  64. isCurrentSpanHidden: boolean;
  65. numOfFilteredSpansAbove: number;
  66. numOfSpansOutOfViewAbove: number;
  67. }): React.ReactNode {
  68. const {
  69. isCurrentSpanHidden,
  70. numOfSpansOutOfViewAbove,
  71. isCurrentSpanFilteredOut,
  72. numOfFilteredSpansAbove,
  73. } = input;
  74. const messages: React.ReactNode[] = [];
  75. const showHiddenSpansMessage = !isCurrentSpanHidden && numOfSpansOutOfViewAbove > 0;
  76. if (showHiddenSpansMessage) {
  77. messages.push(
  78. <span key="spans-out-of-view">
  79. <strong>{numOfSpansOutOfViewAbove}</strong> {t('spans out of view')}
  80. </span>
  81. );
  82. }
  83. const showFilteredSpansMessage =
  84. !isCurrentSpanFilteredOut && numOfFilteredSpansAbove > 0;
  85. if (showFilteredSpansMessage) {
  86. if (!isCurrentSpanHidden) {
  87. if (numOfFilteredSpansAbove === 1) {
  88. messages.push(
  89. <span key="spans-filtered">
  90. {tct('[numOfSpans] hidden span', {
  91. numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
  92. })}
  93. </span>
  94. );
  95. } else {
  96. messages.push(
  97. <span key="spans-filtered">
  98. {tct('[numOfSpans] hidden spans', {
  99. numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
  100. })}
  101. </span>
  102. );
  103. }
  104. }
  105. }
  106. if (messages.length <= 0) {
  107. return null;
  108. }
  109. return <MessageRow>{messages}</MessageRow>;
  110. }
  111. generateLimitExceededMessage() {
  112. const {waterfallModel} = this.props;
  113. const {parsedTrace} = waterfallModel;
  114. if (hasAllSpans(parsedTrace)) {
  115. return null;
  116. }
  117. return (
  118. <MessageRow>
  119. {t(
  120. 'The next spans are unavailable. You may have exceeded the span limit or need to address missing instrumentation.'
  121. )}
  122. </MessageRow>
  123. );
  124. }
  125. toggleSpanTree = (spanID: string) => () => {
  126. this.props.waterfallModel.toggleSpanSubTree(spanID);
  127. // Update horizontal scroll states after this subtree was either hidden or
  128. // revealed.
  129. this.props.updateScrollState();
  130. };
  131. render() {
  132. const {
  133. waterfallModel,
  134. spans,
  135. organization,
  136. dragProps,
  137. onWheel,
  138. generateContentSpanBarRef,
  139. markSpanOutOfView,
  140. markSpanInView,
  141. storeSpanBar,
  142. } = this.props;
  143. const generateBounds = waterfallModel.generateBounds({
  144. viewStart: dragProps.viewWindowStart,
  145. viewEnd: dragProps.viewWindowEnd,
  146. });
  147. type AccType = {
  148. numOfFilteredSpansAbove: number;
  149. numOfSpansOutOfViewAbove: number;
  150. spanNumber: number;
  151. spanTree: React.ReactNode[];
  152. };
  153. const numOfSpans = spans.reduce((sum: number, payload: EnhancedProcessedSpanType) => {
  154. switch (payload.type) {
  155. case 'root_span':
  156. case 'span':
  157. case 'span_group_chain': {
  158. return sum + 1;
  159. }
  160. default: {
  161. return sum;
  162. }
  163. }
  164. }, 0);
  165. const {spanTree, numOfSpansOutOfViewAbove, numOfFilteredSpansAbove} = spans.reduce(
  166. (acc: AccType, payload: EnhancedProcessedSpanType) => {
  167. const {type} = payload;
  168. switch (payload.type) {
  169. case 'filtered_out': {
  170. acc.numOfFilteredSpansAbove += 1;
  171. return acc;
  172. }
  173. case 'out_of_view': {
  174. acc.numOfSpansOutOfViewAbove += 1;
  175. return acc;
  176. }
  177. default: {
  178. break;
  179. }
  180. }
  181. const previousSpanNotDisplayed =
  182. acc.numOfFilteredSpansAbove > 0 || acc.numOfSpansOutOfViewAbove > 0;
  183. if (previousSpanNotDisplayed) {
  184. const infoMessage = this.generateInfoMessage({
  185. isCurrentSpanHidden: false,
  186. numOfSpansOutOfViewAbove: acc.numOfSpansOutOfViewAbove,
  187. isCurrentSpanFilteredOut: false,
  188. numOfFilteredSpansAbove: acc.numOfFilteredSpansAbove,
  189. });
  190. acc.spanTree.push(infoMessage);
  191. }
  192. const spanNumber = acc.spanNumber;
  193. const {span, treeDepth, continuingTreeDepths} = payload;
  194. if (payload.type === 'span_group_chain') {
  195. acc.spanTree.push(
  196. <SpanDescendantGroupBar
  197. key={`${spanNumber}-span-group`}
  198. event={waterfallModel.event}
  199. span={span}
  200. generateBounds={generateBounds}
  201. treeDepth={treeDepth}
  202. continuingTreeDepths={continuingTreeDepths}
  203. spanNumber={spanNumber}
  204. spanGrouping={payload.spanNestedGrouping as EnhancedSpan[]}
  205. toggleSpanGroup={payload.toggleNestedSpanGroup as () => void}
  206. onWheel={onWheel}
  207. generateContentSpanBarRef={generateContentSpanBarRef}
  208. />
  209. );
  210. acc.spanNumber = spanNumber + 1;
  211. return acc;
  212. }
  213. if (payload.type === 'span_group_siblings') {
  214. acc.spanTree.push(
  215. <SpanSiblingGroupBar
  216. key={`${spanNumber}-span-sibling`}
  217. event={waterfallModel.event}
  218. span={span}
  219. generateBounds={generateBounds}
  220. treeDepth={treeDepth}
  221. continuingTreeDepths={continuingTreeDepths}
  222. spanNumber={spanNumber}
  223. spanGrouping={payload.spanSiblingGrouping as EnhancedSpan[]}
  224. toggleSiblingSpanGroup={payload.toggleSiblingSpanGroup}
  225. isLastSibling={payload.isLastSibling ?? false}
  226. occurrence={payload.occurrence}
  227. onWheel={onWheel}
  228. generateContentSpanBarRef={generateContentSpanBarRef}
  229. />
  230. );
  231. acc.spanNumber = spanNumber + 1;
  232. return acc;
  233. }
  234. const key = getSpanID(span, `span-${spanNumber}`);
  235. const isLast = payload.isLastSibling;
  236. const isRoot = type === 'root_span';
  237. const spanBarColor: string = pickBarColor(getSpanOperation(span));
  238. const numOfSpanChildren = payload.numOfSpanChildren;
  239. acc.numOfFilteredSpansAbove = 0;
  240. acc.numOfSpansOutOfViewAbove = 0;
  241. let toggleSpanGroup: (() => void) | undefined = undefined;
  242. if (payload.type === 'span') {
  243. toggleSpanGroup = payload.toggleNestedSpanGroup;
  244. }
  245. let toggleSiblingSpanGroup:
  246. | ((span: SpanType, occurrence: number) => void)
  247. | undefined = undefined;
  248. if (payload.type === 'span' && payload.isFirstSiblingOfGroup) {
  249. toggleSiblingSpanGroup = payload.toggleSiblingSpanGroup;
  250. }
  251. let groupType;
  252. if (toggleSpanGroup) {
  253. groupType = GroupType.DESCENDANTS;
  254. } else if (toggleSiblingSpanGroup) {
  255. groupType = GroupType.SIBLINGS;
  256. }
  257. acc.spanTree.push(
  258. <SpanBar
  259. key={key}
  260. organization={organization}
  261. event={waterfallModel.event}
  262. spanBarColor={spanBarColor}
  263. spanBarHatch={type === 'gap'}
  264. span={span}
  265. showSpanTree={!waterfallModel.hiddenSpanSubTrees.has(getSpanID(span))}
  266. numOfSpanChildren={numOfSpanChildren}
  267. trace={waterfallModel.parsedTrace}
  268. generateBounds={generateBounds}
  269. toggleSpanTree={this.toggleSpanTree(getSpanID(span))}
  270. treeDepth={treeDepth}
  271. continuingTreeDepths={continuingTreeDepths}
  272. spanNumber={spanNumber}
  273. isLast={isLast}
  274. isRoot={isRoot}
  275. showEmbeddedChildren={payload.showEmbeddedChildren}
  276. toggleEmbeddedChildren={payload.toggleEmbeddedChildren}
  277. toggleSiblingSpanGroup={toggleSiblingSpanGroup}
  278. fetchEmbeddedChildrenState={payload.fetchEmbeddedChildrenState}
  279. toggleSpanGroup={toggleSpanGroup}
  280. numOfSpans={numOfSpans}
  281. groupType={groupType}
  282. groupOccurrence={payload.groupOccurrence}
  283. isEmbeddedTransactionTimeAdjusted={payload.isEmbeddedTransactionTimeAdjusted}
  284. onWheel={onWheel}
  285. generateContentSpanBarRef={generateContentSpanBarRef}
  286. markSpanOutOfView={markSpanOutOfView}
  287. markSpanInView={markSpanInView}
  288. storeSpanBar={storeSpanBar}
  289. />
  290. );
  291. acc.spanNumber = spanNumber + 1;
  292. return acc;
  293. },
  294. {
  295. numOfSpansOutOfViewAbove: 0,
  296. numOfFilteredSpansAbove: 0,
  297. spanTree: [],
  298. spanNumber: 1, // 1-based indexing
  299. }
  300. );
  301. const infoMessage = this.generateInfoMessage({
  302. isCurrentSpanHidden: false,
  303. numOfSpansOutOfViewAbove,
  304. isCurrentSpanFilteredOut: false,
  305. numOfFilteredSpansAbove,
  306. });
  307. return (
  308. <TraceViewContainer ref={this.props.traceViewRef}>
  309. {spanTree}
  310. {infoMessage}
  311. {this.generateLimitExceededMessage()}
  312. </TraceViewContainer>
  313. );
  314. }
  315. }
  316. const TraceViewContainer = styled('div')`
  317. overflow-x: hidden;
  318. border-bottom-left-radius: 3px;
  319. border-bottom-right-radius: 3px;
  320. `;
  321. /**
  322. * Checks if a trace contains all of its spans.
  323. *
  324. * The heuristic used here favors false negatives over false positives.
  325. * This is because showing a warning that the trace is not showing all
  326. * spans when it has them all is more misleading than not showing a
  327. * warning when it is missing some spans.
  328. *
  329. * A simple heuristic to determine when there are unrecorded spans
  330. *
  331. * 1. We assume if there are less than 999 spans, then we have all
  332. * the spans for a transaction. 999 was chosen because most SDKs
  333. * have a default limit of 1000 spans per transaction, but the
  334. * python SDK is 999 for historic reasons.
  335. *
  336. * 2. We assume that if there are unrecorded spans, they should be
  337. * at least 100ms in duration.
  338. *
  339. * While not perfect, this simple heuristic is unlikely to report
  340. * false positives.
  341. */
  342. function hasAllSpans(trace: ParsedTraceType): boolean {
  343. const {traceEndTimestamp, spans} = trace;
  344. if (spans.length < 999) {
  345. return true;
  346. }
  347. const lastSpan = spans.reduce((latest, span) =>
  348. latest.timestamp > span.timestamp ? latest : span
  349. );
  350. const missingDuration = traceEndTimestamp - lastSpan.timestamp;
  351. return missingDuration < 0.1;
  352. }
  353. export default withScrollbarManager(SpanTree);