newTraceDetailsTransactionBar.tsx 30 KB

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