issuesTraceWaterfall.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. 'issue_details'
  135. );
  136. }
  137. // Construct the visual representation of the tree
  138. props.tree.build();
  139. // Find all the nodes that match the event id from the error so that we can try and
  140. // link the user to the most specific one.
  141. const nodes = IssuesTraceTree.FindAll(props.tree.root, n => {
  142. if (isTraceErrorNode(n)) {
  143. return n.value.event_id === props.event.eventID;
  144. }
  145. if (isTransactionNode(n)) {
  146. if (n.value.event_id === props.event.eventID) {
  147. return true;
  148. }
  149. for (const e of n.errors) {
  150. if (e.event_id === props.event.eventID) {
  151. return true;
  152. }
  153. }
  154. for (const p of n.performance_issues) {
  155. if (p.event_id === props.event.eventID) {
  156. return true;
  157. }
  158. }
  159. }
  160. if (isSpanNode(n)) {
  161. if (n.value.span_id === props.event.eventID) {
  162. return true;
  163. }
  164. for (const e of n.errors) {
  165. if (e.event_id === props.event.eventID) {
  166. return true;
  167. }
  168. }
  169. for (const p of n.performance_issues) {
  170. if (p.event_id === props.event.eventID) {
  171. return true;
  172. }
  173. }
  174. }
  175. return false;
  176. });
  177. // By order of priority, we want to find the error node, then the span node, then the transaction node.
  178. // This is because the error node as standalone is the most specific one, otherwise we look for the span that
  179. // the error may have been attributed to, otherwise we look at the transaction.
  180. const node =
  181. nodes?.find(n => isTraceErrorNode(n)) ||
  182. nodes?.find(n => isSpanNode(n)) ||
  183. nodes?.find(n => isTransactionNode(n));
  184. const index = node ? props.tree.list.indexOf(node) : -1;
  185. if (node) {
  186. const preserveNodes: Array<TraceTreeNode<TraceTree.NodeValue>> = [node];
  187. let start = index;
  188. while (--start > 0) {
  189. if (
  190. isTraceErrorNode(props.tree.list[start]!) ||
  191. node.errors.size > 0 ||
  192. node.performance_issues.size > 0
  193. ) {
  194. preserveNodes.push(props.tree.list[start]!);
  195. break;
  196. }
  197. }
  198. start = index;
  199. while (++start < props.tree.list.length) {
  200. if (
  201. isTraceErrorNode(props.tree.list[start]!) ||
  202. node.errors.size > 0 ||
  203. node.performance_issues.size > 0
  204. ) {
  205. preserveNodes.push(props.tree.list[start]!);
  206. break;
  207. }
  208. }
  209. props.tree.collapseList(preserveNodes);
  210. }
  211. if (index === -1 || !node) {
  212. const hasScrollComponent = !!props.event.eventID;
  213. if (hasScrollComponent) {
  214. Sentry.withScope(scope => {
  215. scope.setFingerprint(['trace-view-issesu-scroll-to-node-error']);
  216. scope.captureMessage('Failed to scroll to node in issues trace tree');
  217. });
  218. }
  219. return;
  220. }
  221. // We dont want to focus the row at load time, because it will cause the page to scroll down to
  222. // the trace section. Mark is as scrolled on load so nothing will happen.
  223. previouslyFocusedNodeRef.current = node;
  224. traceScheduler.once('initialize virtualized list', () => {
  225. function onTargetRowMeasure() {
  226. if (!node || !viewManager.row_measurer.cache.has(node)) {
  227. return;
  228. }
  229. viewManager.row_measurer.off('row measure end', onTargetRowMeasure);
  230. if (viewManager.isOutsideOfView(node)) {
  231. viewManager.scrollRowIntoViewHorizontally(node, 0, 48, 'measured');
  232. }
  233. }
  234. viewManager.scrollToRow(index, 'center');
  235. viewManager.row_measurer.on('row measure end', onTargetRowMeasure);
  236. // setRowAsFocused(node, null, traceStateRef.current.search.resultsLookup, index);
  237. traceDispatch({
  238. type: 'set roving index',
  239. node,
  240. index,
  241. action_source: 'load',
  242. });
  243. });
  244. }, [
  245. traceDispatch,
  246. viewManager,
  247. traceScheduler,
  248. props.tree,
  249. props.organization,
  250. props.event,
  251. isLoadingSubscriptionDetails,
  252. hasExceededPerformanceUsageLimit,
  253. ]);
  254. useTraceTimelineChangeSync({
  255. tree: props.tree,
  256. traceScheduler,
  257. });
  258. useTraceSpaceListeners({
  259. view: traceView,
  260. viewManager,
  261. traceScheduler,
  262. });
  263. useDividerResizeSync(traceScheduler);
  264. const onLoadScrollStatus = useTraceIssuesOnLoad({
  265. onTraceLoad,
  266. event: props.event,
  267. tree: props.tree,
  268. });
  269. return (
  270. <Fragment>
  271. <TraceTypeWarnings
  272. tree={props.tree}
  273. traceSlug={props.traceSlug}
  274. organization={organization}
  275. />
  276. <IssuesTraceGrid
  277. layout={traceState.preferences.layout}
  278. rowCount={
  279. props.tree.type === 'trace' && onLoadScrollStatus === 'success'
  280. ? props.tree.list.length
  281. : 8
  282. }
  283. >
  284. <IssuesPointerDisabled>
  285. <Trace
  286. metaQueryResults={props.meta}
  287. trace={props.tree}
  288. rerender={rerender}
  289. trace_id={props.traceSlug}
  290. onRowClick={onRowClick}
  291. onTraceSearch={noopTraceSearch}
  292. previouslyFocusedNodeRef={previouslyFocusedNodeRef}
  293. manager={viewManager}
  294. scheduler={traceScheduler}
  295. forceRerender={forceRender}
  296. isLoading={props.tree.type === 'loading' || onLoadScrollStatus === 'pending'}
  297. />
  298. </IssuesPointerDisabled>
  299. {props.tree.type === 'loading' || onLoadScrollStatus === 'pending' ? (
  300. <TraceWaterfallState.Loading />
  301. ) : props.tree.type === 'error' ? (
  302. <TraceWaterfallState.Error />
  303. ) : props.tree.type === 'empty' ? (
  304. <TraceWaterfallState.Empty />
  305. ) : null}
  306. </IssuesTraceGrid>
  307. </Fragment>
  308. );
  309. }
  310. const IssuesPointerDisabled = styled('div')`
  311. pointer-events: none;
  312. position: relative;
  313. height: 100%;
  314. width: 100%;
  315. `;
  316. const ROW_HEIGHT = 24;
  317. const MIN_ROW_COUNT = 1;
  318. const HEADER_HEIGHT = 38;
  319. const MAX_HEIGHT = 12 * ROW_HEIGHT + HEADER_HEIGHT;
  320. const MAX_ROW_COUNT = Math.floor(MAX_HEIGHT / ROW_HEIGHT);
  321. const IssuesTraceGrid = styled(TraceGrid)<{
  322. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  323. rowCount: number;
  324. }>`
  325. display: block;
  326. flex-grow: 1;
  327. max-height: ${MAX_HEIGHT}px;
  328. height: ${p =>
  329. Math.min(Math.max(p.rowCount, MIN_ROW_COUNT), MAX_ROW_COUNT) * ROW_HEIGHT +
  330. HEADER_HEIGHT}px;
  331. `;