newTraceDetailsTransactionBar.tsx 30 KB

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