newTraceDetailsTransactionBar.tsx 30 KB

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