Compare commits

..

3 Commits

Author SHA1 Message Date
nikhilmantri0902
43509681fa chore: remove comments 2026-02-22 17:35:49 +05:30
nikhilmantri0902
ff5fcc0e98 chore: remove explicit URL decoding that causes crashes on K8s parameters 2026-02-21 11:21:37 +05:30
nikhilmantri0902
122d88c4d2 chore: fixed all failing sites where double decoding is happening in infra monitoring 2026-02-21 11:07:50 +05:30
16 changed files with 90 additions and 160 deletions

View File

@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -103,9 +103,10 @@ function K8sClustersList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -105,9 +105,10 @@ function K8sDaemonSetsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -106,9 +106,10 @@ function K8sDeploymentsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -101,9 +101,10 @@ function K8sJobsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -10,6 +10,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { safeParseJSON } from './commonUtils';
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
import { IEntityColumn } from './utils';
@@ -58,9 +59,10 @@ function K8sHeader({
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
const decoded = decodeURIComponent(urlFilters);
const parsed = JSON.parse(decoded);
filters = parsed;
const parsed = safeParseJSON<IBuilderQuery['filters']>(urlFilters);
if (parsed) {
filters = parsed;
}
}
return {
...currentQuery,

View File

@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -104,9 +104,10 @@ function K8sNamespacesList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -99,9 +99,10 @@ function K8sNodesList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -29,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -92,9 +92,10 @@ function K8sPodsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -105,9 +105,10 @@ function K8sStatefulSetsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -105,9 +105,10 @@ function K8sVolumesList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -5,6 +5,7 @@
/* eslint-disable prefer-destructuring */
import { useMemo } from 'react';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Table, Tooltip, Typography } from 'antd';
import { Progress } from 'antd/lib';
@@ -260,6 +261,19 @@ export const filterDuplicateFilters = (
return uniqueFilters;
};
export const safeParseJSON = <T,>(value: string): T | null => {
if (!value) {
return null;
}
try {
return JSON.parse(value) as T;
} catch (e) {
console.error('Error parsing JSON from URL parameter:', e);
// TODO: Should we capture this error in Sentry?
return null;
}
};
export const getOrderByFromParams = (
searchParams: URLSearchParams,
returnNullAsDefault = false,
@@ -271,9 +285,12 @@ export const getOrderByFromParams = (
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
);
if (orderByFromParams) {
const decoded = decodeURIComponent(orderByFromParams);
const parsed = JSON.parse(decoded);
return parsed as { columnName: string; order: 'asc' | 'desc' };
const parsed = safeParseJSON<{ columnName: string; order: 'asc' | 'desc' }>(
orderByFromParams,
);
if (parsed) {
return parsed;
}
}
if (returnNullAsDefault) {
return null;
@@ -287,13 +304,7 @@ export const getFiltersFromParams = (
): IBuilderQuery['filters'] | null => {
const filtersFromParams = searchParams.get(queryKey);
if (filtersFromParams) {
try {
const decoded = decodeURIComponent(filtersFromParams);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['filters'];
} catch (error) {
return null;
}
return safeParseJSON<IBuilderQuery['filters']>(filtersFromParams);
}
return null;
};

View File

@@ -15,7 +15,6 @@ import './TraceWaterfall.styles.scss';
export interface IInterestedSpan {
spanId: string;
isUncollapsed: boolean;
shouldScrollToSpan?: boolean;
}
interface ITraceWaterfallProps {
@@ -110,7 +109,6 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
setTraceFlamegraphStatsWidth={setTraceFlamegraphStatsWidth}
selectedSpan={selectedSpan}
setSelectedSpan={setSelectedSpan}
isFetchingTraceData={isFetchingTraceData}
/>
);
default:
@@ -119,7 +117,6 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
}, [
errorFetchingTraceData,
interestedSpanId,
isFetchingTraceData,
selectedSpan,
setInterestedSpanId,
setSelectedSpan,

View File

@@ -1,16 +1,3 @@
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spin {
animation: spin 1s linear infinite;
}
.success-content {
overflow-y: hidden;
overflow-x: hidden;

View File

@@ -30,7 +30,6 @@ import {
ChevronDown,
ChevronRight,
Leaf,
Loader2,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Span } from 'types/api/trace/getTraceV2';
@@ -44,20 +43,6 @@ import './Success.styles.scss';
const CONNECTOR_WIDTH = 28;
const VERTICAL_CONNECTOR_WIDTH = 1;
// Component to render chevron icon with spinner
function ChevronIcon({
isFetching,
isCollapsed,
}: {
isFetching: boolean;
isCollapsed: boolean;
}): JSX.Element {
if (isFetching) {
return <Loader2 size={14} className="spin" />;
}
return isCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />;
}
interface ITraceMetadata {
traceId: string;
startTime: number;
@@ -73,7 +58,6 @@ interface ISuccessProps {
setTraceFlamegraphStatsWidth: Dispatch<SetStateAction<number>>;
selectedSpan: Span | undefined;
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
isFetchingTraceData: boolean;
}
function SpanOverview({
@@ -86,8 +70,6 @@ function SpanOverview({
filteredSpanIds,
isFilterActive,
traceMetadata,
isFetchingTraceData,
interestedSpanId,
}: {
span: Span;
isSpanCollapsed: boolean;
@@ -98,8 +80,6 @@ function SpanOverview({
filteredSpanIds: string[];
isFilterActive: boolean;
traceMetadata: ITraceMetadata;
isFetchingTraceData: boolean;
interestedSpanId: IInterestedSpan;
}): JSX.Element {
const isRootSpan = span.level === 0;
const { hasEditPermission } = useAppContext();
@@ -165,12 +145,11 @@ function SpanOverview({
}}
className="collapse-uncollapse-button"
>
<ChevronIcon
isFetching={
isFetchingTraceData && interestedSpanId.spanId === span.spanId
}
isCollapsed={isSpanCollapsed}
/>
{isSpanCollapsed ? (
<ChevronRight size={14} />
) : (
<ChevronDown size={14} />
)}
<Typography.Text className="children-count">
{span.subTreeNodeCount}
</Typography.Text>
@@ -363,8 +342,6 @@ function getWaterfallColumns({
handleAddSpanToFunnel,
filteredSpanIds,
isFilterActive,
isFetchingTraceData,
interestedSpanId,
}: {
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
uncollapsedNodes: string[];
@@ -374,10 +351,8 @@ function getWaterfallColumns({
handleAddSpanToFunnel: (span: Span) => void;
filteredSpanIds: string[];
isFilterActive: boolean;
isFetchingTraceData: boolean;
interestedSpanId: IInterestedSpan;
}): ColumnDef<Span, string>[] {
const waterfallColumns: ColumnDef<Span, string>[] = [
}): ColumnDef<Span, any>[] {
const waterfallColumns: ColumnDef<Span, any>[] = [
columnDefHelper.display({
id: 'span-name',
header: '',
@@ -392,8 +367,6 @@ function getWaterfallColumns({
traceMetadata={traceMetadata}
filteredSpanIds={filteredSpanIds}
isFilterActive={isFilterActive}
isFetchingTraceData={isFetchingTraceData}
interestedSpanId={interestedSpanId}
/>
),
size: 450,
@@ -438,7 +411,6 @@ function Success(props: ISuccessProps): JSX.Element {
setTraceFlamegraphStatsWidth,
setSelectedSpan,
selectedSpan,
isFetchingTraceData,
} = props;
const [filteredSpanIds, setFilteredSpanIds] = useState<string[]>([]);
@@ -455,11 +427,7 @@ function Success(props: ISuccessProps): JSX.Element {
const handleCollapseUncollapse = useCallback(
(spanId: string, collapse: boolean) => {
setInterestedSpanId({
spanId,
isUncollapsed: !collapse,
shouldScrollToSpan: false,
});
setInterestedSpanId({ spanId, isUncollapsed: !collapse });
},
[setInterestedSpanId],
);
@@ -477,11 +445,7 @@ function Success(props: ISuccessProps): JSX.Element {
if (range?.startIndex === 0 && instance.isScrolling) {
// do not trigger for trace root as nothing to fetch above
if (spans[0].level !== 0) {
setInterestedSpanId({
spanId: spans[0].spanId,
isUncollapsed: false,
shouldScrollToSpan: false,
});
setInterestedSpanId({ spanId: spans[0].spanId, isUncollapsed: false });
}
return;
}
@@ -490,7 +454,6 @@ function Success(props: ISuccessProps): JSX.Element {
setInterestedSpanId({
spanId: spans[spans.length - 1].spanId,
isUncollapsed: false,
shouldScrollToSpan: false,
});
}
};
@@ -532,8 +495,6 @@ function Success(props: ISuccessProps): JSX.Element {
handleAddSpanToFunnel,
filteredSpanIds,
isFilterActive,
isFetchingTraceData,
interestedSpanId,
}),
[
handleCollapseUncollapse,
@@ -544,8 +505,6 @@ function Success(props: ISuccessProps): JSX.Element {
handleAddSpanToFunnel,
filteredSpanIds,
isFilterActive,
isFetchingTraceData,
interestedSpanId,
],
);
@@ -555,16 +514,12 @@ function Success(props: ISuccessProps): JSX.Element {
(span) => span.spanId === interestedSpanId.spanId,
);
if (idx !== -1) {
// Only scroll to center when navigating (URL/flamegraph), not on
// expand/collapse or virtualizer boundary fetches.
if (interestedSpanId.shouldScrollToSpan) {
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
}, 400);
}
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
}, 400);
setSelectedSpan(spans[idx]);
}

View File

@@ -30,7 +30,6 @@ function TraceDetailsV2(): JSX.Element {
() => ({
spanId: urlQuery.get('spanId') || '',
isUncollapsed: urlQuery.get('spanId') !== '',
shouldScrollToSpan: urlQuery.get('spanId') !== '',
}),
);
const [
@@ -44,7 +43,6 @@ function TraceDetailsV2(): JSX.Element {
setInterestedSpanId({
spanId: urlQuery.get('spanId') || '',
isUncollapsed: urlQuery.get('spanId') !== '',
shouldScrollToSpan: true,
});
}, [urlQuery]);

View File

@@ -9,7 +9,6 @@ import (
var (
SPAN_LIMIT_PER_REQUEST_FOR_WATERFALL float64 = 500
MAX_DEPTH_FOR_SELECTED_SPAN_CHILDREN int = 5
)
type Interval struct {
@@ -89,9 +88,8 @@ func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, un
return isPresentInSubtreeForTheNode, spansFromRootToNode
}
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string, depthFromSelectedSpan int, isSelectedSpanIDUnCollapsed bool) ([]*model.Span, []string) {
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string) []*model.Span {
preOrderTraversal := []*model.Span{}
autoExpandedSpans := []string{}
// sort the children to maintain the order across requests
sort.Slice(span.Children, func(i, j int) bool {
@@ -128,36 +126,15 @@ func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, is
preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
}
nextDepthFromSelectedSpan := -1
if span.SpanID == selectedSpanId && isSelectedSpanIDUnCollapsed {
nextDepthFromSelectedSpan = 1
} else if depthFromSelectedSpan >= 1 && depthFromSelectedSpan < MAX_DEPTH_FOR_SELECTED_SPAN_CHILDREN {
nextDepthFromSelectedSpan = depthFromSelectedSpan + 1
}
for index, child := range span.Children {
// A child is included in the pre-order output if its parent is uncollapsed
// OR if the child falls within MAX_DEPTH_FOR_SELECTED_SPAN_CHILDREN levels
// below the selected span.
isChildWithinMaxDepth := nextDepthFromSelectedSpan >= 1
isAlreadyUncollapsed := slices.Contains(uncollapsedSpans, span.SpanID)
childIsPartOfPreOrder := isPartOfPreOrder && (isAlreadyUncollapsed || isChildWithinMaxDepth)
if isPartOfPreOrder && isChildWithinMaxDepth && !isAlreadyUncollapsed {
if !slices.Contains(autoExpandedSpans, span.SpanID) {
autoExpandedSpans = append(autoExpandedSpans, span.SpanID)
}
}
_childTraversal, _autoExpanded := traverseTrace(child, uncollapsedSpans, level+1, childIsPartOfPreOrder, index != (len(span.Children)-1), selectedSpanId, nextDepthFromSelectedSpan, isSelectedSpanIDUnCollapsed)
_childTraversal := traverseTrace(child, uncollapsedSpans, level+1, isPartOfPreOrder && slices.Contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId)
preOrderTraversal = append(preOrderTraversal, _childTraversal...)
autoExpandedSpans = append(autoExpandedSpans, _autoExpanded...)
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
}
nodeWithoutChildren.SubTreeNodeCount += 1
return preOrderTraversal, autoExpandedSpans
return preOrderTraversal
}
@@ -191,13 +168,7 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
_, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID, updatedUncollapsedSpans, isSelectedSpanIDUnCollapsed)
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spansFromRootToNode...)
_preOrderTraversal, _autoExpanded := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID, -1, isSelectedSpanIDUnCollapsed)
// Merge auto-expanded spans into updatedUncollapsedSpans for returning in response
for _, spanID := range _autoExpanded {
if !slices.Contains(updatedUncollapsedSpans, spanID) {
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spanID)
}
}
_preOrderTraversal := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID)
_selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, selectedSpanID)
if _selectedSpanIndex != -1 {