issuesTraceWaterfall.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import type React from 'react';
  2. import {
  3. Fragment,
  4. useCallback,
  5. useEffect,
  6. useLayoutEffect,
  7. useMemo,
  8. useReducer,
  9. useRef,
  10. } from 'react';
  11. import styled from '@emotion/styled';
  12. import * as Sentry from '@sentry/react';
  13. import type {Event} from 'sentry/types/event';
  14. import type {Project} from 'sentry/types/project';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjects from 'sentry/utils/useProjects';
  18. import {
  19. isSpanNode,
  20. isTraceErrorNode,
  21. isTransactionNode,
  22. } from 'sentry/views/performance/newTraceDetails/traceGuards';
  23. import {IssuesTraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/issuesTraceTree';
  24. import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  25. import {useDividerResizeSync} from 'sentry/views/performance/newTraceDetails/useDividerResizeSync';
  26. import {useTraceSpaceListeners} from 'sentry/views/performance/newTraceDetails/useTraceSpaceListeners';
  27. import type {TraceTreeNode} from './traceModels/traceTreeNode';
  28. import {TraceScheduler} from './traceRenderers/traceScheduler';
  29. import {TraceView as TraceViewModel} from './traceRenderers/traceView';
  30. import {VirtualizedViewManager} from './traceRenderers/virtualizedViewManager';
  31. import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider';
  32. import {Trace} from './trace';
  33. import {traceAnalytics} from './traceAnalytics';
  34. import type {TraceReducerState} from './traceState';
  35. import {
  36. traceNodeAdjacentAnalyticsProperties,
  37. traceNodeAnalyticsName,
  38. } from './traceTreeAnalytics';
  39. import TraceTypeWarnings from './traceTypeWarnings';
  40. import type {TraceWaterfallProps} from './traceWaterfall';
  41. import {TraceGrid} from './traceWaterfall';
  42. import {TraceWaterfallState} from './traceWaterfallState';
  43. import {useTraceIssuesOnLoad} from './useTraceOnLoad';
  44. import {useTraceTimelineChangeSync} from './useTraceTimelineChangeSync';
  45. const noopTraceSearch = () => {};
  46. interface IssuesTraceWaterfallProps extends Omit<TraceWaterfallProps, 'tree'> {
  47. event: Event;
  48. tree: IssuesTraceTree;
  49. }
  50. export function IssuesTraceWaterfall(props: IssuesTraceWaterfallProps) {
  51. const {projects} = useProjects();
  52. const organization = useOrganization();
  53. const traceState = useTraceState();
  54. const traceDispatch = useTraceStateDispatch();
  55. const [forceRender, rerender] = useReducer(x => (x + 1) % Number.MAX_SAFE_INTEGER, 0);
  56. const traceView = useMemo(() => new TraceViewModel(), []);
  57. const traceScheduler = useMemo(() => new TraceScheduler(), []);
  58. const projectsRef = useRef<Project[]>(projects);
  59. projectsRef.current = projects;
  60. useEffect(() => {
  61. trackAnalytics('performance_views.trace_view_v1_page_load', {
  62. organization: props.organization,
  63. source: props.source,
  64. });
  65. }, [props.organization, props.source]);
  66. const previouslyFocusedNodeRef = useRef<TraceTreeNode<TraceTree.NodeValue> | null>(
  67. null
  68. );
  69. // Assign the trace state to a ref so we can access it without re-rendering
  70. const traceStateRef = useRef<TraceReducerState>(traceState);
  71. traceStateRef.current = traceState;
  72. const traceStatePreferencesRef = useRef<
  73. Pick<TraceReducerState['preferences'], 'autogroup' | 'missing_instrumentation'>
  74. >(traceState.preferences);
  75. traceStatePreferencesRef.current = traceState.preferences;
  76. // Initialize the view manager right after the state reducer
  77. const viewManager = useMemo(() => {
  78. return new VirtualizedViewManager(
  79. {
  80. list: {width: traceState.preferences.list.width},
  81. span_list: {width: 1 - traceState.preferences.list.width},
  82. },
  83. traceScheduler,
  84. traceView
  85. );
  86. // We only care about initial state when we initialize the view manager
  87. // eslint-disable-next-line react-hooks/exhaustive-deps
  88. }, []);
  89. // Initialize the tabs reducer when the tree initializes
  90. useLayoutEffect(() => {
  91. return traceDispatch({
  92. type: 'set roving count',
  93. items: props.tree.list.length - 1,
  94. });
  95. }, [props.tree.list.length, traceDispatch]);
  96. const onRowClick = useCallback(
  97. (
  98. node: TraceTreeNode<TraceTree.NodeValue>,
  99. _event: React.MouseEvent<HTMLElement>,
  100. index: number
  101. ) => {
  102. trackAnalytics('trace.trace_layout.span_row_click', {
  103. organization,
  104. num_children: node.children.length,
  105. type: traceNodeAnalyticsName(node),
  106. project_platform:
  107. projects.find(p => p.slug === node.metadata.project_slug)?.platform || 'other',
  108. ...traceNodeAdjacentAnalyticsProperties(node),
  109. });
  110. traceDispatch({
  111. type: 'set roving index',
  112. node,
  113. index,
  114. action_source: 'click',
  115. });
  116. },
  117. [organization, projects, traceDispatch]
  118. );
  119. // Callback that is invoked when the trace loads and reaches its initialied state,
  120. // that is when the trace tree data and any data that the trace depends on is loaded,
  121. // but the trace is not yet rendered in the view.
  122. const onTraceLoad = useCallback(() => {
  123. traceAnalytics.trackTraceShape(props.tree, projectsRef.current, props.organization);
  124. // Construct the visual representation of the tree
  125. props.tree.build();
  126. // Find all the nodes that match the event id from the error so that we can try and
  127. // link the user to the most specific one.
  128. const nodes = IssuesTraceTree.FindAll(props.tree.root, n => {
  129. if (isTraceErrorNode(n)) {
  130. return n.value.event_id === props.event.eventID;
  131. }
  132. if (isTransactionNode(n)) {
  133. if (n.value.event_id === props.event.eventID) {
  134. return true;
  135. }
  136. for (const e of n.errors) {
  137. if (e.event_id === props.event.eventID) {
  138. return true;
  139. }
  140. }
  141. for (const p of n.performance_issues) {
  142. if (p.event_id === props.event.eventID) {
  143. return true;
  144. }
  145. }
  146. }
  147. if (isSpanNode(n)) {
  148. if (n.value.span_id === props.event.eventID) {
  149. return true;
  150. }
  151. for (const e of n.errors) {
  152. if (e.event_id === props.event.eventID) {
  153. return true;
  154. }
  155. }
  156. for (const p of n.performance_issues) {
  157. if (p.event_id === props.event.eventID) {
  158. return true;
  159. }
  160. }
  161. }
  162. return false;
  163. });
  164. // By order of priority, we want to find the error node, then the span node, then the transaction node.
  165. // This is because the error node as standalone is the most specific one, otherwise we look for the span that
  166. // the error may have been attributed to, otherwise we look at the transaction.
  167. const node =
  168. nodes?.find(n => isTraceErrorNode(n)) ||
  169. nodes?.find(n => isSpanNode(n)) ||
  170. nodes?.find(n => isTransactionNode(n));
  171. if (node) {
  172. props.tree.collapseList([node]);
  173. }
  174. const index = node ? props.tree.list.indexOf(node) : -1;
  175. if (index === -1 || !node) {
  176. const hasScrollComponent = !!props.event.eventID;
  177. if (hasScrollComponent) {
  178. Sentry.withScope(scope => {
  179. scope.setFingerprint(['trace-view-issesu-scroll-to-node-error']);
  180. scope.captureMessage('Failed to scroll to node in issues trace tree');
  181. });
  182. }
  183. return;
  184. }
  185. // We dont want to focus the row at load time, because it will cause the page to scroll down to
  186. // the trace section. Mark is as scrolled on load so nothing will happen.
  187. previouslyFocusedNodeRef.current = node;
  188. traceScheduler.once('initialize virtualized list', () => {
  189. function onTargetRowMeasure() {
  190. if (!node || !viewManager.row_measurer.cache.has(node)) {
  191. return;
  192. }
  193. viewManager.row_measurer.off('row measure end', onTargetRowMeasure);
  194. if (viewManager.isOutsideOfView(node)) {
  195. viewManager.scrollRowIntoViewHorizontally(node!, 0, 48, 'measured');
  196. }
  197. }
  198. viewManager.scrollToRow(index, 'center');
  199. viewManager.row_measurer.on('row measure end', onTargetRowMeasure);
  200. // setRowAsFocused(node, null, traceStateRef.current.search.resultsLookup, index);
  201. traceDispatch({
  202. type: 'set roving index',
  203. node,
  204. index,
  205. action_source: 'load',
  206. });
  207. });
  208. }, [
  209. traceDispatch,
  210. viewManager,
  211. traceScheduler,
  212. props.tree,
  213. props.organization,
  214. props.event,
  215. ]);
  216. useTraceTimelineChangeSync({
  217. tree: props.tree,
  218. traceScheduler,
  219. });
  220. useTraceSpaceListeners({
  221. view: traceView,
  222. viewManager,
  223. traceScheduler,
  224. });
  225. useDividerResizeSync(traceScheduler);
  226. const onLoadScrollStatus = useTraceIssuesOnLoad({
  227. onTraceLoad,
  228. event: props.event,
  229. tree: props.tree,
  230. });
  231. return (
  232. <Fragment>
  233. <TraceTypeWarnings
  234. tree={props.tree}
  235. traceSlug={props.traceSlug}
  236. organization={organization}
  237. />
  238. <IssuesTraceGrid
  239. layout={traceState.preferences.layout}
  240. rowCount={
  241. props.tree.type === 'trace' && onLoadScrollStatus === 'success'
  242. ? props.tree.list.length
  243. : 8
  244. }
  245. >
  246. <IssuesPointerDisabled>
  247. <Trace
  248. trace={props.tree}
  249. rerender={rerender}
  250. trace_id={props.traceSlug}
  251. onRowClick={onRowClick}
  252. onTraceSearch={noopTraceSearch}
  253. previouslyFocusedNodeRef={previouslyFocusedNodeRef}
  254. manager={viewManager}
  255. scheduler={traceScheduler}
  256. forceRerender={forceRender}
  257. isLoading={props.tree.type === 'loading' || onLoadScrollStatus === 'pending'}
  258. />
  259. </IssuesPointerDisabled>
  260. {props.tree.type === 'loading' || onLoadScrollStatus === 'pending' ? (
  261. <TraceWaterfallState.Loading />
  262. ) : props.tree.type === 'error' ? (
  263. <TraceWaterfallState.Error />
  264. ) : props.tree.type === 'empty' ? (
  265. <TraceWaterfallState.Empty />
  266. ) : null}
  267. </IssuesTraceGrid>
  268. </Fragment>
  269. );
  270. }
  271. const IssuesPointerDisabled = styled('div')`
  272. pointer-events: none;
  273. position: relative;
  274. height: 100%;
  275. width: 100%;
  276. `;
  277. const ROW_HEIGHT = 24;
  278. const MIN_ROW_COUNT = 1;
  279. const HEADER_HEIGHT = 28;
  280. const MAX_HEIGHT = 12 * ROW_HEIGHT + HEADER_HEIGHT;
  281. const MAX_ROW_COUNT = Math.floor(MAX_HEIGHT / ROW_HEIGHT);
  282. const IssuesTraceGrid = styled(TraceGrid)<{
  283. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  284. rowCount: number;
  285. }>`
  286. display: block;
  287. flex-grow: 1;
  288. max-height: ${MAX_HEIGHT}px;
  289. height: ${p =>
  290. Math.min(Math.max(p.rowCount, MIN_ROW_COUNT), MAX_ROW_COUNT) * ROW_HEIGHT +
  291. HEADER_HEIGHT}px;
  292. `;