issuesTraceWaterfall.tsx 12 KB

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