newTraceDetailsTransactionBar.tsx 30 KB

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