newTraceDetailsTransactionBar.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  1. import {createRef, Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import {Observer} from 'mobx-react';
  6. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  7. import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
  8. import {SpanDetailProps} from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails';
  9. import NewTraceDetailsSpanTree from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanTree';
  10. import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
  11. import * as SpanContext from 'sentry/components/events/interfaces/spans/spanContext';
  12. import {MeasurementMarker} from 'sentry/components/events/interfaces/spans/styles';
  13. import {
  14. getMeasurementBounds,
  15. parseTraceDetailsURLHash,
  16. SpanBoundsType,
  17. SpanGeneratedBoundsType,
  18. transactionTargetHash,
  19. VerticalMark,
  20. } from 'sentry/components/events/interfaces/spans/utils';
  21. import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
  22. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  23. import Link from 'sentry/components/links/link';
  24. import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
  25. import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
  26. import {
  27. Row,
  28. RowCell,
  29. RowCellContainer,
  30. RowReplayTimeIndicators,
  31. } from 'sentry/components/performance/waterfall/row';
  32. import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
  33. import {
  34. DividerContainer,
  35. DividerLine,
  36. DividerLineGhostContainer,
  37. ErrorBadge,
  38. } from 'sentry/components/performance/waterfall/rowDivider';
  39. import {
  40. RowTitle,
  41. RowTitleContainer,
  42. RowTitleContent,
  43. } from 'sentry/components/performance/waterfall/rowTitle';
  44. import {
  45. ConnectorBar,
  46. TOGGLE_BORDER_BOX,
  47. TreeConnector,
  48. TreeToggle,
  49. TreeToggleContainer,
  50. TreeToggleIcon,
  51. } from 'sentry/components/performance/waterfall/treeConnector';
  52. import {
  53. getDurationDisplay,
  54. getHumanDuration,
  55. } from 'sentry/components/performance/waterfall/utils';
  56. import {TransactionProfileIdProvider} from 'sentry/components/profiling/transactionProfileIdProvider';
  57. import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
  58. import {Tooltip} from 'sentry/components/tooltip';
  59. import {IconZoom} from 'sentry/icons/iconZoom';
  60. import {t} from 'sentry/locale';
  61. import {space} from 'sentry/styles/space';
  62. import {EventTransaction, Organization} from 'sentry/types';
  63. import {defined} from 'sentry/utils';
  64. import toPercent from 'sentry/utils/number/toPercent';
  65. import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
  66. import {TraceError, TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  67. import {
  68. isTraceError,
  69. isTraceRoot,
  70. isTraceTransaction,
  71. } from 'sentry/utils/performance/quickTrace/utils';
  72. import Projects from 'sentry/utils/projects';
  73. import {useApiQuery} from 'sentry/utils/queryClient';
  74. import useRouter from 'sentry/utils/useRouter';
  75. import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
  76. import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
  77. import {EventDetail} from './newTraceDetailsContent';
  78. import {ProjectBadgeContainer} from './styles';
  79. import {TraceInfo, TraceRoot, TreeDepth} from './types';
  80. import {shortenErrorTitle} from './utils';
  81. const MARGIN_LEFT = 0;
  82. const TRANSACTION_BAR_HEIGHT = 24;
  83. type Props = {
  84. addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
  85. continuingDepths: TreeDepth[];
  86. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  87. hasGuideAnchor: boolean;
  88. index: number;
  89. isBarScrolledTo: boolean;
  90. isExpanded: boolean;
  91. isLast: boolean;
  92. isOrphan: boolean;
  93. isVisible: boolean;
  94. location: Location;
  95. onBarScrolledTo: () => void;
  96. onWheel: (deltaX: number) => void;
  97. organization: Organization;
  98. removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
  99. toggleExpandedState: () => void;
  100. traceInfo: TraceInfo;
  101. traceViewRef: React.RefObject<HTMLDivElement>;
  102. transaction: TraceRoot | TraceFullDetailed | TraceError;
  103. barColor?: string;
  104. isOrphanError?: boolean;
  105. measurements?: Map<number, VerticalMark>;
  106. numOfOrphanErrors?: number;
  107. onRowClick?: (detailKey: EventDetail | SpanDetailProps | undefined) => void;
  108. onlyOrphanErrors?: boolean;
  109. };
  110. function NewTraceDetailsTransactionBar(props: Props) {
  111. const hashValues = parseTraceDetailsURLHash(props.location.hash);
  112. const eventIDInQueryParam = !!(
  113. isTraceTransaction(props.transaction) &&
  114. hashValues?.eventId &&
  115. hashValues.eventId === props.transaction.event_id
  116. );
  117. const isHighlighted = !!(!hashValues?.spanId && eventIDInQueryParam);
  118. const highlightEmbeddedSpan = !!(hashValues?.spanId && eventIDInQueryParam);
  119. const [showEmbeddedChildren, setShowEmbeddedChildren] = useState(
  120. isHighlighted || highlightEmbeddedSpan
  121. );
  122. const [isIntersecting, setIntersecting] = useState(false);
  123. const transactionRowDOMRef = createRef<HTMLDivElement>();
  124. const transactionTitleRef = createRef<HTMLDivElement>();
  125. let spanContentRef: HTMLDivElement | null = null;
  126. const router = useRouter();
  127. const handleWheel = useCallback(
  128. (event: WheelEvent) => {
  129. // https://stackoverflow.com/q/57358640
  130. // https://github.com/facebook/react/issues/14856
  131. if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
  132. return;
  133. }
  134. event.preventDefault();
  135. event.stopPropagation();
  136. if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
  137. return;
  138. }
  139. const {onWheel} = props;
  140. onWheel(event.deltaX);
  141. },
  142. [props]
  143. );
  144. const scrollIntoView = useCallback(() => {
  145. const element = transactionRowDOMRef.current;
  146. if (!element) {
  147. return;
  148. }
  149. const boundingRect = element.getBoundingClientRect();
  150. const offset = boundingRect.top + window.scrollY - TRANSACTION_BAR_HEIGHT;
  151. window.scrollTo(0, offset);
  152. props.onBarScrolledTo();
  153. }, [transactionRowDOMRef, props]);
  154. useEffect(() => {
  155. const {transaction, isBarScrolledTo} = props;
  156. const observer = new IntersectionObserver(([entry]) =>
  157. setIntersecting(entry.isIntersecting)
  158. );
  159. if (transactionRowDOMRef.current) {
  160. observer.observe(transactionRowDOMRef.current);
  161. }
  162. if (
  163. 'event_id' in transaction &&
  164. hashValues?.eventId === transaction.event_id &&
  165. !isIntersecting &&
  166. !isBarScrolledTo
  167. ) {
  168. scrollIntoView();
  169. }
  170. if (isIntersecting) {
  171. props.onBarScrolledTo();
  172. }
  173. return () => {
  174. observer.disconnect();
  175. };
  176. }, [
  177. setIntersecting,
  178. hashValues?.eventId,
  179. hashValues?.spanId,
  180. props,
  181. scrollIntoView,
  182. isIntersecting,
  183. transactionRowDOMRef,
  184. ]);
  185. useEffect(() => {
  186. const transactionTitleRefCurrentCopy = transactionTitleRef.current;
  187. if (transactionTitleRefCurrentCopy) {
  188. transactionTitleRefCurrentCopy.addEventListener('wheel', handleWheel, {
  189. passive: false,
  190. });
  191. }
  192. return () => {
  193. if (transactionTitleRefCurrentCopy) {
  194. transactionTitleRefCurrentCopy.removeEventListener('wheel', handleWheel);
  195. }
  196. };
  197. }, [handleWheel, props, transactionTitleRef]);
  198. const transactionEvent =
  199. isTraceTransaction<TraceFullDetailed>(props.transaction) ||
  200. isTraceError(props.transaction)
  201. ? props.transaction
  202. : undefined;
  203. const {
  204. data: embeddedChildren,
  205. isLoading: isEmbeddedChildrenLoading,
  206. error: embeddedChildrenError,
  207. } = useApiQuery<EventTransaction>(
  208. [
  209. `/organizations/${props.organization.slug}/events/${transactionEvent?.project_slug}:${transactionEvent?.event_id}/`,
  210. ],
  211. {
  212. staleTime: 2 * 60 * 1000,
  213. enabled: showEmbeddedChildren || isHighlighted,
  214. }
  215. );
  216. const waterfallModel = useMemo(() => {
  217. return embeddedChildren
  218. ? new WaterfallModel(
  219. embeddedChildren,
  220. undefined,
  221. undefined,
  222. undefined,
  223. props.traceInfo
  224. )
  225. : null;
  226. }, [embeddedChildren, props.traceInfo]);
  227. useEffect(() => {
  228. if (isTraceTransaction(props.transaction) && !isTraceError(props.transaction)) {
  229. if (isHighlighted && props.onRowClick) {
  230. props.onRowClick({
  231. traceFullDetailedEvent: props.transaction,
  232. event: embeddedChildren,
  233. });
  234. }
  235. }
  236. }, [isHighlighted, embeddedChildren, props, props.transaction]);
  237. const renderEmbeddedChildrenState = () => {
  238. if (showEmbeddedChildren) {
  239. if (isEmbeddedChildrenLoading) {
  240. return (
  241. <MessageRow>
  242. <span>{t('Loading embedded transaction')}</span>
  243. </MessageRow>
  244. );
  245. }
  246. if (embeddedChildrenError) {
  247. return (
  248. <MessageRow>
  249. <span>{t('Error loading embedded transaction')}</span>
  250. </MessageRow>
  251. );
  252. }
  253. }
  254. return null;
  255. };
  256. const handleRowCellClick = () => {
  257. const {transaction, organization, location} = props;
  258. if (isTraceError(transaction)) {
  259. browserHistory.push(generateIssueEventTarget(transaction, organization));
  260. return;
  261. }
  262. if (isTraceTransaction<TraceFullDetailed>(transaction)) {
  263. router.replace({
  264. ...location,
  265. hash: transactionTargetHash(transaction.event_id),
  266. });
  267. }
  268. };
  269. const getCurrentOffset = () => {
  270. const {transaction} = props;
  271. const {generation} = transaction;
  272. return getOffset(generation);
  273. };
  274. const renderMeasurements = () => {
  275. const {measurements, generateBounds} = props;
  276. if (!measurements) {
  277. return null;
  278. }
  279. return (
  280. <Fragment>
  281. {Array.from(measurements.values()).map(verticalMark => {
  282. const mark = Object.values(verticalMark.marks)[0];
  283. const {timestamp} = mark;
  284. const bounds = getMeasurementBounds(timestamp, generateBounds);
  285. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  286. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  287. return null;
  288. }
  289. return (
  290. <MeasurementMarker
  291. key={String(timestamp)}
  292. style={{
  293. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  294. }}
  295. failedThreshold={verticalMark.failedThreshold}
  296. />
  297. );
  298. })}
  299. </Fragment>
  300. );
  301. };
  302. const renderConnector = (hasToggle: boolean) => {
  303. const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = props;
  304. const {generation = 0} = transaction;
  305. const eventId =
  306. isTraceTransaction<TraceFullDetailed>(transaction) || isTraceError(transaction)
  307. ? transaction.event_id
  308. : transaction.traceSlug;
  309. if (generation === 0) {
  310. if (hasToggle) {
  311. return (
  312. <ConnectorBar
  313. style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
  314. orphanBranch={false}
  315. />
  316. );
  317. }
  318. return null;
  319. }
  320. const connectorBars: Array<React.ReactNode> = continuingDepths.map(
  321. ({depth, isOrphanDepth}) => {
  322. if (generation - depth <= 1) {
  323. // If the difference is less than or equal to 1, then it means that the continued
  324. // bar is from its direct parent. In this case, do not render a connector bar
  325. // because the tree connector below will suffice.
  326. return null;
  327. }
  328. const left = -1 * getOffset(generation - depth - 1) - 2;
  329. return (
  330. <ConnectorBar
  331. style={{left}}
  332. key={`${eventId}-${depth}`}
  333. orphanBranch={isOrphanDepth}
  334. />
  335. );
  336. }
  337. );
  338. const embeddedChildrenLength =
  339. (embeddedChildren && waterfallModel && waterfallModel.rootSpan.children.length) ??
  340. 0;
  341. if (
  342. hasToggle &&
  343. (isExpanded || (showEmbeddedChildren && embeddedChildrenLength > 0))
  344. ) {
  345. connectorBars.push(
  346. <ConnectorBar
  347. style={{
  348. right: '15px',
  349. height: '10px',
  350. bottom: isLast ? `-${ROW_HEIGHT / 2 + 1}px` : '0',
  351. top: 'auto',
  352. }}
  353. key={`${eventId}-last`}
  354. orphanBranch={false}
  355. />
  356. );
  357. }
  358. return (
  359. <TreeConnector isLast={isLast} hasToggler={hasToggle} orphanBranch={isOrphan}>
  360. {connectorBars}
  361. </TreeConnector>
  362. );
  363. };
  364. const renderEmbeddedTransactionsBadge = (): React.ReactNode => {
  365. return (
  366. <Tooltip
  367. title={
  368. <span>
  369. {showEmbeddedChildren
  370. ? t(
  371. 'This transaction is showing a direct child. Remove transaction to hide'
  372. )
  373. : t('This transaction has a direct child. Add transaction to view')}
  374. </span>
  375. }
  376. position="top"
  377. containerDisplayMode="block"
  378. delay={400}
  379. >
  380. <StyledZoomIcon
  381. isZoomIn={!showEmbeddedChildren}
  382. onClick={() => {
  383. setShowEmbeddedChildren(prev => !prev);
  384. if (
  385. (props.isExpanded && !showEmbeddedChildren) ||
  386. (!props.isExpanded && showEmbeddedChildren)
  387. ) {
  388. props.toggleExpandedState();
  389. }
  390. }}
  391. />
  392. </Tooltip>
  393. );
  394. };
  395. const renderEmbeddedChildren = () => {
  396. if (!embeddedChildren || !showEmbeddedChildren || !waterfallModel) {
  397. return null;
  398. }
  399. const {
  400. organization,
  401. traceViewRef,
  402. location,
  403. isLast,
  404. traceInfo,
  405. isExpanded,
  406. toggleExpandedState,
  407. } = props;
  408. const profileId = embeddedChildren.contexts?.profile?.profile_id ?? null;
  409. if (isExpanded) {
  410. toggleExpandedState();
  411. }
  412. return (
  413. <Fragment>
  414. <QuickTraceQuery
  415. event={embeddedChildren}
  416. location={location}
  417. orgSlug={organization.slug}
  418. >
  419. {results => (
  420. <ProfilesProvider
  421. orgSlug={organization.slug}
  422. projectSlug={embeddedChildren.projectSlug ?? ''}
  423. profileId={profileId || ''}
  424. >
  425. <ProfileContext.Consumer>
  426. {profiles => (
  427. <ProfileGroupProvider
  428. type="flamechart"
  429. input={profiles?.type === 'resolved' ? profiles.data : null}
  430. traceID={profileId || ''}
  431. >
  432. <TransactionProfileIdProvider
  433. projectId={embeddedChildren.projectID}
  434. timestamp={embeddedChildren.dateReceived}
  435. transactionId={embeddedChildren.id}
  436. >
  437. <SpanContext.Provider>
  438. <SpanContext.Consumer>
  439. {spanContextProps => (
  440. <Observer>
  441. {() => (
  442. <NewTraceDetailsSpanTree
  443. measurements={props.measurements}
  444. quickTrace={results}
  445. location={props.location}
  446. onRowClick={props.onRowClick}
  447. traceInfo={traceInfo}
  448. traceViewHeaderRef={traceViewRef}
  449. traceViewRef={traceViewRef}
  450. parentContinuingDepths={props.continuingDepths}
  451. traceHasMultipleRoots={props.continuingDepths.some(
  452. c => c.depth === 0 && c.isOrphanDepth
  453. )}
  454. parentIsOrphan={props.isOrphan}
  455. parentIsLast={isLast}
  456. parentGeneration={transaction.generation ?? 0}
  457. organization={organization}
  458. waterfallModel={waterfallModel}
  459. filterSpans={waterfallModel.filterSpans}
  460. spans={waterfallModel
  461. .getWaterfall({
  462. viewStart: 0,
  463. viewEnd: 1,
  464. })
  465. .slice(1)}
  466. focusedSpanIds={waterfallModel.focusedSpanIds}
  467. spanContextProps={spanContextProps}
  468. operationNameFilters={
  469. waterfallModel.operationNameFilters
  470. }
  471. />
  472. )}
  473. </Observer>
  474. )}
  475. </SpanContext.Consumer>
  476. </SpanContext.Provider>
  477. </TransactionProfileIdProvider>
  478. </ProfileGroupProvider>
  479. )}
  480. </ProfileContext.Consumer>
  481. </ProfilesProvider>
  482. )}
  483. </QuickTraceQuery>
  484. </Fragment>
  485. );
  486. };
  487. const renderToggle = (errored: boolean) => {
  488. const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = props;
  489. const left = getCurrentOffset();
  490. const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0;
  491. let childrenLength: number | string =
  492. (!isTraceError(transaction) && transaction.children?.length) || 0;
  493. const generation = transaction.generation || 0;
  494. if (childrenLength <= 0 && !hasOrphanErrors && !showEmbeddedChildren) {
  495. return (
  496. <TreeToggleContainer style={{left: `${left}px`}}>
  497. {renderConnector(false)}
  498. </TreeToggleContainer>
  499. );
  500. }
  501. if (showEmbeddedChildren) {
  502. childrenLength =
  503. embeddedChildren && waterfallModel
  504. ? waterfallModel.rootSpan.children.length
  505. : '?';
  506. } else {
  507. childrenLength = childrenLength + (numOfOrphanErrors ?? 0);
  508. }
  509. const isRoot = generation === 0;
  510. return (
  511. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  512. {renderConnector(true)}
  513. <TreeToggle
  514. disabled={isRoot}
  515. isExpanded={isExpanded}
  516. errored={errored}
  517. onClick={event => {
  518. event.stopPropagation();
  519. if (isRoot || showEmbeddedChildren) {
  520. return;
  521. }
  522. toggleExpandedState();
  523. setShowEmbeddedChildren(false);
  524. }}
  525. >
  526. <span>{childrenLength}</span>
  527. {!isRoot && !showEmbeddedChildren && (
  528. <div>
  529. <TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
  530. </div>
  531. )}
  532. </TreeToggle>
  533. </TreeToggleContainer>
  534. );
  535. };
  536. const renderTitle = (_: ScrollbarManager.ScrollbarManagerChildrenProps) => {
  537. const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
  538. props;
  539. const left = getCurrentOffset();
  540. const errored = isTraceTransaction<TraceFullDetailed>(transaction)
  541. ? transaction.errors &&
  542. transaction.errors.length + transaction.performance_issues.length > 0
  543. : false;
  544. const projectBadge = (isTraceTransaction<TraceFullDetailed>(transaction) ||
  545. isTraceError(transaction)) && (
  546. <Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
  547. {({projects}) => {
  548. const project = projects.find(p => p.slug === transaction.project_slug);
  549. return (
  550. <ProjectBadgeContainer>
  551. <Tooltip title={transaction.project_slug}>
  552. <ProjectBadge
  553. project={project ? project : {slug: transaction.project_slug}}
  554. avatarSize={16}
  555. hideName
  556. />
  557. </Tooltip>
  558. </ProjectBadgeContainer>
  559. );
  560. }}
  561. </Projects>
  562. );
  563. const content = isTraceError(transaction) ? (
  564. <Fragment>
  565. {projectBadge}
  566. <RowTitleContent errored>
  567. <ErrorLink to={generateIssueEventTarget(transaction, organization)}>
  568. <strong>{'Unknown \u2014 '}</strong>
  569. {shortenErrorTitle(transaction.title)}
  570. </ErrorLink>
  571. </RowTitleContent>
  572. </Fragment>
  573. ) : isTraceTransaction<TraceFullDetailed>(transaction) ? (
  574. <Fragment>
  575. {projectBadge}
  576. <RowTitleContent errored={errored}>
  577. <strong>
  578. {transaction['transaction.op']}
  579. {' \u2014 '}
  580. </strong>
  581. {transaction.transaction}
  582. </RowTitleContent>
  583. </Fragment>
  584. ) : (
  585. <RowTitleContent errored={false}>
  586. <strong>{'Trace \u2014 '}</strong>
  587. {transaction.traceSlug}
  588. </RowTitleContent>
  589. );
  590. return (
  591. <RowTitleContainer
  592. ref={ref => {
  593. if (!ref) {
  594. removeContentSpanBarRef(spanContentRef);
  595. return;
  596. }
  597. addContentSpanBarRef(ref);
  598. spanContentRef = ref;
  599. }}
  600. >
  601. {renderToggle(errored)}
  602. <RowTitle
  603. style={{
  604. left: `${left}px`,
  605. width: '100%',
  606. }}
  607. >
  608. {content}
  609. </RowTitle>
  610. </RowTitleContainer>
  611. );
  612. };
  613. const renderDivider = (
  614. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  615. ) => {
  616. if (isHighlighted) {
  617. // Mock component to preserve layout spacing
  618. return (
  619. <DividerLine
  620. showDetail={isHighlighted}
  621. style={{
  622. position: 'absolute',
  623. }}
  624. />
  625. );
  626. }
  627. const {addDividerLineRef} = dividerHandlerChildrenProps;
  628. return (
  629. <DividerLine
  630. ref={addDividerLineRef()}
  631. style={{
  632. position: 'absolute',
  633. }}
  634. onMouseEnter={() => {
  635. dividerHandlerChildrenProps.setHover(true);
  636. }}
  637. onMouseLeave={() => {
  638. dividerHandlerChildrenProps.setHover(false);
  639. }}
  640. onMouseOver={() => {
  641. dividerHandlerChildrenProps.setHover(true);
  642. }}
  643. onMouseDown={e => {
  644. dividerHandlerChildrenProps.onDragStart(e);
  645. }}
  646. onClick={event => {
  647. // we prevent the propagation of the clicks from this component to prevent
  648. // the span detail from being opened.
  649. event.stopPropagation();
  650. }}
  651. />
  652. );
  653. };
  654. const renderGhostDivider = (
  655. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  656. ) => {
  657. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  658. return (
  659. <DividerLineGhostContainer
  660. style={{
  661. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  662. display: 'none',
  663. }}
  664. >
  665. <DividerLine
  666. ref={addGhostDividerLineRef()}
  667. style={{
  668. right: 0,
  669. }}
  670. className="hovering"
  671. onClick={event => {
  672. // the ghost divider line should not be interactive.
  673. // we prevent the propagation of the clicks from this component to prevent
  674. // the span detail from being opened.
  675. event.stopPropagation();
  676. }}
  677. />
  678. </DividerLineGhostContainer>
  679. );
  680. };
  681. const renderErrorBadge = () => {
  682. const {transaction} = props;
  683. if (
  684. isTraceRoot(transaction) ||
  685. isTraceError(transaction) ||
  686. !(transaction.errors.length + transaction.performance_issues.length)
  687. ) {
  688. return null;
  689. }
  690. return <ErrorBadge />;
  691. };
  692. const renderRectangle = () => {
  693. const {transaction, traceInfo, barColor} = props;
  694. // Use 1 as the difference in the case that startTimestamp === endTimestamp
  695. const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
  696. const start_timestamp = isTraceError(transaction)
  697. ? transaction.timestamp
  698. : transaction.start_timestamp;
  699. if (!(start_timestamp && transaction.timestamp)) {
  700. return null;
  701. }
  702. const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp);
  703. const startPercentage = startPosition / delta;
  704. const duration = Math.abs(transaction.timestamp - start_timestamp);
  705. const widthPercentage = duration / delta;
  706. return (
  707. <StyledRowRectangle
  708. style={{
  709. backgroundColor: barColor,
  710. left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
  711. width: toPercent(widthPercentage || 0),
  712. }}
  713. >
  714. {renderPerformanceIssues()}
  715. {isTraceError(transaction) ? (
  716. <ErrorBadge />
  717. ) : (
  718. <Fragment>
  719. {renderErrorBadge()}
  720. <DurationPill
  721. durationDisplay={getDurationDisplay({
  722. left: startPercentage,
  723. width: widthPercentage,
  724. })}
  725. showDetail={isHighlighted}
  726. >
  727. {getHumanDuration(duration)}
  728. </DurationPill>
  729. </Fragment>
  730. )}
  731. </StyledRowRectangle>
  732. );
  733. };
  734. const renderPerformanceIssues = () => {
  735. const {transaction, barColor} = props;
  736. if (isTraceError(transaction) || isTraceRoot(transaction)) {
  737. return null;
  738. }
  739. const rows: React.ReactElement[] = [];
  740. // Use 1 as the difference in the case that startTimestamp === endTimestamp
  741. const delta = Math.abs(transaction.timestamp - transaction.start_timestamp) || 1;
  742. for (let i = 0; i < transaction.performance_issues.length; i++) {
  743. const issue = transaction.performance_issues[i];
  744. const startPosition = Math.abs(issue.start - transaction.start_timestamp);
  745. const startPercentage = startPosition / delta;
  746. const duration = Math.abs(issue.end - issue.start);
  747. const widthPercentage = duration / delta;
  748. rows.push(
  749. <RowRectangle
  750. style={{
  751. backgroundColor: barColor,
  752. left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
  753. width: toPercent(widthPercentage || 0),
  754. }}
  755. spanBarType={SpanBarType.AFFECTED}
  756. />
  757. );
  758. }
  759. return rows;
  760. };
  761. const renderHeader = ({
  762. dividerHandlerChildrenProps,
  763. scrollbarManagerChildrenProps,
  764. }: {
  765. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
  766. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
  767. }) => {
  768. const {hasGuideAnchor, index, transaction, onlyOrphanErrors = false} = props;
  769. const {dividerPosition} = dividerHandlerChildrenProps;
  770. const hideDurationRectangle = isTraceRoot(transaction) && onlyOrphanErrors;
  771. return (
  772. <RowCellContainer showDetail={isHighlighted}>
  773. <RowCell
  774. data-test-id="transaction-row-title"
  775. data-type="span-row-cell"
  776. style={{
  777. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  778. paddingTop: 0,
  779. }}
  780. showDetail={isHighlighted}
  781. onClick={handleRowCellClick}
  782. ref={transactionTitleRef}
  783. >
  784. <GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
  785. {renderTitle(scrollbarManagerChildrenProps)}
  786. </GuideAnchor>
  787. </RowCell>
  788. <DividerContainer>
  789. {renderDivider(dividerHandlerChildrenProps)}
  790. {!isTraceRoot(transaction) &&
  791. !isTraceError(transaction) &&
  792. renderEmbeddedTransactionsBadge()}
  793. </DividerContainer>
  794. <RowCell
  795. data-test-id="transaction-row-duration"
  796. data-type="span-row-cell"
  797. showStriping={index % 2 !== 0}
  798. style={{
  799. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  800. paddingTop: 0,
  801. overflow: 'visible',
  802. }}
  803. showDetail={isHighlighted}
  804. onClick={handleRowCellClick}
  805. >
  806. <RowReplayTimeIndicators />
  807. <GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
  808. {!hideDurationRectangle && renderRectangle()}
  809. {renderMeasurements()}
  810. </GuideAnchor>
  811. </RowCell>
  812. {!isHighlighted && renderGhostDivider(dividerHandlerChildrenProps)}
  813. </RowCellContainer>
  814. );
  815. };
  816. const {isVisible, transaction} = props;
  817. return (
  818. <div>
  819. <StyledRow
  820. ref={transactionRowDOMRef}
  821. visible={isVisible}
  822. showBorder={isHighlighted}
  823. cursor={
  824. isTraceTransaction<TraceFullDetailed>(transaction) ? 'pointer' : 'default'
  825. }
  826. >
  827. <ScrollbarManager.Consumer>
  828. {scrollbarManagerChildrenProps => (
  829. <DividerHandlerManager.Consumer>
  830. {dividerHandlerChildrenProps =>
  831. renderHeader({
  832. dividerHandlerChildrenProps,
  833. scrollbarManagerChildrenProps,
  834. })
  835. }
  836. </DividerHandlerManager.Consumer>
  837. )}
  838. </ScrollbarManager.Consumer>
  839. </StyledRow>
  840. {renderEmbeddedChildrenState()}
  841. {renderEmbeddedChildren()}
  842. </div>
  843. );
  844. }
  845. function getOffset(generation) {
  846. return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  847. }
  848. export default NewTraceDetailsTransactionBar;
  849. const StyledRow = styled(Row)`
  850. &,
  851. ${RowCellContainer} {
  852. overflow: visible;
  853. }
  854. `;
  855. const ErrorLink = styled(Link)`
  856. color: ${p => p.theme.error};
  857. `;
  858. const StyledRowRectangle = styled(RowRectangle)`
  859. display: flex;
  860. align-items: center;
  861. `;
  862. export const StyledZoomIcon = styled(IconZoom)`
  863. position: absolute;
  864. left: -20px;
  865. top: 4px;
  866. height: 16px;
  867. width: 18px;
  868. z-index: 1000;
  869. background: ${p => p.theme.background};
  870. padding: 1px;
  871. border: 1px solid ${p => p.theme.border};
  872. border-radius: ${space(0.5)};
  873. `;