123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931 |
- import {Component, createRef, useEffect, useRef} from 'react';
- import {
- AutoSizer,
- CellMeasurer,
- CellMeasurerCache,
- List as ReactVirtualizedList,
- ListRowProps,
- OverscanIndicesGetterParams,
- WindowScroller,
- } from 'react-virtualized';
- import styled from '@emotion/styled';
- import {withProfiler} from '@sentry/react';
- import differenceWith from 'lodash/differenceWith';
- import isEqual from 'lodash/isEqual';
- import throttle from 'lodash/throttle';
- import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
- import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
- import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
- import {t, tct} from 'sentry/locale';
- import {Organization} from 'sentry/types';
- import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
- import {setGroupedEntityTag} from 'sentry/utils/performanceForSentry';
- import {DragManagerChildrenProps} from './dragManager';
- import {ActiveOperationFilter} from './filter';
- import {ScrollbarManagerChildrenProps, withScrollbarManager} from './scrollbarManager';
- import {ProfiledSpanBar} from './spanBar';
- import * as SpanContext from './spanContext';
- import {SpanDescendantGroupBar} from './spanDescendantGroupBar';
- import SpanSiblingGroupBar from './spanSiblingGroupBar';
- import {
- EnhancedProcessedSpanType,
- EnhancedSpan,
- FilterSpans,
- GroupType,
- ParsedTraceType,
- SpanTreeNode,
- SpanTreeNodeType,
- SpanType,
- } from './types';
- import {getSpanID, getSpanOperation, isGapSpan, spanTargetHash} from './utils';
- import WaterfallModel from './waterfallModel';
- type PropType = ScrollbarManagerChildrenProps & {
- dragProps: DragManagerChildrenProps;
- filterSpans: FilterSpans | undefined;
- operationNameFilters: ActiveOperationFilter;
- organization: Organization;
- spanContextProps: SpanContext.SpanContextProps;
- spans: EnhancedProcessedSpanType[];
- traceViewHeaderRef: React.RefObject<HTMLDivElement>;
- traceViewRef: React.RefObject<HTMLDivElement>;
- waterfallModel: WaterfallModel;
- focusedSpanIds?: Set<string>;
- };
- type StateType = {
- headerPos: number;
- spanRows: Record<string, {spanRow: React.RefObject<HTMLDivElement>; treeDepth: number}>;
- };
- const listRef = createRef<ReactVirtualizedList>();
- class SpanTree extends Component<PropType> {
- state: StateType = {
- headerPos: 0,
- // Stores each visible span row ref along with its tree depth. This is used to calculate the
- // horizontal auto-scroll positioning
- spanRows: {},
- };
- componentDidMount() {
- setGroupedEntityTag('spans.total', 1000, this.props.spans.length);
- if (location.hash) {
- const {spans} = this.props;
- // This reducer searches for the index of the anchored span.
- // It's possible that one of the spans within an autogroup is anchored, so we need to search
- // for the index within the autogroupings as well.
- const {finalIndex, isIndexFound} = spans.reduce(
- (
- acc: {
- finalIndex: number;
- isIndexFound: boolean;
- },
- span,
- currIndex
- ) => {
- if ('type' in span.span || acc.isIndexFound) {
- return acc;
- }
- if (spanTargetHash(span.span.span_id) === location.hash) {
- acc.finalIndex = currIndex;
- acc.isIndexFound = true;
- }
- if (span.type === 'span_group_siblings') {
- if (!span.spanSiblingGrouping) {
- return acc;
- }
- const indexWithinGroup = span.spanSiblingGrouping.findIndex(
- s => spanTargetHash(s.span.span_id) === location.hash
- );
- if (indexWithinGroup === -1) {
- return acc;
- }
- acc.finalIndex = currIndex + indexWithinGroup;
- acc.isIndexFound = true;
- return acc;
- }
- if (span.type === 'span_group_chain') {
- if (!span.spanNestedGrouping) {
- return acc;
- }
- const indexWithinGroup = span.spanNestedGrouping.findIndex(
- s => spanTargetHash(s.span.span_id) === location.hash
- );
- if (indexWithinGroup === -1) {
- return acc;
- }
- acc.finalIndex = currIndex + indexWithinGroup;
- acc.isIndexFound = true;
- return acc;
- }
- return acc;
- },
- {finalIndex: -1, isIndexFound: false}
- );
- if (!isIndexFound) {
- return;
- }
- // This is just an estimate of where the anchored span is located. We can't get its precise location here, since
- // we need it to mount and use its boundingbox to determine that, but since the list is virtualized, there's no guarantee that
- // this span is mounted initially. The actual scroll positioning will be determined within the SpanBar instance that is anchored,
- // since this scroll estimation will allow that span to mount
- window.scrollTo(0, window.scrollY + ROW_HEIGHT * finalIndex);
- }
- }
- shouldComponentUpdate(nextProps: PropType) {
- if (
- this.props.dragProps.isDragging !== nextProps.dragProps.isDragging ||
- this.props.dragProps.isWindowSelectionDragging !==
- nextProps.dragProps.isWindowSelectionDragging
- ) {
- return true;
- }
- if (
- nextProps.dragProps.isDragging ||
- nextProps.dragProps.isWindowSelectionDragging ||
- isEqual(this.props.spans, nextProps.spans)
- ) {
- return false;
- }
- return true;
- }
- componentDidUpdate(prevProps: PropType) {
- // If the filters or minimap drag props have changed, we can't pinpoint the exact
- // spans that we need to recalculate the heights for, so recompute them all
- if (
- !isEqual(prevProps.filterSpans, this.props.filterSpans) ||
- !isEqual(prevProps.dragProps, this.props.dragProps) ||
- !isEqual(prevProps.operationNameFilters, this.props.operationNameFilters)
- ) {
- this.cache.clearAll();
- listRef.current?.recomputeRowHeights();
- return;
- }
- // When the spans change, we can be more efficient with recomputing heights.
- // Measuring cells is an expensive operation, so efficiency here is key.
- // We will look specifically at the cells that need to have their heights recalculated, and clear
- // their respective slots in the cache.
- if (prevProps.spans.length !== this.props.spans.length) {
- // If there are filters applied, it's difficult to find the exact positioning of the spans that
- // changed. It's easier to just clear the cache instead
- if (this.props.operationNameFilters) {
- this.cache.clearAll();
- listRef.current?.recomputeRowHeights();
- return;
- }
- // When the structure of the span tree is changed in an update, this can be due to the following reasons:
- // - A subtree was collapsed or expanded
- // - An autogroup was collapsed or expanded
- // - An embedded transaction was collapsed or expanded
- // Notice how in each case, there are two subcases: collapsing and expanding
- // In the collapse case, spans are *removed* from the tree, whereas expansion *adds* spans to the tree
- // We will have to determine which of these cases occurred, and then use that info to determine which specific cells
- // need to be recomputed.
- const comparator = (
- span1: EnhancedProcessedSpanType,
- span2: EnhancedProcessedSpanType
- ) => {
- if (isGapSpan(span1.span) || isGapSpan(span2.span)) {
- return isEqual(span1.span, span2.span);
- }
- return span1.span.span_id === span2.span.span_id;
- };
- // Case 1: Spans were removed due to a subtree or group collapsing
- if (prevProps.spans.length > this.props.spans.length) {
- // diffLeft will tell us all spans that have been removed in this update.
- const diffLeft = new Set(
- differenceWith(prevProps.spans, this.props.spans, comparator)
- );
- prevProps.spans.forEach((span, index) => {
- // We only want to clear the cache for spans that are expanded.
- if (this.props.spanContextProps.isSpanExpanded(span.span)) {
- this.cache.clear(index, 0);
- }
- });
- // This loop will ensure that any expanded spans after the spans which were removed
- // will have their cache slots cleared, since the new spans which will occupy those slots will not be expanded.
- this.props.spans.forEach(({span}, index) => {
- if (this.props.spanContextProps.isSpanExpanded(span)) {
- // Since spans were removed, the index in the new state is offset by the num of spans removed
- this.cache.clear(index + diffLeft.size, 0);
- }
- });
- }
- // Case 2: Spans were added due to a subtree or group expanding
- else {
- // diffRight will tell us all spans that have been added in this update.
- const diffRight = new Set(
- differenceWith(this.props.spans, prevProps.spans, comparator)
- );
- prevProps.spans.forEach(({span}, index) => {
- // We only want to clear the cache for spans that are added.
- if (this.props.spanContextProps.isSpanExpanded(span)) {
- this.cache.clear(index, 0);
- }
- });
- this.props.spans.forEach(({span}, index) => {
- if (this.props.spanContextProps.isSpanExpanded(span)) {
- this.cache.clear(index, 0);
- }
- });
- // This loop will ensure that any expanded spans after the spans which were added
- // will have their cache slots cleared, since the new spans which will occupy those slots will not be expanded.
- prevProps.spans.forEach((span, index) => {
- if (
- !diffRight.has(span) &&
- this.props.spanContextProps.isSpanExpanded(span.span)
- ) {
- // Since spans were removed, the index in the new state is offset by the num of spans removed
- this.cache.clear(index + diffRight.size, 0);
- }
- });
- }
- listRef.current?.forceUpdateGrid();
- }
- }
- cache = new CellMeasurerCache({
- fixedWidth: true,
- defaultHeight: ROW_HEIGHT,
- minHeight: ROW_HEIGHT,
- });
- generateInfoMessage(input: {
- filteredSpansAbove: EnhancedProcessedSpanType[];
- isCurrentSpanFilteredOut: boolean;
- isCurrentSpanHidden: boolean;
- outOfViewSpansAbove: EnhancedProcessedSpanType[];
- }): JSX.Element | null {
- const {
- isCurrentSpanHidden,
- outOfViewSpansAbove,
- isCurrentSpanFilteredOut,
- filteredSpansAbove,
- } = input;
- const {focusedSpanIds, waterfallModel, organization} = this.props;
- const messages: React.ReactNode[] = [];
- let firstHiddenSpanId = '0';
- const numOfSpansOutOfViewAbove = outOfViewSpansAbove.length;
- const showHiddenSpansMessage = !isCurrentSpanHidden && numOfSpansOutOfViewAbove > 0;
- if (showHiddenSpansMessage) {
- firstHiddenSpanId = getSpanID(outOfViewSpansAbove[0].span);
- messages.push(
- <span key={`spans-out-of-view-${firstHiddenSpanId}`}>
- <strong>{numOfSpansOutOfViewAbove}</strong> {t('spans out of view')}
- </span>
- );
- }
- const numOfFilteredSpansAbove = filteredSpansAbove.length;
- const showFilteredSpansMessage =
- !isCurrentSpanFilteredOut && numOfFilteredSpansAbove > 0;
- if (showFilteredSpansMessage) {
- firstHiddenSpanId = getSpanID(filteredSpansAbove[0].span);
- if (!isCurrentSpanHidden) {
- if (numOfFilteredSpansAbove === 1) {
- messages.push(
- <span key={`spans-filtered-${firstHiddenSpanId}`}>
- {tct('[numOfSpans] hidden span', {
- numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
- })}
- </span>
- );
- } else {
- messages.push(
- <span key={`spans-filtered-${firstHiddenSpanId}`}>
- {tct('[numOfSpans] hidden spans', {
- numOfSpans: <strong>{numOfFilteredSpansAbove}</strong>,
- })}
- </span>
- );
- }
- }
- }
- if (messages.length <= 0) {
- return null;
- }
- const isClickable = focusedSpanIds && showFilteredSpansMessage;
- return (
- <MessageRow
- key={`message-row-${firstHiddenSpanId}`}
- onClick={
- isClickable
- ? () => {
- trackAdvancedAnalyticsEvent(
- 'issue_details.performance.hidden_spans_expanded',
- {organization}
- );
- waterfallModel.expandHiddenSpans(filteredSpansAbove.slice(0));
- // We must clear the cache at this point, since the code in componentDidUpdate is unable to effectively
- // determine the specific cache slots to clear when hidden spans are expanded
- this.cache.clearAll();
- }
- : undefined
- }
- cursor={isClickable ? 'pointer' : 'default'}
- >
- {messages}
- </MessageRow>
- );
- }
- generateLimitExceededMessage() {
- const {waterfallModel} = this.props;
- const {parsedTrace} = waterfallModel;
- if (hasAllSpans(parsedTrace)) {
- return null;
- }
- return (
- <MessageRow>
- {t(
- 'The next spans are unavailable. You may have exceeded the span limit or need to address missing instrumentation.'
- )}
- </MessageRow>
- );
- }
- toggleSpanTree = (spanID: string) => () => {
- this.props.waterfallModel.toggleSpanSubTree(spanID);
- };
- generateSpanTree = () => {
- const {
- waterfallModel,
- spans,
- organization,
- dragProps,
- onWheel,
- addContentSpanBarRef,
- removeContentSpanBarRef,
- storeSpanBar,
- } = this.props;
- const generateBounds = waterfallModel.generateBounds({
- viewStart: dragProps.viewWindowStart,
- viewEnd: dragProps.viewWindowEnd,
- });
- type AccType = {
- filteredSpansAbove: EnhancedProcessedSpanType[];
- outOfViewSpansAbove: EnhancedProcessedSpanType[];
- spanNumber: number;
- spanTree: SpanTreeNode[];
- };
- const numOfSpans = spans.reduce((sum: number, payload: EnhancedProcessedSpanType) => {
- switch (payload.type) {
- case 'root_span':
- case 'span':
- case 'span_group_chain': {
- return sum + 1;
- }
- default: {
- return sum;
- }
- }
- }, 0);
- const isEmbeddedSpanTree = waterfallModel.isEmbeddedSpanTree;
- const {spanTree, outOfViewSpansAbove, filteredSpansAbove} = spans.reduce(
- (acc: AccType, payload: EnhancedProcessedSpanType, index: number) => {
- const {type} = payload;
- switch (payload.type) {
- case 'filtered_out': {
- acc.filteredSpansAbove.push(payload);
- return acc;
- }
- case 'out_of_view': {
- acc.outOfViewSpansAbove.push(payload);
- return acc;
- }
- default: {
- break;
- }
- }
- const previousSpanNotDisplayed =
- acc.filteredSpansAbove.length > 0 || acc.outOfViewSpansAbove.length > 0;
- if (previousSpanNotDisplayed) {
- const infoMessage = this.generateInfoMessage({
- isCurrentSpanHidden: false,
- filteredSpansAbove: acc.filteredSpansAbove,
- outOfViewSpansAbove: acc.outOfViewSpansAbove,
- isCurrentSpanFilteredOut: false,
- });
- if (infoMessage) {
- acc.spanTree.push({type: SpanTreeNodeType.MESSAGE, element: infoMessage});
- }
- }
- const spanNumber = acc.spanNumber;
- const {span, treeDepth, continuingTreeDepths} = payload;
- if (payload.type === 'span_group_chain') {
- const groupingContainsAffectedSpan =
- isEmbeddedSpanTree &&
- payload.spanNestedGrouping?.find(
- ({span: s}) =>
- !isGapSpan(s) && waterfallModel.affectedSpanIds?.includes(s.span_id)
- );
- acc.spanTree.push({
- type: SpanTreeNodeType.DESCENDANT_GROUP,
- props: {
- event: waterfallModel.event,
- span,
- spanBarType: groupingContainsAffectedSpan
- ? SpanBarType.AUTOGROUPED_AND_AFFECTED
- : SpanBarType.AUTOGROUPED,
- generateBounds,
- getCurrentLeftPos: this.props.getCurrentLeftPos,
- treeDepth,
- continuingTreeDepths,
- spanNumber,
- spanGrouping: payload.spanNestedGrouping as EnhancedSpan[],
- toggleSpanGroup: payload.toggleNestedSpanGroup as () => void,
- onWheel,
- addContentSpanBarRef,
- removeContentSpanBarRef,
- },
- });
- acc.spanNumber = spanNumber + 1;
- acc.outOfViewSpansAbove = [];
- acc.filteredSpansAbove = [];
- return acc;
- }
- if (payload.type === 'span_group_siblings') {
- const groupingContainsAffectedSpan =
- isEmbeddedSpanTree &&
- payload.spanSiblingGrouping?.find(
- ({span: s}) =>
- !isGapSpan(s) && waterfallModel.affectedSpanIds?.includes(s.span_id)
- );
- acc.spanTree.push({
- type: SpanTreeNodeType.SIBLING_GROUP,
- props: {
- event: waterfallModel.event,
- span,
- spanBarType: groupingContainsAffectedSpan
- ? SpanBarType.AUTOGROUPED_AND_AFFECTED
- : SpanBarType.AUTOGROUPED,
- generateBounds,
- getCurrentLeftPos: this.props.getCurrentLeftPos,
- treeDepth,
- continuingTreeDepths,
- spanNumber,
- spanGrouping: payload.spanSiblingGrouping as EnhancedSpan[],
- toggleSiblingSpanGroup: payload.toggleSiblingSpanGroup,
- isLastSibling: payload.isLastSibling ?? false,
- occurrence: payload.occurrence,
- onWheel,
- addContentSpanBarRef,
- removeContentSpanBarRef,
- isEmbeddedSpanTree,
- },
- });
- acc.spanNumber = spanNumber + 1;
- acc.outOfViewSpansAbove = [];
- acc.filteredSpansAbove = [];
- return acc;
- }
- const isLast = payload.isLastSibling;
- const isRoot = type === 'root_span';
- const spanBarColor: string = pickBarColor(getSpanOperation(span));
- const numOfSpanChildren = payload.numOfSpanChildren;
- acc.outOfViewSpansAbove = [];
- acc.filteredSpansAbove = [];
- let toggleSpanGroup: (() => void) | undefined = undefined;
- if (payload.type === 'span') {
- toggleSpanGroup = payload.toggleNestedSpanGroup;
- }
- let toggleSiblingSpanGroup:
- | ((span: SpanType, occurrence: number) => void)
- | undefined = undefined;
- if (payload.type === 'span' && payload.isFirstSiblingOfGroup) {
- toggleSiblingSpanGroup = payload.toggleSiblingSpanGroup;
- }
- let groupType;
- if (toggleSpanGroup) {
- groupType = GroupType.DESCENDANTS;
- } else if (toggleSiblingSpanGroup) {
- groupType = GroupType.SIBLINGS;
- }
- const isAffectedSpan =
- !('type' in span) &&
- isEmbeddedSpanTree &&
- waterfallModel.affectedSpanIds?.includes(span.span_id);
- let spanBarType: SpanBarType | undefined = undefined;
- if (type === 'gap') {
- spanBarType = SpanBarType.GAP;
- }
- if (isAffectedSpan) {
- spanBarType = SpanBarType.AFFECTED;
- }
- acc.spanTree.push({
- type: SpanTreeNodeType.SPAN,
- props: {
- organization,
- event: waterfallModel.event,
- spanBarColor,
- spanBarType,
- span,
- showSpanTree: !waterfallModel.hiddenSpanSubTrees.has(getSpanID(span)),
- numOfSpanChildren,
- trace: waterfallModel.parsedTrace,
- generateBounds,
- toggleSpanTree: this.toggleSpanTree(getSpanID(span)),
- treeDepth,
- continuingTreeDepths,
- spanNumber,
- isLast,
- isRoot,
- showEmbeddedChildren: payload.showEmbeddedChildren,
- toggleEmbeddedChildren: payload.toggleEmbeddedChildren,
- toggleSiblingSpanGroup,
- fetchEmbeddedChildrenState: payload.fetchEmbeddedChildrenState,
- toggleSpanGroup,
- numOfSpans,
- groupType,
- groupOccurrence: payload.groupOccurrence,
- isEmbeddedTransactionTimeAdjusted: payload.isEmbeddedTransactionTimeAdjusted,
- onWheel,
- addContentSpanBarRef,
- removeContentSpanBarRef,
- storeSpanBar,
- getCurrentLeftPos: this.props.getCurrentLeftPos,
- resetCellMeasureCache: () => this.cache.clear(index, 0),
- },
- });
- acc.spanNumber = spanNumber + 1;
- return acc;
- },
- {
- filteredSpansAbove: [],
- outOfViewSpansAbove: [],
- spanTree: [],
- spanNumber: 1, // 1-based indexing
- }
- );
- const infoMessage = this.generateInfoMessage({
- isCurrentSpanHidden: false,
- outOfViewSpansAbove,
- isCurrentSpanFilteredOut: false,
- filteredSpansAbove,
- });
- if (infoMessage) {
- spanTree.push({type: SpanTreeNodeType.MESSAGE, element: infoMessage});
- }
- return spanTree;
- };
- renderRow(props: ListRowProps, spanTree: SpanTreeNode[]) {
- return (
- <SpanRow
- {...props}
- spanTree={spanTree}
- spanContextProps={this.props.spanContextProps}
- cache={this.cache}
- addSpanRowToState={this.addSpanRowToState}
- removeSpanRowFromState={this.removeSpanRowFromState}
- />
- );
- }
- // Overscan is necessary to ensure a smooth horizontal autoscrolling experience.
- // This function will allow the spanTree to mount spans which are not immediately visible
- // in the view. If they are mounted too late, the horizontal autoscroll will look super glitchy
- overscanIndicesGetter(params: OverscanIndicesGetterParams) {
- const {startIndex, stopIndex, overscanCellsCount, cellCount} = params;
- return {
- overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
- };
- }
- addSpanRowToState = (
- spanId: string,
- spanRow: React.RefObject<HTMLDivElement>,
- treeDepth: number
- ) => {
- this.setState((prevState: StateType) => {
- const newSpanRows = {...prevState.spanRows};
- newSpanRows[spanId] = {spanRow, treeDepth};
- return {spanRows: newSpanRows};
- });
- };
- removeSpanRowFromState = (spanId: string) => {
- this.setState((prevState: StateType) => {
- const newSpanRows = {...prevState.spanRows};
- delete newSpanRows[spanId];
- return {spanRows: newSpanRows};
- });
- };
- isSpanRowVisible = (spanRow: React.RefObject<HTMLDivElement>) => {
- const {traceViewHeaderRef} = this.props;
- if (!spanRow.current || !traceViewHeaderRef.current) {
- return false;
- }
- const headerBottom = traceViewHeaderRef.current?.getBoundingClientRect().bottom;
- const viewportBottom = window.innerHeight || document.documentElement.clientHeight;
- const {bottom, top} = spanRow.current.getBoundingClientRect();
- // 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
- return bottom < viewportBottom && bottom > headerBottom && top > 0;
- };
- throttledOnScroll = throttle(
- () => {
- const spanRowsArray = Object.values(this.state.spanRows);
- const {depthSum, visibleSpanCount, isRootSpanVisible} = spanRowsArray.reduce(
- (acc, {spanRow, treeDepth}) => {
- if (!spanRow.current || !this.isSpanRowVisible(spanRow)) {
- return acc;
- }
- if (treeDepth === 0) {
- acc.isRootSpanVisible = true;
- }
- acc.depthSum += treeDepth;
- acc.visibleSpanCount += 1;
- return acc;
- },
- {
- depthSum: 0,
- visibleSpanCount: 0,
- isRootSpanVisible: false,
- }
- );
- // If the root is visible, we do not want to shift the view around so just pass 0 instead of the average
- const averageDepth =
- isRootSpanVisible || visibleSpanCount === 0
- ? 0
- : Math.round(depthSum / visibleSpanCount);
- this.props.updateHorizontalScrollState(averageDepth);
- },
- 500,
- {trailing: true}
- );
- render() {
- const spanTree = this.generateSpanTree();
- const infoMessage = spanTree[spanTree.length - 1];
- if (!infoMessage) {
- spanTree.pop();
- }
- const limitExceededMessage = this.generateLimitExceededMessage();
- limitExceededMessage &&
- spanTree.push({type: SpanTreeNodeType.MESSAGE, element: limitExceededMessage});
- return (
- <TraceViewContainer ref={this.props.traceViewRef}>
- <WindowScroller onScroll={this.throttledOnScroll}>
- {({height, isScrolling, onChildScroll, scrollTop}) => (
- <AutoSizer disableHeight>
- {({width}) => (
- <ReactVirtualizedList
- autoHeight
- isScrolling={isScrolling}
- onScroll={onChildScroll}
- scrollTop={scrollTop}
- deferredMeasurementCache={this.cache}
- height={height}
- width={width}
- rowHeight={this.cache.rowHeight}
- rowCount={spanTree.length}
- rowRenderer={props => this.renderRow(props, spanTree)}
- ref={listRef}
- />
- )}
- </AutoSizer>
- )}
- </WindowScroller>
- </TraceViewContainer>
- );
- }
- }
- type SpanRowProps = ListRowProps & {
- addSpanRowToState: (
- spanId: string,
- spanRow: React.RefObject<HTMLDivElement>,
- treeDepth: number
- ) => void;
- cache: CellMeasurerCache;
- removeSpanRowFromState: (spanId: string) => void;
- spanContextProps: SpanContext.SpanContextProps;
- spanTree: SpanTreeNode[];
- };
- function SpanRow(props: SpanRowProps) {
- const {
- index,
- parent,
- style,
- columnIndex,
- spanTree,
- cache,
- spanContextProps,
- addSpanRowToState,
- removeSpanRowFromState,
- } = props;
- const rowRef = useRef<HTMLDivElement>(null);
- const spanNode = spanTree[index];
- useEffect(() => {
- // Gap spans do not have IDs, so we can't really store them. This should not be a big deal, since
- // we only need to keep track of spans to calculate an average depth, a few missing spans will not
- // throw off the calculation too hard
- if (spanNode.type !== SpanTreeNodeType.MESSAGE && !isGapSpan(spanNode.props.span)) {
- addSpanRowToState(spanNode.props.span.span_id, rowRef, spanNode.props.treeDepth);
- }
- return () => {
- if (spanNode.type !== SpanTreeNodeType.MESSAGE && !isGapSpan(spanNode.props.span)) {
- removeSpanRowFromState(spanNode.props.span.span_id);
- }
- };
- }, [rowRef, spanNode, addSpanRowToState, removeSpanRowFromState]);
- const renderSpanNode = (
- node: SpanTreeNode,
- extraProps: {
- cellMeasurerCache: CellMeasurerCache;
- listRef: React.RefObject<ReactVirtualizedList>;
- measure: () => void;
- } & SpanContext.SpanContextProps
- ) => {
- switch (node.type) {
- case SpanTreeNodeType.SPAN:
- return (
- <ProfiledSpanBar
- key={getSpanID(node.props.span, `span-${node.props.spanNumber}`)}
- {...node.props}
- {...extraProps}
- />
- );
- case SpanTreeNodeType.DESCENDANT_GROUP:
- return (
- <SpanDescendantGroupBar
- key={`${node.props.spanNumber}-span-group`}
- {...node.props}
- didAnchoredSpanMount={extraProps.didAnchoredSpanMount}
- />
- );
- case SpanTreeNodeType.SIBLING_GROUP:
- return (
- <SpanSiblingGroupBar
- key={`${node.props.spanNumber}-span-sibling`}
- {...node.props}
- didAnchoredSpanMount={extraProps.didAnchoredSpanMount}
- />
- );
- case SpanTreeNodeType.MESSAGE:
- return node.element;
- default:
- return null;
- }
- };
- return (
- <CellMeasurer
- cache={cache}
- parent={parent}
- columnIndex={columnIndex}
- rowIndex={index}
- >
- {({measure}) => (
- <div style={style} ref={rowRef}>
- {renderSpanNode(spanNode, {
- measure,
- listRef,
- cellMeasurerCache: cache,
- ...spanContextProps,
- })}
- </div>
- )}
- </CellMeasurer>
- );
- }
- const TraceViewContainer = styled('div')`
- overflow-x: hidden;
- border-bottom-left-radius: 3px;
- border-bottom-right-radius: 3px;
- `;
- /**
- * Checks if a trace contains all of its spans.
- *
- * The heuristic used here favors false negatives over false positives.
- * This is because showing a warning that the trace is not showing all
- * spans when it has them all is more misleading than not showing a
- * warning when it is missing some spans.
- *
- * A simple heuristic to determine when there are unrecorded spans
- *
- * 1. We assume if there are less than 999 spans, then we have all
- * the spans for a transaction. 999 was chosen because most SDKs
- * have a default limit of 1000 spans per transaction, but the
- * python SDK is 999 for historic reasons.
- *
- * 2. We assume that if there are unrecorded spans, they should be
- * at least 100ms in duration.
- *
- * While not perfect, this simple heuristic is unlikely to report
- * false positives.
- */
- function hasAllSpans(trace: ParsedTraceType): boolean {
- const {traceEndTimestamp, spans} = trace;
- if (spans.length < 999) {
- return true;
- }
- const lastSpan = spans.reduce((latest, span) =>
- latest.timestamp > span.timestamp ? latest : span
- );
- const missingDuration = traceEndTimestamp - lastSpan.timestamp;
- return missingDuration < 0.1;
- }
- export default withProfiler(withScrollbarManager(SpanTree));
|