spanTree.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. import {Component, createRef, useEffect, useRef} from 'react';
  2. import {
  3. AutoSizer,
  4. CellMeasurer,
  5. CellMeasurerCache,
  6. List as ReactVirtualizedList,
  7. ListRowProps,
  8. OverscanIndicesGetterParams,
  9. WindowScroller,
  10. } from 'react-virtualized';
  11. import styled from '@emotion/styled';
  12. import {withProfiler} from '@sentry/react';
  13. import differenceWith from 'lodash/differenceWith';
  14. import isEqual from 'lodash/isEqual';
  15. import throttle from 'lodash/throttle';
  16. import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
  17. import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
  18. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  19. import {t, tct} from 'sentry/locale';
  20. import {Organization} from 'sentry/types';
  21. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  22. import {setGroupedEntityTag} from 'sentry/utils/performanceForSentry';
  23. import {DragManagerChildrenProps} from './dragManager';
  24. import {ActiveOperationFilter} from './filter';
  25. import {ScrollbarManagerChildrenProps, withScrollbarManager} from './scrollbarManager';
  26. import {ProfiledSpanBar} from './spanBar';
  27. import * as SpanContext from './spanContext';
  28. import {SpanDescendantGroupBar} from './spanDescendantGroupBar';
  29. import SpanSiblingGroupBar from './spanSiblingGroupBar';
  30. import {
  31. EnhancedProcessedSpanType,
  32. EnhancedSpan,
  33. FilterSpans,
  34. GroupType,
  35. ParsedTraceType,
  36. SpanTreeNode,
  37. SpanTreeNodeType,
  38. SpanType,
  39. } from './types';
  40. import {getSpanID, getSpanOperation, isGapSpan, spanTargetHash} from './utils';
  41. import WaterfallModel from './waterfallModel';
  42. type PropType = ScrollbarManagerChildrenProps & {
  43. dragProps: DragManagerChildrenProps;
  44. filterSpans: FilterSpans | undefined;
  45. operationNameFilters: ActiveOperationFilter;
  46. organization: Organization;
  47. spanContextProps: SpanContext.SpanContextProps;
  48. spans: EnhancedProcessedSpanType[];
  49. traceViewHeaderRef: React.RefObject<HTMLDivElement>;
  50. traceViewRef: React.RefObject<HTMLDivElement>;
  51. waterfallModel: WaterfallModel;
  52. focusedSpanIds?: Set<string>;
  53. };
  54. type StateType = {
  55. headerPos: number;
  56. spanRows: Record<string, {spanRow: React.RefObject<HTMLDivElement>; treeDepth: number}>;
  57. };
  58. const listRef = createRef<ReactVirtualizedList>();
  59. class SpanTree extends Component<PropType> {
  60. state: StateType = {
  61. headerPos: 0,
  62. // Stores each visible span row ref along with its tree depth. This is used to calculate the
  63. // horizontal auto-scroll positioning
  64. spanRows: {},
  65. };
  66. componentDidMount() {
  67. setGroupedEntityTag('spans.total', 1000, this.props.spans.length);
  68. if (location.hash) {
  69. const {spans} = this.props;
  70. // This reducer searches for the index of the anchored span.
  71. // It's possible that one of the spans within an autogroup is anchored, so we need to search
  72. // for the index within the autogroupings as well.
  73. const {finalIndex, isIndexFound} = spans.reduce(
  74. (
  75. acc: {
  76. finalIndex: number;
  77. isIndexFound: boolean;
  78. },
  79. span,
  80. currIndex
  81. ) => {
  82. if ('type' in span.span || acc.isIndexFound) {
  83. return acc;
  84. }
  85. if (spanTargetHash(span.span.span_id) === location.hash) {
  86. acc.finalIndex = currIndex;
  87. acc.isIndexFound = true;
  88. }
  89. if (span.type === 'span_group_siblings') {
  90. if (!span.spanSiblingGrouping) {
  91. return acc;
  92. }
  93. const indexWithinGroup = span.spanSiblingGrouping.findIndex(
  94. s => spanTargetHash(s.span.span_id) === location.hash
  95. );
  96. if (indexWithinGroup === -1) {
  97. return acc;
  98. }
  99. acc.finalIndex = currIndex + indexWithinGroup;
  100. acc.isIndexFound = true;
  101. return acc;
  102. }
  103. if (span.type === 'span_group_chain') {
  104. if (!span.spanNestedGrouping) {
  105. return acc;
  106. }
  107. const indexWithinGroup = span.spanNestedGrouping.findIndex(
  108. s => spanTargetHash(s.span.span_id) === location.hash
  109. );
  110. if (indexWithinGroup === -1) {
  111. return acc;
  112. }
  113. acc.finalIndex = currIndex + indexWithinGroup;
  114. acc.isIndexFound = true;
  115. return acc;
  116. }
  117. return acc;
  118. },
  119. {finalIndex: -1, isIndexFound: false}
  120. );
  121. if (!isIndexFound) {
  122. return;
  123. }
  124. // This is just an estimate of where the anchored span is located. We can't get its precise location here, since
  125. // we need it to mount and use its boundingbox to determine that, but since the list is virtualized, there's no guarantee that
  126. // this span is mounted initially. The actual scroll positioning will be determined within the SpanBar instance that is anchored,
  127. // since this scroll estimation will allow that span to mount
  128. window.scrollTo(0, window.scrollY + ROW_HEIGHT * finalIndex);
  129. }
  130. }
  131. shouldComponentUpdate(nextProps: PropType) {
  132. if (
  133. this.props.dragProps.isDragging !== nextProps.dragProps.isDragging ||
  134. this.props.dragProps.isWindowSelectionDragging !==
  135. nextProps.dragProps.isWindowSelectionDragging
  136. ) {
  137. return true;
  138. }
  139. if (
  140. nextProps.dragProps.isDragging ||
  141. nextProps.dragProps.isWindowSelectionDragging ||
  142. isEqual(this.props.spans, nextProps.spans)
  143. ) {
  144. return false;
  145. }
  146. return true;
  147. }
  148. componentDidUpdate(prevProps: PropType) {
  149. // If the filters or minimap drag props have changed, we can't pinpoint the exact
  150. // spans that we need to recalculate the heights for, so recompute them all
  151. if (
  152. !isEqual(prevProps.filterSpans, this.props.filterSpans) ||
  153. !isEqual(prevProps.dragProps, this.props.dragProps) ||
  154. !isEqual(prevProps.operationNameFilters, this.props.operationNameFilters)
  155. ) {
  156. this.cache.clearAll();
  157. listRef.current?.recomputeRowHeights();
  158. return;
  159. }
  160. // When the spans change, we can be more efficient with recomputing heights.
  161. // Measuring cells is an expensive operation, so efficiency here is key.
  162. // We will look specifically at the cells that need to have their heights recalculated, and clear
  163. // their respective slots in the cache.
  164. if (prevProps.spans.length !== this.props.spans.length) {
  165. // If there are filters applied, it's difficult to find the exact positioning of the spans that
  166. // changed. It's easier to just clear the cache instead
  167. if (this.props.operationNameFilters) {
  168. this.cache.clearAll();
  169. listRef.current?.recomputeRowHeights();
  170. return;
  171. }
  172. // When the structure of the span tree is changed in an update, this can be due to the following reasons:
  173. // - A subtree was collapsed or expanded
  174. // - An autogroup was collapsed or expanded
  175. // - An embedded transaction was collapsed or expanded
  176. // Notice how in each case, there are two subcases: collapsing and expanding
  177. // In the collapse case, spans are *removed* from the tree, whereas expansion *adds* spans to the tree
  178. // We will have to determine which of these cases occurred, and then use that info to determine which specific cells
  179. // need to be recomputed.
  180. const comparator = (
  181. span1: EnhancedProcessedSpanType,
  182. span2: EnhancedProcessedSpanType
  183. ) => {
  184. if (isGapSpan(span1.span) || isGapSpan(span2.span)) {
  185. return isEqual(span1.span, span2.span);
  186. }
  187. return span1.span.span_id === span2.span.span_id;
  188. };
  189. // Case 1: Spans were removed due to a subtree or group collapsing
  190. if (prevProps.spans.length > this.props.spans.length) {
  191. // diffLeft will tell us all spans that have been removed in this update.
  192. const diffLeft = new Set(
  193. differenceWith(prevProps.spans, this.props.spans, comparator)
  194. );
  195. prevProps.spans.forEach((span, index) => {
  196. // We only want to clear the cache for spans that are expanded.
  197. if (this.props.spanContextProps.isSpanExpanded(span.span)) {
  198. this.cache.clear(index, 0);
  199. }
  200. });
  201. // This loop will ensure that any expanded spans after the spans which were removed
  202. // will have their cache slots cleared, since the new spans which will occupy those slots will not be expanded.
  203. this.props.spans.forEach(({span}, index) => {
  204. if (this.props.spanContextProps.isSpanExpanded(span)) {
  205. // Since spans were removed, the index in the new state is offset by the num of spans removed
  206. this.cache.clear(index + diffLeft.size, 0);
  207. }
  208. });
  209. }
  210. // Case 2: Spans were added due to a subtree or group expanding
  211. else {
  212. // diffRight will tell us all spans that have been added in this update.
  213. const diffRight = new Set(
  214. differenceWith(this.props.spans, prevProps.spans, comparator)
  215. );
  216. prevProps.spans.forEach(({span}, index) => {
  217. // We only want to clear the cache for spans that are added.
  218. if (this.props.spanContextProps.isSpanExpanded(span)) {
  219. this.cache.clear(index, 0);
  220. }
  221. });
  222. this.props.spans.forEach(({span}, index) => {
  223. if (this.props.spanContextProps.isSpanExpanded(span)) {
  224. this.cache.clear(index, 0);
  225. }
  226. });
  227. // This loop will ensure that any expanded spans after the spans which were added
  228. // will have their cache slots cleared, since the new spans which will occupy those slots will not be expanded.
  229. prevProps.spans.forEach((span, index) => {
  230. if (
  231. !diffRight.has(span) &&
  232. this.props.spanContextProps.isSpanExpanded(span.span)
  233. ) {
  234. // Since spans were removed, the index in the new state is offset by the num of spans removed
  235. this.cache.clear(index + diffRight.size, 0);
  236. }
  237. });
  238. }
  239. listRef.current?.forceUpdateGrid();
  240. }
  241. }
  242. cache = new CellMeasurerCache({
  243. fixedWidth: true,
  244. defaultHeight: ROW_HEIGHT,
  245. minHeight: ROW_HEIGHT,
  246. });
  247. generateInfoMessage(input: {
  248. filteredSpansAbove: EnhancedProcessedSpanType[];
  249. isCurrentSpanFilteredOut: boolean;
  250. isCurrentSpanHidden: boolean;
  251. outOfViewSpansAbove: EnhancedProcessedSpanType[];
  252. }): JSX.Element | null {
  253. const {
  254. isCurrentSpanHidden,
  255. outOfViewSpansAbove,
  256. isCurrentSpanFilteredOut,
  257. filteredSpansAbove,
  258. } = input;
  259. const {focusedSpanIds, waterfallModel, organization} = this.props;
  260. const messages: React.ReactNode[] = [];
  261. let firstHiddenSpanId = '0';
  262. const numOfSpansOutOfViewAbove = outOfViewSpansAbove.length;
  263. const showHiddenSpansMessage = !isCurrentSpanHidden && numOfSpansOutOfViewAbove > 0;
  264. if (showHiddenSpansMessage) {
  265. firstHiddenSpanId = getSpanID(outOfViewSpansAbove[0].span);
  266. messages.push(
  267. <span key={`spans-out-of-view-${firstHiddenSpanId}`}>
  268. <strong>{numOfSpansOutOfViewAbove}</strong> {t('spans out of view')}
  269. </span>
  270. );
  271. }
  272. const numOfFilteredSpansAbove = filteredSpansAbove.length;
  273. const showFilteredSpansMessage =
  274. !isCurrentSpanFilteredOut && numOfFilteredSpansAbove > 0;
  275. if (showFilteredSpansMessage) {
  276. firstHiddenSpanId = getSpanID(filteredSpansAbove[0].span);
  277. if (!isCurrentSpanHidden) {
  278. if (numOfFilteredSpansAbove === 1) {
  279. messages.push(
  280. <span key={`spans-filtered-${firstHiddenSpanId}`}>
  281. {tct('[numOfSpans] hidden span', {
  282. numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
  283. })}
  284. </span>
  285. );
  286. } else {
  287. messages.push(
  288. <span key={`spans-filtered-${firstHiddenSpanId}`}>
  289. {tct('[numOfSpans] hidden spans', {
  290. numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
  291. })}
  292. </span>
  293. );
  294. }
  295. }
  296. }
  297. if (messages.length <= 0) {
  298. return null;
  299. }
  300. const isClickable = focusedSpanIds && showFilteredSpansMessage;
  301. return (
  302. <MessageRow
  303. key={`message-row-${firstHiddenSpanId}`}
  304. onClick={
  305. isClickable
  306. ? () => {
  307. trackAdvancedAnalyticsEvent(
  308. 'issue_details.performance.hidden_spans_expanded',
  309. {organization}
  310. );
  311. waterfallModel.expandHiddenSpans(filteredSpansAbove.slice(0));
  312. // We must clear the cache at this point, since the code in componentDidUpdate is unable to effectively
  313. // determine the specific cache slots to clear when hidden spans are expanded
  314. this.cache.clearAll();
  315. }
  316. : undefined
  317. }
  318. cursor={isClickable ? 'pointer' : 'default'}
  319. >
  320. {messages}
  321. </MessageRow>
  322. );
  323. }
  324. generateLimitExceededMessage() {
  325. const {waterfallModel} = this.props;
  326. const {parsedTrace} = waterfallModel;
  327. if (hasAllSpans(parsedTrace)) {
  328. return null;
  329. }
  330. return (
  331. <MessageRow>
  332. {t(
  333. 'The next spans are unavailable. You may have exceeded the span limit or need to address missing instrumentation.'
  334. )}
  335. </MessageRow>
  336. );
  337. }
  338. toggleSpanTree = (spanID: string) => () => {
  339. this.props.waterfallModel.toggleSpanSubTree(spanID);
  340. };
  341. generateSpanTree = () => {
  342. const {
  343. waterfallModel,
  344. spans,
  345. organization,
  346. dragProps,
  347. onWheel,
  348. addContentSpanBarRef,
  349. removeContentSpanBarRef,
  350. storeSpanBar,
  351. } = this.props;
  352. const generateBounds = waterfallModel.generateBounds({
  353. viewStart: dragProps.viewWindowStart,
  354. viewEnd: dragProps.viewWindowEnd,
  355. });
  356. type AccType = {
  357. filteredSpansAbove: EnhancedProcessedSpanType[];
  358. outOfViewSpansAbove: EnhancedProcessedSpanType[];
  359. spanNumber: number;
  360. spanTree: SpanTreeNode[];
  361. };
  362. const numOfSpans = spans.reduce((sum: number, payload: EnhancedProcessedSpanType) => {
  363. switch (payload.type) {
  364. case 'root_span':
  365. case 'span':
  366. case 'span_group_chain': {
  367. return sum + 1;
  368. }
  369. default: {
  370. return sum;
  371. }
  372. }
  373. }, 0);
  374. const isEmbeddedSpanTree = waterfallModel.isEmbeddedSpanTree;
  375. const {spanTree, outOfViewSpansAbove, filteredSpansAbove} = spans.reduce(
  376. (acc: AccType, payload: EnhancedProcessedSpanType, index: number) => {
  377. const {type} = payload;
  378. switch (payload.type) {
  379. case 'filtered_out': {
  380. acc.filteredSpansAbove.push(payload);
  381. return acc;
  382. }
  383. case 'out_of_view': {
  384. acc.outOfViewSpansAbove.push(payload);
  385. return acc;
  386. }
  387. default: {
  388. break;
  389. }
  390. }
  391. const previousSpanNotDisplayed =
  392. acc.filteredSpansAbove.length > 0 || acc.outOfViewSpansAbove.length > 0;
  393. if (previousSpanNotDisplayed) {
  394. const infoMessage = this.generateInfoMessage({
  395. isCurrentSpanHidden: false,
  396. filteredSpansAbove: acc.filteredSpansAbove,
  397. outOfViewSpansAbove: acc.outOfViewSpansAbove,
  398. isCurrentSpanFilteredOut: false,
  399. });
  400. if (infoMessage) {
  401. acc.spanTree.push({type: SpanTreeNodeType.MESSAGE, element: infoMessage});
  402. }
  403. }
  404. const spanNumber = acc.spanNumber;
  405. const {span, treeDepth, continuingTreeDepths} = payload;
  406. if (payload.type === 'span_group_chain') {
  407. const groupingContainsAffectedSpan =
  408. isEmbeddedSpanTree &&
  409. payload.spanNestedGrouping?.find(
  410. ({span: s}) =>
  411. !isGapSpan(s) && waterfallModel.affectedSpanIds?.includes(s.span_id)
  412. );
  413. acc.spanTree.push({
  414. type: SpanTreeNodeType.DESCENDANT_GROUP,
  415. props: {
  416. event: waterfallModel.event,
  417. span,
  418. spanBarType: groupingContainsAffectedSpan
  419. ? SpanBarType.AUTOGROUPED_AND_AFFECTED
  420. : SpanBarType.AUTOGROUPED,
  421. generateBounds,
  422. getCurrentLeftPos: this.props.getCurrentLeftPos,
  423. treeDepth,
  424. continuingTreeDepths,
  425. spanNumber,
  426. spanGrouping: payload.spanNestedGrouping as EnhancedSpan[],
  427. toggleSpanGroup: payload.toggleNestedSpanGroup as () => void,
  428. onWheel,
  429. addContentSpanBarRef,
  430. removeContentSpanBarRef,
  431. },
  432. });
  433. acc.spanNumber = spanNumber + 1;
  434. acc.outOfViewSpansAbove = [];
  435. acc.filteredSpansAbove = [];
  436. return acc;
  437. }
  438. if (payload.type === 'span_group_siblings') {
  439. const groupingContainsAffectedSpan =
  440. isEmbeddedSpanTree &&
  441. payload.spanSiblingGrouping?.find(
  442. ({span: s}) =>
  443. !isGapSpan(s) && waterfallModel.affectedSpanIds?.includes(s.span_id)
  444. );
  445. acc.spanTree.push({
  446. type: SpanTreeNodeType.SIBLING_GROUP,
  447. props: {
  448. event: waterfallModel.event,
  449. span,
  450. spanBarType: groupingContainsAffectedSpan
  451. ? SpanBarType.AUTOGROUPED_AND_AFFECTED
  452. : SpanBarType.AUTOGROUPED,
  453. generateBounds,
  454. getCurrentLeftPos: this.props.getCurrentLeftPos,
  455. treeDepth,
  456. continuingTreeDepths,
  457. spanNumber,
  458. spanGrouping: payload.spanSiblingGrouping as EnhancedSpan[],
  459. toggleSiblingSpanGroup: payload.toggleSiblingSpanGroup,
  460. isLastSibling: payload.isLastSibling ?? false,
  461. occurrence: payload.occurrence,
  462. onWheel,
  463. addContentSpanBarRef,
  464. removeContentSpanBarRef,
  465. isEmbeddedSpanTree,
  466. },
  467. });
  468. acc.spanNumber = spanNumber + 1;
  469. acc.outOfViewSpansAbove = [];
  470. acc.filteredSpansAbove = [];
  471. return acc;
  472. }
  473. const isLast = payload.isLastSibling;
  474. const isRoot = type === 'root_span';
  475. const spanBarColor: string = pickBarColor(getSpanOperation(span));
  476. const numOfSpanChildren = payload.numOfSpanChildren;
  477. acc.outOfViewSpansAbove = [];
  478. acc.filteredSpansAbove = [];
  479. let toggleSpanGroup: (() => void) | undefined = undefined;
  480. if (payload.type === 'span') {
  481. toggleSpanGroup = payload.toggleNestedSpanGroup;
  482. }
  483. let toggleSiblingSpanGroup:
  484. | ((span: SpanType, occurrence: number) => void)
  485. | undefined = undefined;
  486. if (payload.type === 'span' && payload.isFirstSiblingOfGroup) {
  487. toggleSiblingSpanGroup = payload.toggleSiblingSpanGroup;
  488. }
  489. let groupType;
  490. if (toggleSpanGroup) {
  491. groupType = GroupType.DESCENDANTS;
  492. } else if (toggleSiblingSpanGroup) {
  493. groupType = GroupType.SIBLINGS;
  494. }
  495. const isAffectedSpan =
  496. !('type' in span) &&
  497. isEmbeddedSpanTree &&
  498. waterfallModel.affectedSpanIds?.includes(span.span_id);
  499. let spanBarType: SpanBarType | undefined = undefined;
  500. if (type === 'gap') {
  501. spanBarType = SpanBarType.GAP;
  502. }
  503. if (isAffectedSpan) {
  504. spanBarType = SpanBarType.AFFECTED;
  505. }
  506. acc.spanTree.push({
  507. type: SpanTreeNodeType.SPAN,
  508. props: {
  509. organization,
  510. event: waterfallModel.event,
  511. spanBarColor,
  512. spanBarType,
  513. span,
  514. showSpanTree: !waterfallModel.hiddenSpanSubTrees.has(getSpanID(span)),
  515. numOfSpanChildren,
  516. trace: waterfallModel.parsedTrace,
  517. generateBounds,
  518. toggleSpanTree: this.toggleSpanTree(getSpanID(span)),
  519. treeDepth,
  520. continuingTreeDepths,
  521. spanNumber,
  522. isLast,
  523. isRoot,
  524. showEmbeddedChildren: payload.showEmbeddedChildren,
  525. toggleEmbeddedChildren: payload.toggleEmbeddedChildren,
  526. toggleSiblingSpanGroup,
  527. fetchEmbeddedChildrenState: payload.fetchEmbeddedChildrenState,
  528. toggleSpanGroup,
  529. numOfSpans,
  530. groupType,
  531. groupOccurrence: payload.groupOccurrence,
  532. isEmbeddedTransactionTimeAdjusted: payload.isEmbeddedTransactionTimeAdjusted,
  533. onWheel,
  534. addContentSpanBarRef,
  535. removeContentSpanBarRef,
  536. storeSpanBar,
  537. getCurrentLeftPos: this.props.getCurrentLeftPos,
  538. resetCellMeasureCache: () => this.cache.clear(index, 0),
  539. },
  540. });
  541. acc.spanNumber = spanNumber + 1;
  542. return acc;
  543. },
  544. {
  545. filteredSpansAbove: [],
  546. outOfViewSpansAbove: [],
  547. spanTree: [],
  548. spanNumber: 1, // 1-based indexing
  549. }
  550. );
  551. const infoMessage = this.generateInfoMessage({
  552. isCurrentSpanHidden: false,
  553. outOfViewSpansAbove,
  554. isCurrentSpanFilteredOut: false,
  555. filteredSpansAbove,
  556. });
  557. if (infoMessage) {
  558. spanTree.push({type: SpanTreeNodeType.MESSAGE, element: infoMessage});
  559. }
  560. return spanTree;
  561. };
  562. renderRow(props: ListRowProps, spanTree: SpanTreeNode[]) {
  563. return (
  564. <SpanRow
  565. {...props}
  566. spanTree={spanTree}
  567. spanContextProps={this.props.spanContextProps}
  568. cache={this.cache}
  569. addSpanRowToState={this.addSpanRowToState}
  570. removeSpanRowFromState={this.removeSpanRowFromState}
  571. />
  572. );
  573. }
  574. // Overscan is necessary to ensure a smooth horizontal autoscrolling experience.
  575. // This function will allow the spanTree to mount spans which are not immediately visible
  576. // in the view. If they are mounted too late, the horizontal autoscroll will look super glitchy
  577. overscanIndicesGetter(params: OverscanIndicesGetterParams) {
  578. const {startIndex, stopIndex, overscanCellsCount, cellCount} = params;
  579. return {
  580. overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
  581. overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
  582. };
  583. }
  584. addSpanRowToState = (
  585. spanId: string,
  586. spanRow: React.RefObject<HTMLDivElement>,
  587. treeDepth: number
  588. ) => {
  589. this.setState((prevState: StateType) => {
  590. const newSpanRows = {...prevState.spanRows};
  591. newSpanRows[spanId] = {spanRow, treeDepth};
  592. return {spanRows: newSpanRows};
  593. });
  594. };
  595. removeSpanRowFromState = (spanId: string) => {
  596. this.setState((prevState: StateType) => {
  597. const newSpanRows = {...prevState.spanRows};
  598. delete newSpanRows[spanId];
  599. return {spanRows: newSpanRows};
  600. });
  601. };
  602. isSpanRowVisible = (spanRow: React.RefObject<HTMLDivElement>) => {
  603. const {traceViewHeaderRef} = this.props;
  604. if (!spanRow.current || !traceViewHeaderRef.current) {
  605. return false;
  606. }
  607. const headerBottom = traceViewHeaderRef.current?.getBoundingClientRect().bottom;
  608. const viewportBottom = window.innerHeight || document.documentElement.clientHeight;
  609. const {bottom, top} = spanRow.current.getBoundingClientRect();
  610. // We determine if a span row is visible if it is above the viewport bottom boundary, below the header, and also below the top of the viewport
  611. return bottom < viewportBottom && bottom > headerBottom && top > 0;
  612. };
  613. throttledOnScroll = throttle(
  614. () => {
  615. const spanRowsArray = Object.values(this.state.spanRows);
  616. const {depthSum, visibleSpanCount, isRootSpanVisible} = spanRowsArray.reduce(
  617. (acc, {spanRow, treeDepth}) => {
  618. if (!spanRow.current || !this.isSpanRowVisible(spanRow)) {
  619. return acc;
  620. }
  621. if (treeDepth === 0) {
  622. acc.isRootSpanVisible = true;
  623. }
  624. acc.depthSum += treeDepth;
  625. acc.visibleSpanCount += 1;
  626. return acc;
  627. },
  628. {
  629. depthSum: 0,
  630. visibleSpanCount: 0,
  631. isRootSpanVisible: false,
  632. }
  633. );
  634. // If the root is visible, we do not want to shift the view around so just pass 0 instead of the average
  635. const averageDepth =
  636. isRootSpanVisible || visibleSpanCount === 0
  637. ? 0
  638. : Math.round(depthSum / visibleSpanCount);
  639. this.props.updateHorizontalScrollState(averageDepth);
  640. },
  641. 500,
  642. {trailing: true}
  643. );
  644. render() {
  645. const spanTree = this.generateSpanTree();
  646. const infoMessage = spanTree[spanTree.length - 1];
  647. if (!infoMessage) {
  648. spanTree.pop();
  649. }
  650. const limitExceededMessage = this.generateLimitExceededMessage();
  651. limitExceededMessage &&
  652. spanTree.push({type: SpanTreeNodeType.MESSAGE, element: limitExceededMessage});
  653. return (
  654. <TraceViewContainer ref={this.props.traceViewRef}>
  655. <WindowScroller onScroll={this.throttledOnScroll}>
  656. {({height, isScrolling, onChildScroll, scrollTop}) => (
  657. <AutoSizer disableHeight>
  658. {({width}) => (
  659. <ReactVirtualizedList
  660. autoHeight
  661. isScrolling={isScrolling}
  662. onScroll={onChildScroll}
  663. scrollTop={scrollTop}
  664. deferredMeasurementCache={this.cache}
  665. height={height}
  666. width={width}
  667. rowHeight={this.cache.rowHeight}
  668. rowCount={spanTree.length}
  669. rowRenderer={props => this.renderRow(props, spanTree)}
  670. ref={listRef}
  671. />
  672. )}
  673. </AutoSizer>
  674. )}
  675. </WindowScroller>
  676. </TraceViewContainer>
  677. );
  678. }
  679. }
  680. type SpanRowProps = ListRowProps & {
  681. addSpanRowToState: (
  682. spanId: string,
  683. spanRow: React.RefObject<HTMLDivElement>,
  684. treeDepth: number
  685. ) => void;
  686. cache: CellMeasurerCache;
  687. removeSpanRowFromState: (spanId: string) => void;
  688. spanContextProps: SpanContext.SpanContextProps;
  689. spanTree: SpanTreeNode[];
  690. };
  691. function SpanRow(props: SpanRowProps) {
  692. const {
  693. index,
  694. parent,
  695. style,
  696. columnIndex,
  697. spanTree,
  698. cache,
  699. spanContextProps,
  700. addSpanRowToState,
  701. removeSpanRowFromState,
  702. } = props;
  703. const rowRef = useRef<HTMLDivElement>(null);
  704. const spanNode = spanTree[index];
  705. useEffect(() => {
  706. // Gap spans do not have IDs, so we can't really store them. This should not be a big deal, since
  707. // we only need to keep track of spans to calculate an average depth, a few missing spans will not
  708. // throw off the calculation too hard
  709. if (spanNode.type !== SpanTreeNodeType.MESSAGE && !isGapSpan(spanNode.props.span)) {
  710. addSpanRowToState(spanNode.props.span.span_id, rowRef, spanNode.props.treeDepth);
  711. }
  712. return () => {
  713. if (spanNode.type !== SpanTreeNodeType.MESSAGE && !isGapSpan(spanNode.props.span)) {
  714. removeSpanRowFromState(spanNode.props.span.span_id);
  715. }
  716. };
  717. }, [rowRef, spanNode, addSpanRowToState, removeSpanRowFromState]);
  718. const renderSpanNode = (
  719. node: SpanTreeNode,
  720. extraProps: {
  721. cellMeasurerCache: CellMeasurerCache;
  722. listRef: React.RefObject<ReactVirtualizedList>;
  723. measure: () => void;
  724. } & SpanContext.SpanContextProps
  725. ) => {
  726. switch (node.type) {
  727. case SpanTreeNodeType.SPAN:
  728. return (
  729. <ProfiledSpanBar
  730. key={getSpanID(node.props.span, `span-${node.props.spanNumber}`)}
  731. {...node.props}
  732. {...extraProps}
  733. />
  734. );
  735. case SpanTreeNodeType.DESCENDANT_GROUP:
  736. return (
  737. <SpanDescendantGroupBar
  738. key={`${node.props.spanNumber}-span-group`}
  739. {...node.props}
  740. didAnchoredSpanMount={extraProps.didAnchoredSpanMount}
  741. />
  742. );
  743. case SpanTreeNodeType.SIBLING_GROUP:
  744. return (
  745. <SpanSiblingGroupBar
  746. key={`${node.props.spanNumber}-span-sibling`}
  747. {...node.props}
  748. didAnchoredSpanMount={extraProps.didAnchoredSpanMount}
  749. />
  750. );
  751. case SpanTreeNodeType.MESSAGE:
  752. return node.element;
  753. default:
  754. return null;
  755. }
  756. };
  757. return (
  758. <CellMeasurer
  759. cache={cache}
  760. parent={parent}
  761. columnIndex={columnIndex}
  762. rowIndex={index}
  763. >
  764. {({measure}) => (
  765. <div style={style} ref={rowRef}>
  766. {renderSpanNode(spanNode, {
  767. measure,
  768. listRef,
  769. cellMeasurerCache: cache,
  770. ...spanContextProps,
  771. })}
  772. </div>
  773. )}
  774. </CellMeasurer>
  775. );
  776. }
  777. const TraceViewContainer = styled('div')`
  778. overflow-x: hidden;
  779. border-bottom-left-radius: 3px;
  780. border-bottom-right-radius: 3px;
  781. `;
  782. /**
  783. * Checks if a trace contains all of its spans.
  784. *
  785. * The heuristic used here favors false negatives over false positives.
  786. * This is because showing a warning that the trace is not showing all
  787. * spans when it has them all is more misleading than not showing a
  788. * warning when it is missing some spans.
  789. *
  790. * A simple heuristic to determine when there are unrecorded spans
  791. *
  792. * 1. We assume if there are less than 999 spans, then we have all
  793. * the spans for a transaction. 999 was chosen because most SDKs
  794. * have a default limit of 1000 spans per transaction, but the
  795. * python SDK is 999 for historic reasons.
  796. *
  797. * 2. We assume that if there are unrecorded spans, they should be
  798. * at least 100ms in duration.
  799. *
  800. * While not perfect, this simple heuristic is unlikely to report
  801. * false positives.
  802. */
  803. function hasAllSpans(trace: ParsedTraceType): boolean {
  804. const {traceEndTimestamp, spans} = trace;
  805. if (spans.length < 999) {
  806. return true;
  807. }
  808. const lastSpan = spans.reduce((latest, span) =>
  809. latest.timestamp > span.timestamp ? latest : span
  810. );
  811. const missingDuration = traceEndTimestamp - lastSpan.timestamp;
  812. return missingDuration < 0.1;
  813. }
  814. export default withProfiler(withScrollbarManager(SpanTree));