123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- import type React from 'react';
- import {
- Fragment,
- useCallback,
- useEffect,
- useLayoutEffect,
- useMemo,
- useReducer,
- useRef,
- } from 'react';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import type {Event} from 'sentry/types/event';
- import type {Project} from 'sentry/types/project';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import useOrganization from 'sentry/utils/useOrganization';
- import useProjects from 'sentry/utils/useProjects';
- import {
- isSpanNode,
- isTraceErrorNode,
- isTransactionNode,
- } from 'sentry/views/performance/newTraceDetails/traceGuards';
- import {IssuesTraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/issuesTraceTree';
- import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
- import {useDividerResizeSync} from 'sentry/views/performance/newTraceDetails/useDividerResizeSync';
- import {useTraceSpaceListeners} from 'sentry/views/performance/newTraceDetails/useTraceSpaceListeners';
- import type {TraceTreeNode} from './traceModels/traceTreeNode';
- import {TraceScheduler} from './traceRenderers/traceScheduler';
- import {TraceView as TraceViewModel} from './traceRenderers/traceView';
- import {VirtualizedViewManager} from './traceRenderers/virtualizedViewManager';
- import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider';
- import {Trace} from './trace';
- import {traceAnalytics} from './traceAnalytics';
- import type {TraceReducerState} from './traceState';
- import {
- traceNodeAdjacentAnalyticsProperties,
- traceNodeAnalyticsName,
- } from './traceTreeAnalytics';
- import TraceTypeWarnings from './traceTypeWarnings';
- import type {TraceWaterfallProps} from './traceWaterfall';
- import {TraceGrid} from './traceWaterfall';
- import {TraceWaterfallState} from './traceWaterfallState';
- import {useTraceIssuesOnLoad} from './useTraceOnLoad';
- import {useTraceTimelineChangeSync} from './useTraceTimelineChangeSync';
- const noopTraceSearch = () => {};
- interface IssuesTraceWaterfallProps extends Omit<TraceWaterfallProps, 'tree'> {
- event: Event;
- tree: IssuesTraceTree;
- }
- export function IssuesTraceWaterfall(props: IssuesTraceWaterfallProps) {
- const {projects} = useProjects();
- const organization = useOrganization();
- const traceState = useTraceState();
- const traceDispatch = useTraceStateDispatch();
- const [forceRender, rerender] = useReducer(x => (x + 1) % Number.MAX_SAFE_INTEGER, 0);
- const traceView = useMemo(() => new TraceViewModel(), []);
- const traceScheduler = useMemo(() => new TraceScheduler(), []);
- const projectsRef = useRef<Project[]>(projects);
- projectsRef.current = projects;
- useEffect(() => {
- trackAnalytics('performance_views.trace_view_v1_page_load', {
- organization: props.organization,
- source: props.source,
- });
- }, [props.organization, props.source]);
- const previouslyFocusedNodeRef = useRef<TraceTreeNode<TraceTree.NodeValue> | null>(
- null
- );
- // Assign the trace state to a ref so we can access it without re-rendering
- const traceStateRef = useRef<TraceReducerState>(traceState);
- traceStateRef.current = traceState;
- const traceStatePreferencesRef = useRef<
- Pick<TraceReducerState['preferences'], 'autogroup' | 'missing_instrumentation'>
- >(traceState.preferences);
- traceStatePreferencesRef.current = traceState.preferences;
- // Initialize the view manager right after the state reducer
- const viewManager = useMemo(() => {
- return new VirtualizedViewManager(
- {
- list: {width: traceState.preferences.list.width},
- span_list: {width: 1 - traceState.preferences.list.width},
- },
- traceScheduler,
- traceView
- );
- // We only care about initial state when we initialize the view manager
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- // Initialize the tabs reducer when the tree initializes
- useLayoutEffect(() => {
- return traceDispatch({
- type: 'set roving count',
- items: props.tree.list.length - 1,
- });
- }, [props.tree.list.length, traceDispatch]);
- const onRowClick = useCallback(
- (
- node: TraceTreeNode<TraceTree.NodeValue>,
- _event: React.MouseEvent<HTMLElement>,
- index: number
- ) => {
- trackAnalytics('trace.trace_layout.span_row_click', {
- organization,
- num_children: node.children.length,
- type: traceNodeAnalyticsName(node),
- project_platform:
- projects.find(p => p.slug === node.metadata.project_slug)?.platform || 'other',
- ...traceNodeAdjacentAnalyticsProperties(node),
- });
- traceDispatch({
- type: 'set roving index',
- node,
- index,
- action_source: 'click',
- });
- },
- [organization, projects, traceDispatch]
- );
- // Callback that is invoked when the trace loads and reaches its initialied state,
- // that is when the trace tree data and any data that the trace depends on is loaded,
- // but the trace is not yet rendered in the view.
- const onTraceLoad = useCallback(() => {
- traceAnalytics.trackTraceShape(props.tree, projectsRef.current, props.organization);
- // Construct the visual representation of the tree
- props.tree.build();
- // Find all the nodes that match the event id from the error so that we can try and
- // link the user to the most specific one.
- const nodes = IssuesTraceTree.FindAll(props.tree.root, n => {
- if (isTraceErrorNode(n)) {
- return n.value.event_id === props.event.eventID;
- }
- if (isTransactionNode(n)) {
- if (n.value.event_id === props.event.eventID) {
- return true;
- }
- for (const e of n.errors) {
- if (e.event_id === props.event.eventID) {
- return true;
- }
- }
- for (const p of n.performance_issues) {
- if (p.event_id === props.event.eventID) {
- return true;
- }
- }
- }
- if (isSpanNode(n)) {
- if (n.value.span_id === props.event.eventID) {
- return true;
- }
- for (const e of n.errors) {
- if (e.event_id === props.event.eventID) {
- return true;
- }
- }
- for (const p of n.performance_issues) {
- if (p.event_id === props.event.eventID) {
- return true;
- }
- }
- }
- return false;
- });
- // By order of priority, we want to find the error node, then the span node, then the transaction node.
- // This is because the error node as standalone is the most specific one, otherwise we look for the span that
- // the error may have been attributed to, otherwise we look at the transaction.
- const node =
- nodes?.find(n => isTraceErrorNode(n)) ||
- nodes?.find(n => isSpanNode(n)) ||
- nodes?.find(n => isTransactionNode(n));
- if (node) {
- props.tree.collapseList([node]);
- }
- const index = node ? props.tree.list.indexOf(node) : -1;
- if (index === -1 || !node) {
- const hasScrollComponent = !!props.event.eventID;
- if (hasScrollComponent) {
- Sentry.withScope(scope => {
- scope.setFingerprint(['trace-view-issesu-scroll-to-node-error']);
- scope.captureMessage('Failed to scroll to node in issues trace tree');
- });
- }
- return;
- }
- // We dont want to focus the row at load time, because it will cause the page to scroll down to
- // the trace section. Mark is as scrolled on load so nothing will happen.
- previouslyFocusedNodeRef.current = node;
- traceScheduler.once('initialize virtualized list', () => {
- function onTargetRowMeasure() {
- if (!node || !viewManager.row_measurer.cache.has(node)) {
- return;
- }
- viewManager.row_measurer.off('row measure end', onTargetRowMeasure);
- if (viewManager.isOutsideOfView(node)) {
- viewManager.scrollRowIntoViewHorizontally(node!, 0, 48, 'measured');
- }
- }
- viewManager.scrollToRow(index, 'center');
- viewManager.row_measurer.on('row measure end', onTargetRowMeasure);
- // setRowAsFocused(node, null, traceStateRef.current.search.resultsLookup, index);
- traceDispatch({
- type: 'set roving index',
- node,
- index,
- action_source: 'load',
- });
- });
- }, [
- traceDispatch,
- viewManager,
- traceScheduler,
- props.tree,
- props.organization,
- props.event,
- ]);
- useTraceTimelineChangeSync({
- tree: props.tree,
- traceScheduler,
- });
- useTraceSpaceListeners({
- view: traceView,
- viewManager,
- traceScheduler,
- });
- useDividerResizeSync(traceScheduler);
- const onLoadScrollStatus = useTraceIssuesOnLoad({
- onTraceLoad,
- event: props.event,
- tree: props.tree,
- });
- return (
- <Fragment>
- <TraceTypeWarnings
- tree={props.tree}
- traceSlug={props.traceSlug}
- organization={organization}
- />
- <IssuesTraceGrid
- layout={traceState.preferences.layout}
- rowCount={
- props.tree.type === 'trace' && onLoadScrollStatus === 'success'
- ? props.tree.list.length
- : 8
- }
- >
- <IssuesPointerDisabled>
- <Trace
- trace={props.tree}
- rerender={rerender}
- trace_id={props.traceSlug}
- onRowClick={onRowClick}
- onTraceSearch={noopTraceSearch}
- previouslyFocusedNodeRef={previouslyFocusedNodeRef}
- manager={viewManager}
- scheduler={traceScheduler}
- forceRerender={forceRender}
- isLoading={props.tree.type === 'loading' || onLoadScrollStatus === 'pending'}
- />
- </IssuesPointerDisabled>
- {props.tree.type === 'loading' || onLoadScrollStatus === 'pending' ? (
- <TraceWaterfallState.Loading />
- ) : props.tree.type === 'error' ? (
- <TraceWaterfallState.Error />
- ) : props.tree.type === 'empty' ? (
- <TraceWaterfallState.Empty />
- ) : null}
- </IssuesTraceGrid>
- </Fragment>
- );
- }
- const IssuesPointerDisabled = styled('div')`
- pointer-events: none;
- position: relative;
- height: 100%;
- width: 100%;
- `;
- const ROW_HEIGHT = 24;
- const MIN_ROW_COUNT = 1;
- const HEADER_HEIGHT = 28;
- const MAX_HEIGHT = 12 * ROW_HEIGHT + HEADER_HEIGHT;
- const MAX_ROW_COUNT = Math.floor(MAX_HEIGHT / ROW_HEIGHT);
- const IssuesTraceGrid = styled(TraceGrid)<{
- layout: 'drawer bottom' | 'drawer left' | 'drawer right';
- rowCount: number;
- }>`
- display: block;
- flex-grow: 1;
- max-height: ${MAX_HEIGHT}px;
- height: ${p =>
- Math.min(Math.max(p.rowCount, MIN_ROW_COUNT), MAX_ROW_COUNT) * ROW_HEIGHT +
- HEADER_HEIGHT}px;
- `;
|