import type React from 'react'; import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {browserHistory} from 'react-router'; import {type Theme, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import {PlatformIcon} from 'platformicons'; import * as qs from 'query-string'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Placeholder from 'sentry/components/placeholder'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization, PlatformKey, Project} from 'sentry/types'; import type { TraceError, TracePerformanceIssue, } from 'sentry/utils/performance/quickTrace/types'; import {clamp} from 'sentry/utils/profiling/colors/utils'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import { getRovingIndexActionFromEvent, type RovingTabIndexAction, type RovingTabIndexUserActions, } from 'sentry/views/performance/newTraceDetails/rovingTabIndex'; import type { TraceSearchAction, TraceSearchState, } from 'sentry/views/performance/newTraceDetails/traceSearch/traceSearch'; import { isAutogroupedNode, isMissingInstrumentationNode, isNoDataNode, isParentAutogroupedNode, isSpanNode, isTraceErrorNode, isTraceNode, isTransactionNode, } from './guards'; import { makeTraceNodeBarColor, ParentAutogroupNode, type TraceTree, type TraceTreeNode, } from './traceTree'; import { useVirtualizedList, type VirtualizedRow, type VirtualizedViewManager, } from './virtualizedViewManager'; function Chevron(props: {direction: 'up' | 'down' | 'left'}) { return ( ); } function Fire() { return ( ); } function Profile() { return ( ); } function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null { if (Array.isArray(maybePath)) { return maybePath; } if (typeof maybePath === 'string') { return [maybePath as TraceTree.NodePath]; } return null; } const format = (v: number, abbrev: string, precision: number) => { if (v === 0) { return '0' + abbrev; } return v.toFixed(precision) + abbrev; }; function getDuration(duration_ms: number) { if (duration_ms >= 24 * 60 * 60 * 1e3) { return format((duration_ms / 24) * 60 * 60e3, 'd', 2); } if (duration_ms >= 60 * 60 * 1e3) { return format((duration_ms / 60) * 60e3, 'h', 2); } if (duration_ms >= 60 * 1e3) { return format(duration_ms / 60e3, 'min', 2); } if (duration_ms >= 1e3) { return format(duration_ms / 1e3, 's', 2); } return format(duration_ms, 'ms', 2); } const COUNT_FORMATTER = Intl.NumberFormat(undefined, {notation: 'compact'}); const NO_ERRORS = new Set(); const NO_PERFORMANCE_ISSUES = new Set(); const NO_PROFILES = []; interface RovingTabIndexState { index: number | null; items: number | null; node: TraceTreeNode | null; } function computeNextIndexFromAction( current_index: number, action: RovingTabIndexUserActions, items: number ): number { switch (action) { case 'next': if (current_index === items) { return 0; } return current_index + 1; case 'previous': if (current_index === 0) { return items; } return current_index - 1; case 'last': return items; case 'first': return 0; default: throw new TypeError(`Invalid or not implemented reducer action - ${action}`); } } const RIGHT_COLUMN_EVEN_CLASSNAME = `TraceRightColumn`; const RIGHT_COLUMN_ODD_CLASSNAME = [RIGHT_COLUMN_EVEN_CLASSNAME, 'Odd'].join(' '); const CHILDREN_COUNT_WRAPPER_CLASSNAME = `TraceChildrenCountWrapper`; const CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME = [ CHILDREN_COUNT_WRAPPER_CLASSNAME, 'Orphaned', ].join(' '); function maybeFocusRow( ref: HTMLDivElement | null, node: TraceTreeNode, previouslyFocusedNodeRef: React.MutableRefObject | null> ) { if (!ref) return; if (node === previouslyFocusedNodeRef.current) return; ref.focus(); previouslyFocusedNodeRef.current = node; } interface TraceProps { forceRerender: number; manager: VirtualizedViewManager; onRowClick: ( node: TraceTreeNode | null, event: React.MouseEvent | null ) => void; onTraceSearch: ( tree: TraceTree, query: string, node: TraceTreeNode | null ) => void; previouslyFocusedNodeRef: React.MutableRefObject | null>; rerender: () => void; roving_dispatch: React.Dispatch; roving_state: RovingTabIndexState; scrollQueueRef: React.MutableRefObject<{ eventId?: string; path?: TraceTree.NodePath[]; } | null>; searchResultsIteratorIndex: number | null; searchResultsMap: Map, number>; search_dispatch: React.Dispatch; search_state: TraceSearchState; trace: TraceTree; trace_id: string; } export function Trace({ trace, trace_id, roving_state, roving_dispatch, search_state, search_dispatch, onRowClick, manager, scrollQueueRef, searchResultsIteratorIndex, searchResultsMap, previouslyFocusedNodeRef, onTraceSearch, rerender, forceRerender, }: TraceProps) { const theme = useTheme(); const api = useApi(); const {projects} = useProjects(); const organization = useOrganization(); const containerRef = useRef(null); const rerenderRef = useRef(rerender); rerenderRef.current = rerender; const treePromiseStatusRef = useRef, 'loading' | 'error' | 'success'>>(); if (!treePromiseStatusRef.current) { treePromiseStatusRef.current = new Map(); } const treeRef = useRef(trace); treeRef.current = trace; const searchStateRef = useRef(search_state); searchStateRef.current = search_state; const rovingTabIndexStateRef = useRef(roving_state); rovingTabIndexStateRef.current = roving_state; if ( trace.root.space && (trace.root.space[0] !== manager.to_origin || trace.root.space[1] !== manager.trace_space.width) ) { manager.initializeTraceSpace([trace.root.space[0], 0, trace.root.space[1], 1]); const maybeQueue = decodeScrollQueue(qs.parse(location.search).node); const maybeEventId = qs.parse(location.search)?.eventId; if (maybeQueue || maybeEventId) { scrollQueueRef.current = { eventId: maybeEventId as string, path: maybeQueue as TraceTreeNode['path'], }; } } const loadedRef = useRef(false); useEffect(() => { if (loadedRef.current) { return; } if (trace.type !== 'trace' || !manager) { return; } loadedRef.current = true; if (!scrollQueueRef.current) { if (searchStateRef.current.query) { onTraceSearch(treeRef.current, searchStateRef.current.query, null); } return; } // Node path has higher specificity than eventId const promise = scrollQueueRef.current?.path ? manager.scrollToPath(trace, scrollQueueRef.current.path, rerenderRef.current, { api, organization, }) : scrollQueueRef.current.eventId ? manager.scrollToEventID( scrollQueueRef?.current?.eventId, trace, rerenderRef.current, { api, organization, } ) : Promise.resolve(null); promise .then(maybeNode => { if (!maybeNode) { Sentry.captureMessage('Failled to find and scroll to node in tree'); return; } if (maybeNode.node.space) { manager.animateViewTo(maybeNode.node.space); } onRowClick(maybeNode.node, null); roving_dispatch({ type: 'set index', index: maybeNode.index, node: maybeNode.node, }); manager.list?.scrollToRow(maybeNode.index, 'top'); manager.scrollRowIntoViewHorizontally(maybeNode.node, 0, 12, 'exact'); if (searchStateRef.current.query) { onTraceSearch(treeRef.current, searchStateRef.current.query, maybeNode.node); } }) .finally(() => { // Important to set scrollQueueRef.current to null and trigger a rerender // after the promise resolves as we show a loading state during scroll, // else the screen could jump around while we fetch span data scrollQueueRef.current = null; rerenderRef.current(); }); }, [ api, scrollQueueRef, organization, trace, trace_id, manager, onTraceSearch, onRowClick, roving_dispatch, ]); const handleZoomIn = useCallback( ( event: React.MouseEvent | React.KeyboardEvent, node: TraceTreeNode, value: boolean ) => { if (!isTransactionNode(node) && !isSpanNode(node)) { throw new TypeError('Node must be a transaction or span'); } event.stopPropagation(); rerenderRef.current(); treeRef.current .zoomIn(node, value, { api, organization, }) .then(() => { rerenderRef.current(); if (searchStateRef.current.query) { const previousNode = rovingTabIndexStateRef.current.node || searchStateRef.current.node; onTraceSearch(treeRef.current, searchStateRef.current.query, previousNode); } treePromiseStatusRef.current!.set(node, 'success'); }) .catch(_e => { treePromiseStatusRef.current!.set(node, 'error'); }); }, [api, organization, onTraceSearch] ); const handleExpandNode = useCallback( ( event: React.MouseEvent | React.KeyboardEvent, node: TraceTreeNode, value: boolean ) => { event.stopPropagation(); treeRef.current.expand(node, value); rerenderRef.current(); if (searchStateRef.current.query) { const previousNode = rovingTabIndexStateRef.current.node || searchStateRef.current.node; onTraceSearch(treeRef.current, searchStateRef.current.query, previousNode); } }, [onTraceSearch] ); const onVirtulizedRowClick = useCallback( ( event: React.MouseEvent, index: number, node: TraceTreeNode ) => { previouslyFocusedNodeRef.current = node; const {eventId: _eventId, ...query} = qs.parse(location.search); browserHistory.replace({ pathname: location.pathname, query: { ...query, node: node.path, }, }); onRowClick(node, event); roving_dispatch({type: 'set index', index, node}); if (searchStateRef.current.resultsLookup.has(node)) { const idx = searchStateRef.current.resultsLookup.get(node)!; search_dispatch({ type: 'set iterator index', resultIndex: index, resultIteratorIndex: idx, node, }); } else { search_dispatch({type: 'clear iterator index'}); } }, [roving_dispatch, onRowClick, search_dispatch, previouslyFocusedNodeRef] ); const onRowKeyDown = useCallback( ( event: React.KeyboardEvent, index: number, node: TraceTreeNode ) => { if (!manager.list) { return; } const action = getRovingIndexActionFromEvent(event); if (action) { event.preventDefault(); const nextIndex = computeNextIndexFromAction( index, action, treeRef.current.list.length - 1 ); manager.scrollToRow(nextIndex); roving_dispatch({ type: 'set index', index: nextIndex, node: treeRef.current.list[nextIndex], }); const nextNode = treeRef.current.list[nextIndex]; const offset = nextNode.depth >= node.depth ? manager.trace_physical_space.width / 2 : 0; if (manager.isOutsideOfViewOnKeyDown(trace.list[nextIndex], offset)) { manager.scrollRowIntoViewHorizontally(trace.list[nextIndex], 0, offset); } if (searchStateRef.current.resultsLookup.has(trace.list[nextIndex])) { const idx = searchStateRef.current.resultsLookup.get(trace.list[nextIndex])!; search_dispatch({ type: 'set iterator index', resultIndex: nextIndex, resultIteratorIndex: idx, node, }); } else { search_dispatch({type: 'clear iterator index'}); } } if (event.key === 'ArrowLeft') { if (node.zoomedIn) handleZoomIn(event, node, false); if (node.expanded) handleExpandNode(event, node, false); } if (event.key === 'ArrowRight') { if (!node.zoomedIn && node.canFetch) handleZoomIn(event, node, true); if (!node.expanded) handleExpandNode(event, node, true); } }, [ manager, roving_dispatch, search_dispatch, handleExpandNode, handleZoomIn, trace.list, ] ); // @TODO this is the implementation of infinite scroll. Once the user // reaches the end of the list, we fetch more data. The data is not yet // being appended to the tree as we need to figure out UX for this. // onRowsRendered callback should be passed to the List component // const limitRef = useRef(null); // if (limitRef.current === null) { // let decodedLimit = getTraceQueryParams(qs.parse(location.search)).limit; // if (typeof decodedLimit === 'string') { // decodedLimit = parseInt(decodedLimit, 2); // } // limitRef.current = decodedLimit; // } // const loadMoreRequestRef = // useRef | null> | null>(null); // const onRowsRendered = useCallback((rows: RenderedRows) => { // if (loadMoreRequestRef.current) { // // in flight request // return; // } // if (rows.stopIndex !== treeRef.current.list.length - 1) { // // not at the end // return; // } // if ( // !loadMoreRequestRef.current && // limitRef.current && // rows.stopIndex === treeRef.current.list.length - 1 // ) { // limitRef.current = limitRef.current + 500; // const promise = fetchTrace(api, { // traceId: trace_id, // orgSlug: organization.slug, // query: qs.stringify(getTraceQueryParams(location, {limit: limitRef.current})), // }) // .then(data => { // return data; // }) // .catch(e => { // return e; // }); // loadMoreRequestRef.current = promise; // } // }, []); const projectLookup: Record = useMemo(() => { return projects.reduce>( (acc, project) => { acc[project.slug] = project.platform; return acc; }, {} ); }, [projects]); const render = useCallback( (n: VirtualizedRow) => { return trace.type !== 'trace' || scrollQueueRef.current ? ( ) : ( ); }, // we add rerender as a dependency to trigger the virtualized list rerender // eslint-disable-next-line react-hooks/exhaustive-deps [ handleExpandNode, handleZoomIn, manager, onVirtulizedRowClick, onRowKeyDown, organization, projectLookup, roving_state.index, searchResultsIteratorIndex, searchResultsMap, theme, trace_id, trace.type, forceRerender, ] ); const [scrollContainer, setScrollContainer] = useState(null); const virtualizedList = useVirtualizedList({ manager, items: trace.list, container: scrollContainer, render, }); return ( { containerRef.current = r; manager.registerContainerRef(r); }} className={`${trace.indicators.length > 0 ? 'WithIndicators' : ''} ${trace.type !== 'trace' || scrollQueueRef.current ? 'Loading' : ''}`} >
manager.registerHorizontalScrollBarContainerRef(r)} >
manager.registerDividerRef(r)} />
manager.registerIndicatorContainerRef(r)} > {trace.indicators.length > 0 ? trace.indicators.map((indicator, i) => { return (
manager.registerIndicatorRef(r, i, indicator)} className={`TraceIndicator ${indicator.poor ? 'Errored' : ''}`} >
{indicator.label}
); }) : null} {manager.interval_bars.map((_, i) => { const indicatorTimestamp = manager.intervals[i] ?? 0; const timestamp = manager.to_origin + indicatorTimestamp; if (trace.type !== 'trace') { return null; } return (
manager.registerTimelineIndicatorRef(r, i)} className="TraceIndicator Timeline" style={{ transform: `translate(${manager.computeTransformXFromTimestamp(timestamp)}px, 0)`, }} >
{indicatorTimestamp > 0 ? getDuration(manager.trace_view.x + indicatorTimestamp) : '0s'}
); })}
setScrollContainer(r)}>
{virtualizedList.rendered}
manager.registerGhostRowRef('list', r)} />
manager.registerGhostRowRef('span_list', r)} />
); } function RenderRow(props: { index: number; isSearchResult: boolean; manager: VirtualizedViewManager; node: TraceTreeNode; onExpand: ( event: React.MouseEvent, node: TraceTreeNode, value: boolean ) => void; onRowClick: ( event: React.MouseEvent, index: number, node: TraceTreeNode ) => void; onRowKeyDown: ( event: React.KeyboardEvent, index: number, node: TraceTreeNode ) => void; onZoomIn: ( event: React.MouseEvent, node: TraceTreeNode, value: boolean ) => void; organization: Organization; previouslyFocusedNodeRef: React.MutableRefObject | null>; projects: Record; searchResultsIteratorIndex: number | null; style: React.CSSProperties; tabIndex: number; theme: Theme; trace_id: string; }) { const virtualized_index = props.index - props.manager.start_virtualized_index; const rowSearchClassName = `${props.isSearchResult ? 'SearchResult' : ''} ${props.searchResultsIteratorIndex === props.index ? 'Highlight' : ''}`; if (isAutogroupedNode(props.node)) { return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`Autogrouped TraceRow ${rowSearchClassName} ${props.node.has_errors ? 'Errored' : ''}`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
) : ( ) } status={props.node.fetchStatus} expanded={!props.node.expanded} onClick={e => props.onExpand(e, props.node, !props.node.expanded)} errored={props.node.has_errors} > {COUNT_FORMATTER.format(props.node.groupCount)}
{t('Autogrouped')} {props.node.value.autogrouped_by.op}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node.space!); }} >
); } if (isTransactionNode(props.node)) { const errored = props.node.value.errors.length > 0 || props.node.value.performance_issues.length > 0; return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`TraceRow ${rowSearchClassName} ${errored ? 'Errored' : ''}`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
{props.node.children.length > 0 || props.node.canFetch ? ( ) : ( '+' ) } status={props.node.fetchStatus} expanded={props.node.expanded || props.node.zoomedIn} onClick={e => props.node.canFetch ? props.onZoomIn(e, props.node, !props.node.zoomedIn) : props.onExpand(e, props.node, !props.node.expanded) } errored={errored} > {props.node.children.length > 0 ? COUNT_FORMATTER.format(props.node.children.length) : null} ) : null}
{props.node.value['transaction.op']} {props.node.value.transaction}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } className={ props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node.space!); }} >
); } if (isSpanNode(props.node)) { const errored = props.node.errors.size > 0 || props.node.performance_issues.size > 0; return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`TraceRow ${rowSearchClassName} ${errored ? 'Errored' : ''}`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
{props.node.children.length > 0 || props.node.canFetch ? ( ) : ( ) } status={props.node.fetchStatus} expanded={props.node.expanded || props.node.zoomedIn} onClick={e => props.node.canFetch ? props.onZoomIn(e, props.node, !props.node.zoomedIn) : props.onExpand(e, props.node, !props.node.expanded) } errored={errored} > {props.node.children.length > 0 ? COUNT_FORMATTER.format(props.node.children.length) : null} ) : null}
{props.node.value.op ?? ''} {!props.node.value.description ? 'unknown' : props.node.value.description.length > 100 ? props.node.value.description.slice(0, 100).trim() + '\u2026' : props.node.value.description}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } className={ props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node.space!); }} >
); } if (isMissingInstrumentationNode(props.node)) { return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`TraceRow ${rowSearchClassName}`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
{t('Missing instrumentation')}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } className={ props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node.space!); }} >
); } if (isTraceNode(props.node)) { return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`TraceRow ${rowSearchClassName} ${props.node.has_errors ? 'Errored' : ''}`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
{' '}
{props.node.children.length > 0 || props.node.canFetch ? ( void 0}> {props.node.children.length > 0 ? COUNT_FORMATTER.format(props.node.children.length) : null} ) : null}
{t('Trace')} {props.trace_id}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } className={ props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node.space!); }} >
); } if (isTraceErrorNode(props.node)) { return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`TraceRow ${rowSearchClassName} Errored`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
{' '}
{t('Error')} {props.node.value.title}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } className={ props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node.space!); }} > {typeof props.node.value.timestamp === 'number' ? (
) : null}
); } if (isNoDataNode(props.node)) { return (
props.tabIndex === props.index ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) : null } tabIndex={props.tabIndex === props.index ? 0 : -1} className={`TraceRow ${rowSearchClassName}`} onClick={e => props.onRowClick(e, props.index, props.node)} onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)} style={{ top: props.style.top, height: props.style.height, }} >
props.manager.registerColumnRef('list', r, virtualized_index, props.node) } >
{t('Empty')}{' '} {tct('[type] did not report any span data', { type: props.node.parent ? isTransactionNode(props.node.parent) ? 'Transaction' : isSpanNode(props.node.parent) ? 'Span' : '' : '', })}
props.manager.registerColumnRef('span_list', r, virtualized_index, props.node) } className={ props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME } />
); } return null; } function RenderPlaceholderRow(props: { index: number; manager: VirtualizedViewManager; node: TraceTreeNode; projects: Record; style: React.CSSProperties; theme: Theme; }) { return (
{props.node.children.length > 0 || props.node.canFetch ? ( void 0} > {props.node.children.length > 0 ? COUNT_FORMATTER.format(props.node.children.length) : null} ) : null}
); } function randomBetween(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } function Connectors(props: { manager: VirtualizedViewManager; node: TraceTreeNode; }) { const showVerticalConnector = ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) || (props.node.value && isParentAutogroupedNode(props.node)); // If the tail node of the collapsed node has no children, // we don't want to render the vertical connector as no children // are being rendered as the chain is entirely collapsed const hideVerticalConnector = showVerticalConnector && props.node.value && props.node instanceof ParentAutogroupNode && !props.node.tail.children.length; return ( {props.node.connectors.map((c, i) => { return (
); })} {showVerticalConnector && !hideVerticalConnector ? (
) : null} {props.node.isLastChild ? (
) : (
)} ); } function ChildrenButton(props: { children: React.ReactNode; expanded: boolean; icon: React.ReactNode; onClick: (e: React.MouseEvent) => void; status: TraceTreeNode['fetchStatus'] | undefined; errored?: boolean; }) { return ( ); } interface TraceBarProps { color: string; errors: TraceTreeNode['errors']; manager: VirtualizedViewManager; node_space: [number, number] | null; performance_issues: TraceTreeNode['performance_issues']; profiles: TraceTreeNode['profiles']; virtualized_index: number; } function TraceBar(props: TraceBarProps) { if (!props.node_space) { return null; } const duration = getDuration(props.node_space[1]); const spanTransform = props.manager.computeSpanCSSMatrixTransform(props.node_space); const [inside, textTransform] = props.manager.computeSpanTextPlacement( props.node_space, duration ); return (
props.manager.registerSpanBarRef(r, props.node_space!, props.virtualized_index) } className="TraceBar" style={ { transform: `matrix(${spanTransform.join(',')})`, '--inverse-span-scale': 1 / spanTransform[0], backgroundColor: props.color, // unknown css variables cannot be part of the style object } as React.CSSProperties } > {props.profiles.length > 0 ? ( ) : null} {props.errors.size > 0 ? ( ) : null} {props.performance_issues.size > 0 ? ( ) : null}
props.manager.registerSpanBarTextRef( r, duration, props.node_space!, props.virtualized_index ) } className="TraceBarDuration" style={{ color: inside ? 'white' : '', transform: `translate(${textTransform ?? 0}px, 0)`, }} > {duration}
); } interface InvisibleTraceBarProps { children: React.ReactNode; manager: VirtualizedViewManager; node_space: [number, number] | null; virtualizedIndex: number; } function InvisibleTraceBar(props: InvisibleTraceBarProps) { if (!props.node_space || !props.children) { return null; } const spanTransform = `translateX(${props.manager.computeTransformXFromTimestamp(props.node_space[0])}px)`; return (
props.manager.registerInvisibleBarRef( r, props.node_space!, props.virtualizedIndex ) } className="TraceBar Invisible" style={ { transform: spanTransform, // undefined css variables break style rules '--inverse-span-scale': 1, // unknown css variables cannot be part of the style object } as React.CSSProperties } onDoubleClick={e => { e.stopPropagation(); props.manager.onZoomIntoSpace(props.node_space!); }} > {props.children}
); } interface PerformanceIssuesProps { manager: VirtualizedViewManager; node_space: [number, number] | null; performance_issues: TraceTreeNode['performance_issues']; } function PerformanceIssues(props: PerformanceIssuesProps) { const performance_issues = useMemo(() => { return [...props.performance_issues]; }, [props.performance_issues]); if (!props.performance_issues.size) { return null; } return ( {performance_issues.map((issue, _i) => { const timestamp = issue.start * 1e3; // Clamp the issue timestamp to the span's timestamp const left = props.manager.computeRelativeLeftPositionFromOrigin( clamp( timestamp, props.node_space![0], props.node_space![0] + props.node_space![1] ), props.node_space! ); const max_width = 100 - left; const issue_duration = (issue.end - issue.start) * 1e3; const width = clamp((issue_duration / props.node_space![1]) * 100, 0, max_width); return (
); })}
); } interface ErrorsProps { errors: TraceTreeNode['errors']; manager: VirtualizedViewManager; node_space: [number, number] | null; } function Errors(props: ErrorsProps) { const errors = useMemo(() => { return [...props.errors]; }, [props.errors]); if (!props.errors.size) { return null; } return ( {errors.map((error, _i) => { const timestamp = error.timestamp ? error.timestamp * 1e3 : props.node_space![0]; // Clamp the error timestamp to the span's timestamp const left = props.manager.computeRelativeLeftPositionFromOrigin( clamp( timestamp, props.node_space![0], props.node_space![0] + props.node_space![1] ), props.node_space! ); return (
); })}
); } interface ProfilesProps { manager: VirtualizedViewManager; node_space: [number, number] | null; profiles: TraceTree.Profile[]; } function Profiles(props: ProfilesProps) { if (!props.profiles.length) { return null; } return ( {props.profiles.map((profile, _i) => { const timestamp = profile.space[0]; // Clamp the profile timestamp to the span's timestamp const left = props.manager.computeRelativeLeftPositionFromOrigin( clamp( timestamp, props.node_space![0], props.node_space![0] + props.node_space![1] ), props.node_space! ); return (
); })}
); } interface AutogroupedTraceBarProps { color: string; entire_space: [number, number] | null; errors: TraceTreeNode['errors']; manager: VirtualizedViewManager; node_spaces: [number, number][]; performance_issues: TraceTreeNode['performance_issues']; profiles: TraceTreeNode['profiles']; virtualized_index: number; } function AutogroupedTraceBar(props: AutogroupedTraceBarProps) { if (props.node_spaces && props.node_spaces.length <= 1) { return ( ); } if (!props.node_spaces || !props.entire_space) { return null; } const duration = getDuration(props.entire_space[1]); const spanTransform = props.manager.computeSpanCSSMatrixTransform(props.entire_space); const [inside, textTransform] = props.manager.computeSpanTextPlacement( props.entire_space, duration ); return (
props.manager.registerSpanBarRef( r, props.entire_space!, props.virtualized_index ) } className="TraceBar Invisible" style={{ transform: `matrix(${spanTransform.join(',')})`, backgroundColor: props.color, }} > {props.node_spaces.map((node_space, i) => { const width = node_space[1] / props.entire_space![1]; const left = props.manager.computeRelativeLeftPositionFromOrigin( node_space[0], props.entire_space! ); return (
); })} {props.profiles.length > 0 ? ( ) : null} {props.errors.size > 0 ? ( ) : null} {props.performance_issues.size > 0 ? ( ) : null}
props.manager.registerSpanBarTextRef( r, duration, props.entire_space!, props.virtualized_index ) } className="TraceBarDuration" style={{ color: inside ? 'white' : '', transform: `translate(${textTransform ?? 0}px, 0)`, }} > {duration}
); } /** * This is a wrapper around the Trace component to apply styles * to the trace tree. It exists because we _do not_ want to trigger * emotion's css parsing logic as it is very slow and will cause * the scrolling to flicker. */ const TraceStylingWrapper = styled('div')` margin: auto; overscroll-behavior: none; box-shadow: 0 0 0 1px ${p => p.theme.border}; position: absolute; left: 0; top: 0; width: 100%; height: 100%; grid-area: trace; padding-top: 26px; &.WithIndicators { padding-top: 44px; &:before { height: 44px; .TraceScrollbarContainer { height: 44px; } } .TraceIndicator.Timeline { .TraceIndicatorLabel { top: 26px; } .TraceIndicatorLine { top: 30px; } } } &:before { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 26px; background-color: ${p => p.theme.backgroundSecondary}; border-bottom: 1px solid ${p => p.theme.border}; } &.Loading { .TraceRow { .TraceLeftColumnInner { width: 100%; } } .TraceRightColumn { background-color: transparent !important; } .TraceDivider { pointer-events: none; } } .TraceScrollbarContainer { height: 26px; position: absolute; left: 0; top: 0; overflow: auto; overscroll-behavior: none; .TraceScrollbarScroller { height: 1px; pointer-events: none; visibility: hidden; } .TraceScrollbarHandle { width: 24px; height: 12px; border-radius: 6px; } } .TraceDivider { position: absolute; height: 100%; background-color: transparent; top: 0; cursor: col-resize; z-index: 10; transform: translateX(calc(var(--translate-x) * 1px)); &:before { content: ''; position: absolute; width: 1px; height: 100%; background-color: ${p => p.theme.border}; left: 50%; } &:hover { &:before { background-color: ${p => p.theme.purple300}; } } } .TraceIndicatorContainer { overflow: hidden; width: 100%; height: 100%; position: absolute; right: 0; top: 0; transform: translateX(calc(var(--translate-x) * 1px)); } .TraceIndicator { z-index: 1; width: 3px; height: 100%; top: 0; position: absolute; .TraceIndicatorLabel { min-width: 34px; text-align: center; position: absolute; font-size: 10px; font-weight: bold; color: ${p => p.theme.textColor}; background-color: ${p => p.theme.background}; border-radius: ${p => p.theme.borderRadius}; border: 1px solid ${p => p.theme.border}; padding: 2px; display: inline-block; line-height: 1; margin-top: 2px; white-space: nowrap; } .TraceIndicatorLine { width: 1px; height: 100%; top: 20px; position: absolute; left: 50%; transform: translateX(-2px); background: repeating-linear-gradient( to bottom, transparent 0 4px, ${p => p.theme.textColor} 4px 8px ) 80%/2px 100% no-repeat; } &.Errored { .TraceIndicatorLabel { border: 1px solid ${p => p.theme.error}; color: ${p => p.theme.error}; } .TraceIndicatorLine { background: repeating-linear-gradient( to bottom, transparent 0 4px, ${p => p.theme.error} 4px 8px ) 80%/2px 100% no-repeat; } } &.Timeline { opacity: 1; z-index: 1; pointer-events: none; .TraceIndicatorLabel { font-weight: normal; min-width: 0; top: 8px; width: auto; border: none; background-color: transparent; color: ${p => p.theme.subText}; } .TraceIndicatorLine { background: ${p => p.theme.translucentGray100}; top: 8px; } } } .TraceRow { display: flex; align-items: center; position: absolute; height: 24px; width: 100%; transition: none; font-size: ${p => p.theme.fontSizeSmall}; --row-background-odd: ${p => p.theme.translucentSurface100}; --row-background-hover: ${p => p.theme.translucentSurface100}; --row-background-focused: ${p => p.theme.translucentSurface200}; &.Hidden { position: absolute; height: 100%; width: 100%; top: 0; z-index: -1; &:hover { background-color: transparent; } * { cursor: default !important; } } .TraceError { position: absolute; top: 50%; transform: translate(-50%, -50%) scaleX(var(--inverse-span-scale)); background: ${p => p.theme.background}; width: 18px !important; height: 18px !important; background-color: ${p => p.theme.error}; border-radius: 50%; display: flex; align-items: center; justify-content: center; svg { fill: ${p => p.theme.white}; } } .TraceProfile { position: absolute; top: 50%; transform: translate(-50%, -50%) scaleX(var(--inverse-span-scale)); background: ${p => p.theme.background}; width: 18px !important; height: 18px !important; background-color: ${p => p.theme.purple300}; border-radius: 50%; display: flex; align-items: center; justify-content: center; svg { width: 12px; height: 12px; margin-left: 2px; fill: ${p => p.theme.white}; } } .TracePerformanceIssue { position: absolute; top: 0; display: flex; align-items: center; justify-content: flex-start; background-color: ${p => p.theme.error}; height: 16px; } .TraceRightColumn.Odd { background-color: var(--row-background-odd); } &:hover { background-color: var(--row-background-hovered); } &.Highlight { box-shadow: inset 0 0 0 1px ${p => p.theme.blue200} !important; .TraceLeftColumn { box-shadow: inset 0px 0 0px 1px ${p => p.theme.blue200} !important; } } &.Highlight, &:focus { outline: none; background-color: var(--row-background-focused); .TraceRightColumn.Odd { background-color: transparent !important; } } &:focus, &[tabindex='0'] { background-color: var(--row-background-focused); box-shadow: inset 0 0 0 1px ${p => p.theme.blue300} !important; .TraceLeftColumn { box-shadow: inset 0px 0 0px 1px ${p => p.theme.blue300} !important; } .TraceRightColumn.Odd { background-color: transparent !important; } } &.Errored { color: ${p => p.theme.error}; .TraceChildrenCount { border: 2px solid ${p => p.theme.error}; svg { fill: ${p => p.theme.error}; } } &:focus, &[tabindex='0'] { box-shadow: inset 0 0 0 1px ${p => p.theme.red300} !important; .TraceLeftColumn { box-shadow: inset 0px 0 0px 1px ${p => p.theme.red300} !important; } } } &.SearchResult { background-color: ${p => p.theme.yellow100}; .TraceRightColumn { background-color: transparent; } } &.Autogrouped { color: ${p => p.theme.blue300}; &.Errored { .TraceChildrenCount { background-color: ${p => p.theme.error} !important; } } .TraceDescription { font-weight: bold; } .TraceChildrenCountWrapper { button { color: ${p => p.theme.white}; background-color: ${p => p.theme.blue300}; } svg { fill: ${p => p.theme.white}; } } } } .TraceLeftColumn { height: 100%; white-space: nowrap; display: flex; align-items: center; overflow: hidden; will-change: width; box-shadow: inset 1px 0 0px 0px transparent; cursor: pointer; width: calc(var(--list-column-width) * 100%); .TraceLeftColumnInner { height: 100%; white-space: nowrap; display: flex; align-items: center; will-change: transform; transform-origin: left center; padding-right: ${space(2)}; img { width: 16px; height: 16px; } } } .TraceRightColumn { height: 100%; overflow: hidden; position: relative; display: flex; align-items: center; will-change: width; z-index: 1; cursor: pointer; width: calc(var(--span-column-width) * 100%); &:hover { .TraceArrow.Visible { opacity: 1; transition: 300ms 300ms ease-out; pointer-events: auto; } } } .TraceBar { position: absolute; height: 16px; width: 100%; background-color: black; transform-origin: left center; &.Invisible { background-color: transparent !important; > div { height: 100%; } } svg { width: 14px; height: 14px; } } .TraceArrow { position: absolute; pointer-events: none; top: 0; width: 14px; height: 24px; opacity: 0; background-color: transparent; border: none; transition: 60ms ease-out; font-size: ${p => p.theme.fontSizeMedium}; color: ${p => p.theme.subText}; padding: 0 2px; display: flex; align-items: center; svg { fill: ${p => p.theme.subText}; } &.Left { left: 0; } &.Right { right: 0; transform: rotate(180deg); } } .TraceBarDuration { display: inline-block; transform-origin: left center; font-size: ${p => p.theme.fontSizeExtraSmall}; color: ${p => p.theme.gray300}; white-space: nowrap; font-variant-numeric: tabular-nums; position: absolute; transition: color 0.1s ease-in-out; } .TraceChildrenCount { height: 16px; white-space: nowrap; min-width: 30px; display: flex; align-items: center; justify-content: center; border-radius: 99px; padding: 0px 4px; transition: all 0.15s ease-in-out; background: ${p => p.theme.background}; border: 2px solid ${p => p.theme.border}; line-height: 0; z-index: 1; font-size: 10px; box-shadow: ${p => p.theme.dropShadowLight}; margin-right: 8px; .TraceChildrenCountContent { + .TraceChildrenCountAction { margin-left: 2px; } } .TraceChildrenCountAction { position: relative; display: flex; align-items: center; justify-content: center; } .TraceActionsLoadingIndicator { margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: ${p => p.theme.background}; animation: show 0.1s ease-in-out forwards; @keyframes show { from { opacity: 0; transform: translate(-50%, -50%) scale(0.86); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } .loading-indicator { border-width: 2px; } .loading-message { display: none; } } svg { width: 7px; transition: none; fill: ${p => p.theme.textColor}; } } .TraceChildrenCountWrapper { display: flex; justify-content: flex-end; align-items: center; min-width: 44px; height: 100%; position: relative; button { transition: none; } &.Orphaned { .TraceVerticalConnector, .TraceVerticalLastChildConnector, .TraceExpandedVerticalConnector { border-left: 2px dashed ${p => p.theme.border}; } &::before { border-bottom: 2px dashed ${p => p.theme.border}; } } &.Root { &:before, .TraceVerticalLastChildConnector { visibility: hidden; } } &::before { content: ''; display: block; width: 50%; height: 2px; border-bottom: 2px solid ${p => p.theme.border}; position: absolute; left: 0; top: 50%; transform: translateY(-50%); } &::after { content: ''; background-color: ${p => p.theme.border}; border-radius: 50%; height: 6px; width: 6px; position: absolute; left: 50%; top: 50%; transform: translateY(-50%); } } .TraceVerticalConnector { position: absolute; left: 0; top: 0; bottom: 0; height: 100%; width: 2px; border-left: 2px solid ${p => p.theme.border}; &.Orphaned { border-left: 2px dashed ${p => p.theme.border}; } } .TraceVerticalLastChildConnector { position: absolute; left: 0; top: 0; bottom: 0; height: 50%; width: 2px; border-left: 2px solid ${p => p.theme.border}; border-bottom-left-radius: 4px; } .TraceExpandedVerticalConnector { position: absolute; bottom: 0; height: 50%; left: 50%; width: 2px; border-left: 2px solid ${p => p.theme.border}; } .TraceOperation { margin-left: 4px; text-overflow: ellipsis; white-space: nowrap; font-weight: bold; } .TraceEmDash { margin-left: 4px; margin-right: 4px; } .TraceDescription { white-space: nowrap; } `;