newTraceDetailsTransactionBar.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  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} from 'sentry/types/event';
  65. import type {Organization} from 'sentry/types/organization';
  66. import {defined} from 'sentry/utils';
  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 {useNavigate} from 'sentry/utils/useNavigate';
  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 navigate = useNavigate();
  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. isPending: 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. navigate(generateIssueEventTarget(transaction, organization));
  270. return;
  271. }
  272. if (isTraceTransaction<TraceFullDetailed>(transaction)) {
  273. navigate(
  274. {
  275. ...location,
  276. hash: transactionTargetHash(transaction.event_id),
  277. query: {
  278. ...location.query,
  279. openPanel: 'open',
  280. },
  281. },
  282. {replace: true}
  283. );
  284. }
  285. };
  286. const getCurrentOffset = () => {
  287. const {transaction} = props;
  288. const {generation} = transaction;
  289. return getOffset(generation);
  290. };
  291. const renderMeasurements = () => {
  292. const {measurements, generateBounds} = props;
  293. if (!measurements) {
  294. return null;
  295. }
  296. return (
  297. <Fragment>
  298. {Array.from(measurements.values()).map(verticalMark => {
  299. const mark = Object.values(verticalMark.marks)[0];
  300. const {timestamp} = mark;
  301. const bounds = getMeasurementBounds(timestamp, generateBounds);
  302. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  303. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  304. return null;
  305. }
  306. return (
  307. <MeasurementMarker
  308. key={String(timestamp)}
  309. style={{
  310. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  311. }}
  312. failedThreshold={verticalMark.failedThreshold}
  313. />
  314. );
  315. })}
  316. </Fragment>
  317. );
  318. };
  319. const renderConnector = (hasToggle: boolean) => {
  320. const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = props;
  321. const {generation = 0} = transaction;
  322. const eventId =
  323. isTraceTransaction<TraceFullDetailed>(transaction) || isTraceError(transaction)
  324. ? transaction.event_id
  325. : transaction.traceSlug;
  326. if (generation === 0) {
  327. if (hasToggle) {
  328. return (
  329. <ConnectorBar
  330. style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
  331. orphanBranch={false}
  332. />
  333. );
  334. }
  335. return null;
  336. }
  337. const connectorBars: Array<React.ReactNode> = continuingDepths.map(
  338. ({depth, isOrphanDepth}) => {
  339. if (generation - depth <= 1) {
  340. // If the difference is less than or equal to 1, then it means that the continued
  341. // bar is from its direct parent. In this case, do not render a connector bar
  342. // because the tree connector below will suffice.
  343. return null;
  344. }
  345. const left = -1 * getOffset(generation - depth - 1) - 2;
  346. return (
  347. <ConnectorBar
  348. style={{left}}
  349. key={`${eventId}-${depth}`}
  350. orphanBranch={isOrphanDepth}
  351. />
  352. );
  353. }
  354. );
  355. const embeddedChildrenLength =
  356. (embeddedChildren && waterfallModel && waterfallModel.rootSpan.children.length) ??
  357. 0;
  358. if (
  359. hasToggle &&
  360. (isExpanded || (showEmbeddedChildren && embeddedChildrenLength > 0))
  361. ) {
  362. connectorBars.push(
  363. <ConnectorBar
  364. style={{
  365. right: '15px',
  366. height: '10px',
  367. bottom: isLast ? `-${ROW_HEIGHT / 2 + 1}px` : '0',
  368. top: 'auto',
  369. }}
  370. key={`${eventId}-last`}
  371. orphanBranch={false}
  372. />
  373. );
  374. }
  375. return (
  376. <TreeConnector isLast={isLast} hasToggler={hasToggle} orphanBranch={isOrphan}>
  377. {connectorBars}
  378. </TreeConnector>
  379. );
  380. };
  381. const renderEmbeddedTransactionsBadge = (): React.ReactNode => {
  382. return (
  383. <Tooltip
  384. title={
  385. <span>
  386. {showEmbeddedChildren
  387. ? t(
  388. 'This transaction is showing a direct child. Remove transaction to hide'
  389. )
  390. : t('This transaction has a direct child. Add transaction to view')}
  391. </span>
  392. }
  393. position="top"
  394. containerDisplayMode="block"
  395. delay={400}
  396. >
  397. <StyledZoomIcon
  398. isZoomIn={!showEmbeddedChildren}
  399. onClick={() => {
  400. setShowEmbeddedChildren(prev => !prev);
  401. if (
  402. (props.isExpanded && !showEmbeddedChildren) ||
  403. (!props.isExpanded && showEmbeddedChildren)
  404. ) {
  405. props.toggleExpandedState();
  406. }
  407. }}
  408. />
  409. </Tooltip>
  410. );
  411. };
  412. const renderEmbeddedChildren = () => {
  413. if (!embeddedChildren || !showEmbeddedChildren || !waterfallModel) {
  414. return null;
  415. }
  416. const {
  417. organization,
  418. traceViewRef,
  419. location,
  420. isLast,
  421. traceInfo,
  422. isExpanded,
  423. toggleExpandedState,
  424. } = props;
  425. const profileId = embeddedChildren.contexts?.profile?.profile_id ?? null;
  426. if (isExpanded) {
  427. toggleExpandedState();
  428. }
  429. return (
  430. <Fragment>
  431. <QuickTraceQuery
  432. event={embeddedChildren}
  433. location={location}
  434. orgSlug={organization.slug}
  435. skipLight={false}
  436. >
  437. {results => (
  438. <ProfilesProvider
  439. orgSlug={organization.slug}
  440. projectSlug={embeddedChildren.projectSlug ?? ''}
  441. profileId={profileId || ''}
  442. >
  443. <ProfileContext.Consumer>
  444. {profiles => (
  445. <ProfileGroupProvider
  446. type="flamechart"
  447. input={profiles?.type === 'resolved' ? profiles.data : null}
  448. traceID={profileId || ''}
  449. >
  450. <TransactionProfileIdProvider
  451. projectId={embeddedChildren.projectID}
  452. timestamp={embeddedChildren.dateReceived}
  453. transactionId={embeddedChildren.id}
  454. >
  455. <SpanContext.Provider>
  456. <SpanContext.Consumer>
  457. {spanContextProps => (
  458. <Observer>
  459. {() => (
  460. <NewTraceDetailsSpanTree
  461. measurements={props.measurements}
  462. quickTrace={results}
  463. location={props.location}
  464. onRowClick={props.onRowClick}
  465. traceInfo={traceInfo}
  466. traceViewHeaderRef={traceViewRef}
  467. traceViewRef={traceViewRef}
  468. parentContinuingDepths={props.continuingDepths}
  469. traceHasMultipleRoots={props.continuingDepths.some(
  470. c => c.depth === 0 && c.isOrphanDepth
  471. )}
  472. parentIsOrphan={props.isOrphan}
  473. parentIsLast={isLast}
  474. parentGeneration={transaction.generation ?? 0}
  475. organization={organization}
  476. waterfallModel={waterfallModel}
  477. filterSpans={waterfallModel.filterSpans}
  478. spans={waterfallModel
  479. .getWaterfall({
  480. viewStart: 0,
  481. viewEnd: 1,
  482. })
  483. .slice(1)}
  484. focusedSpanIds={waterfallModel.focusedSpanIds}
  485. spanContextProps={spanContextProps}
  486. operationNameFilters={
  487. waterfallModel.operationNameFilters
  488. }
  489. />
  490. )}
  491. </Observer>
  492. )}
  493. </SpanContext.Consumer>
  494. </SpanContext.Provider>
  495. </TransactionProfileIdProvider>
  496. </ProfileGroupProvider>
  497. )}
  498. </ProfileContext.Consumer>
  499. </ProfilesProvider>
  500. )}
  501. </QuickTraceQuery>
  502. </Fragment>
  503. );
  504. };
  505. const renderToggle = (errored: boolean) => {
  506. const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = props;
  507. const left = getCurrentOffset();
  508. const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0;
  509. let childrenLength: number | string =
  510. (!isTraceError(transaction) && transaction.children?.length) || 0;
  511. const generation = transaction.generation || 0;
  512. if (childrenLength <= 0 && !hasOrphanErrors && !showEmbeddedChildren) {
  513. return (
  514. <TreeToggleContainer style={{left: `${left}px`}}>
  515. {renderConnector(false)}
  516. </TreeToggleContainer>
  517. );
  518. }
  519. if (showEmbeddedChildren) {
  520. childrenLength =
  521. embeddedChildren && waterfallModel
  522. ? waterfallModel.rootSpan.children.length
  523. : '?';
  524. } else {
  525. childrenLength = childrenLength + (numOfOrphanErrors ?? 0);
  526. }
  527. const isRoot = generation === 0;
  528. return (
  529. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  530. {renderConnector(true)}
  531. <TreeToggle
  532. disabled={isRoot}
  533. isExpanded={isExpanded}
  534. errored={errored}
  535. onClick={event => {
  536. event.stopPropagation();
  537. if (isRoot || showEmbeddedChildren) {
  538. return;
  539. }
  540. toggleExpandedState();
  541. setShowEmbeddedChildren(false);
  542. }}
  543. >
  544. <span>{childrenLength}</span>
  545. {!isRoot && !showEmbeddedChildren && (
  546. <div>
  547. <TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
  548. </div>
  549. )}
  550. </TreeToggle>
  551. </TreeToggleContainer>
  552. );
  553. };
  554. const renderTitle = (_: ScrollbarManager.ScrollbarManagerChildrenProps) => {
  555. const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
  556. props;
  557. const left = getCurrentOffset();
  558. const errored = isTraceTransaction<TraceFullDetailed>(transaction)
  559. ? transaction.errors &&
  560. transaction.errors.length + transaction.performance_issues.length > 0
  561. : false;
  562. const projectBadge = (isTraceTransaction<TraceFullDetailed>(transaction) ||
  563. isTraceError(transaction)) && (
  564. <Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
  565. {({projects}) => {
  566. const project = projects.find(p => p.slug === transaction.project_slug);
  567. return (
  568. <ProjectBadgeContainer>
  569. <Tooltip title={transaction.project_slug}>
  570. <ProjectBadge
  571. project={project ? project : {slug: transaction.project_slug}}
  572. avatarSize={16}
  573. hideName
  574. />
  575. </Tooltip>
  576. </ProjectBadgeContainer>
  577. );
  578. }}
  579. </Projects>
  580. );
  581. const content = isTraceError(transaction) ? (
  582. <Fragment>
  583. {projectBadge}
  584. <RowTitleContent errored>
  585. <ErrorLink to={generateIssueEventTarget(transaction, organization)}>
  586. <strong>{'Unknown \u2014 '}</strong>
  587. {shortenErrorTitle(transaction.title)}
  588. </ErrorLink>
  589. </RowTitleContent>
  590. </Fragment>
  591. ) : isTraceTransaction<TraceFullDetailed>(transaction) ? (
  592. <Fragment>
  593. {projectBadge}
  594. <RowTitleContent errored={errored}>
  595. <strong>
  596. {transaction['transaction.op']}
  597. {' \u2014 '}
  598. </strong>
  599. {transaction.transaction}
  600. </RowTitleContent>
  601. </Fragment>
  602. ) : (
  603. <RowTitleContent errored={false}>
  604. <strong>{'Trace \u2014 '}</strong>
  605. {transaction.traceSlug}
  606. </RowTitleContent>
  607. );
  608. return (
  609. <RowTitleContainer
  610. ref={ref => {
  611. if (!ref) {
  612. removeContentSpanBarRef(spanContentRef);
  613. return;
  614. }
  615. addContentSpanBarRef(ref);
  616. spanContentRef = ref;
  617. }}
  618. >
  619. {renderToggle(errored)}
  620. <RowTitle
  621. style={{
  622. left: `${left}px`,
  623. width: '100%',
  624. }}
  625. >
  626. {content}
  627. </RowTitle>
  628. </RowTitleContainer>
  629. );
  630. };
  631. const renderDivider = (
  632. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  633. ) => {
  634. if (isHighlighted) {
  635. // Mock component to preserve layout spacing
  636. return (
  637. <DividerLine
  638. showDetail={isHighlighted}
  639. style={{
  640. position: 'absolute',
  641. }}
  642. />
  643. );
  644. }
  645. const {addDividerLineRef} = dividerHandlerChildrenProps;
  646. return (
  647. <DividerLine
  648. ref={addDividerLineRef()}
  649. style={{
  650. position: 'absolute',
  651. }}
  652. onMouseEnter={() => {
  653. dividerHandlerChildrenProps.setHover(true);
  654. }}
  655. onMouseLeave={() => {
  656. dividerHandlerChildrenProps.setHover(false);
  657. }}
  658. onMouseOver={() => {
  659. dividerHandlerChildrenProps.setHover(true);
  660. }}
  661. onMouseDown={e => {
  662. dividerHandlerChildrenProps.onDragStart(e);
  663. }}
  664. onClick={event => {
  665. // we prevent the propagation of the clicks from this component to prevent
  666. // the span detail from being opened.
  667. event.stopPropagation();
  668. }}
  669. />
  670. );
  671. };
  672. const renderGhostDivider = (
  673. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  674. ) => {
  675. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  676. return (
  677. <DividerLineGhostContainer
  678. style={{
  679. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  680. display: 'none',
  681. }}
  682. >
  683. <DividerLine
  684. ref={addGhostDividerLineRef()}
  685. style={{
  686. right: 0,
  687. }}
  688. className="hovering"
  689. onClick={event => {
  690. // the ghost divider line should not be interactive.
  691. // we prevent the propagation of the clicks from this component to prevent
  692. // the span detail from being opened.
  693. event.stopPropagation();
  694. }}
  695. />
  696. </DividerLineGhostContainer>
  697. );
  698. };
  699. const renderErrorBadge = () => {
  700. const {transaction} = props;
  701. if (
  702. isTraceRoot(transaction) ||
  703. isTraceError(transaction) ||
  704. !(transaction.errors.length + transaction.performance_issues.length)
  705. ) {
  706. return null;
  707. }
  708. return <ErrorBadge />;
  709. };
  710. const renderMetricsBadge = () => {
  711. const {organization} = props;
  712. const hasMetrics = Object.keys(embeddedChildren?._metrics_summary ?? {}).length > 0;
  713. if (
  714. !hasMetricsExperimentalFeature(organization) ||
  715. isTraceRoot(transaction) ||
  716. isTraceError(transaction) ||
  717. !hasMetrics
  718. ) {
  719. return null;
  720. }
  721. return <MetricsBadge />;
  722. };
  723. const renderRectangle = () => {
  724. const {transaction, traceInfo, barColor} = props;
  725. // Use 1 as the difference in the case that startTimestamp === endTimestamp
  726. const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
  727. const start_timestamp = isTraceError(transaction)
  728. ? transaction.timestamp
  729. : transaction.start_timestamp;
  730. if (!(start_timestamp && transaction.timestamp)) {
  731. return null;
  732. }
  733. const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp);
  734. const startPercentage = startPosition / delta;
  735. const duration = Math.abs(transaction.timestamp - start_timestamp);
  736. const widthPercentage = duration / delta;
  737. return (
  738. <StyledRowRectangle
  739. style={{
  740. backgroundColor: barColor,
  741. left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
  742. width: toPercent(widthPercentage || 0),
  743. }}
  744. >
  745. {renderPerformanceIssues()}
  746. {isTraceError(transaction) ? (
  747. <ErrorBadge />
  748. ) : (
  749. <Fragment>
  750. {renderMetricsBadge()}
  751. {renderErrorBadge()}
  752. <DurationPill
  753. durationDisplay={getDurationDisplay({
  754. left: startPercentage,
  755. width: widthPercentage,
  756. })}
  757. showDetail={isHighlighted}
  758. >
  759. {getHumanDuration(duration)}
  760. </DurationPill>
  761. </Fragment>
  762. )}
  763. </StyledRowRectangle>
  764. );
  765. };
  766. const renderPerformanceIssues = () => {
  767. const {transaction, barColor} = props;
  768. if (isTraceError(transaction) || isTraceRoot(transaction)) {
  769. return null;
  770. }
  771. const rows: React.ReactElement[] = [];
  772. // Use 1 as the difference in the case that startTimestamp === endTimestamp
  773. const delta = Math.abs(transaction.timestamp - transaction.start_timestamp) || 1;
  774. for (let i = 0; i < transaction.performance_issues.length; i++) {
  775. const issue = transaction.performance_issues[i];
  776. const startPosition = Math.abs(issue.start - transaction.start_timestamp);
  777. const startPercentage = startPosition / delta;
  778. const duration = Math.abs(issue.end - issue.start);
  779. const widthPercentage = duration / delta;
  780. rows.push(
  781. <RowRectangle
  782. style={{
  783. backgroundColor: barColor,
  784. left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
  785. width: toPercent(widthPercentage || 0),
  786. }}
  787. spanBarType={SpanBarType.AFFECTED}
  788. />
  789. );
  790. }
  791. return rows;
  792. };
  793. const renderHeader = ({
  794. dividerHandlerChildrenProps,
  795. scrollbarManagerChildrenProps,
  796. }: {
  797. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
  798. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
  799. }) => {
  800. const {hasGuideAnchor, index, transaction, onlyOrphanErrors = false} = props;
  801. const {dividerPosition} = dividerHandlerChildrenProps;
  802. const hideDurationRectangle = isTraceRoot(transaction) && onlyOrphanErrors;
  803. return (
  804. <RowCellContainer showDetail={isHighlighted}>
  805. <RowCell
  806. data-test-id="transaction-row-title"
  807. data-type="span-row-cell"
  808. style={{
  809. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  810. paddingTop: 0,
  811. }}
  812. showDetail={isHighlighted}
  813. onClick={handleRowCellClick}
  814. ref={transactionTitleRef}
  815. >
  816. <GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
  817. {renderTitle(scrollbarManagerChildrenProps)}
  818. </GuideAnchor>
  819. </RowCell>
  820. <DividerContainer>
  821. {renderDivider(dividerHandlerChildrenProps)}
  822. {!isTraceRoot(transaction) &&
  823. !isTraceError(transaction) &&
  824. renderEmbeddedTransactionsBadge()}
  825. </DividerContainer>
  826. <RowCell
  827. data-test-id="transaction-row-duration"
  828. data-type="span-row-cell"
  829. showStriping={index % 2 !== 0}
  830. style={{
  831. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  832. paddingTop: 0,
  833. overflow: 'visible',
  834. }}
  835. showDetail={isHighlighted}
  836. onClick={handleRowCellClick}
  837. >
  838. <RowReplayTimeIndicators />
  839. <GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
  840. {!hideDurationRectangle && renderRectangle()}
  841. {renderMeasurements()}
  842. </GuideAnchor>
  843. </RowCell>
  844. {!isHighlighted && renderGhostDivider(dividerHandlerChildrenProps)}
  845. </RowCellContainer>
  846. );
  847. };
  848. const {isVisible, transaction} = props;
  849. return (
  850. <div>
  851. <StyledRow
  852. ref={transactionRowDOMRef}
  853. visible={isVisible}
  854. showBorder={isHighlighted}
  855. cursor={
  856. isTraceTransaction<TraceFullDetailed>(transaction) ? 'pointer' : 'default'
  857. }
  858. >
  859. <ScrollbarManager.Consumer>
  860. {scrollbarManagerChildrenProps => (
  861. <DividerHandlerManager.Consumer>
  862. {dividerHandlerChildrenProps =>
  863. renderHeader({
  864. dividerHandlerChildrenProps,
  865. scrollbarManagerChildrenProps,
  866. })
  867. }
  868. </DividerHandlerManager.Consumer>
  869. )}
  870. </ScrollbarManager.Consumer>
  871. </StyledRow>
  872. {renderEmbeddedChildrenState()}
  873. {renderEmbeddedChildren()}
  874. </div>
  875. );
  876. }
  877. function getOffset(generation) {
  878. return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  879. }
  880. export default NewTraceDetailsTransactionBar;
  881. const StyledRow = styled(Row)`
  882. &,
  883. ${RowCellContainer} {
  884. overflow: visible;
  885. }
  886. `;
  887. const ErrorLink = styled(Link)`
  888. color: ${p => p.theme.error};
  889. `;
  890. const StyledRowRectangle = styled(RowRectangle)`
  891. display: flex;
  892. align-items: center;
  893. `;
  894. export const StyledZoomIcon = styled(IconZoom)`
  895. position: absolute;
  896. left: -20px;
  897. top: 4px;
  898. height: 16px;
  899. width: 18px;
  900. z-index: 1000;
  901. background: ${p => p.theme.background};
  902. padding: 1px;
  903. border: 1px solid ${p => p.theme.border};
  904. border-radius: ${space(0.5)};
  905. `;