styles.tsx 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414
  1. import {Fragment, type PropsWithChildren, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {LocationDescriptor} from 'history';
  4. import {Button, LinkButton} from 'sentry/components/button';
  5. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  6. import {
  7. DropdownMenu,
  8. type DropdownMenuProps,
  9. type MenuItemProps,
  10. } from 'sentry/components/dropdownMenu';
  11. import EventTagsDataSection from 'sentry/components/events/eventTagsAndScreenshot/tags';
  12. import {generateStats} from 'sentry/components/events/opsBreakdown';
  13. import {DataSection} from 'sentry/components/events/styles';
  14. import FileSize from 'sentry/components/fileSize';
  15. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  16. import KeyValueData, {
  17. CardPanel,
  18. type KeyValueDataContentProps,
  19. Subject,
  20. ValueSection,
  21. } from 'sentry/components/keyValueData';
  22. import {LazyRender, type LazyRenderProps} from 'sentry/components/lazyRender';
  23. import Link from 'sentry/components/links/link';
  24. import Panel from 'sentry/components/panels/panel';
  25. import PanelBody from 'sentry/components/panels/panelBody';
  26. import PanelHeader from 'sentry/components/panels/panelHeader';
  27. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  28. import QuestionTooltip from 'sentry/components/questionTooltip';
  29. import {Tooltip} from 'sentry/components/tooltip';
  30. import {
  31. IconChevron,
  32. IconCircleFill,
  33. IconEllipsis,
  34. IconFocus,
  35. IconJson,
  36. IconOpen,
  37. IconPanel,
  38. IconProfiling,
  39. } from 'sentry/icons';
  40. import {t, tct} from 'sentry/locale';
  41. import {space} from 'sentry/styles/space';
  42. import type {Event, EventTransaction} from 'sentry/types/event';
  43. import type {KeyValueListData} from 'sentry/types/group';
  44. import type {Organization} from 'sentry/types/organization';
  45. import type {Project} from 'sentry/types/project';
  46. import {defined} from 'sentry/utils';
  47. import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10';
  48. import getDuration from 'sentry/utils/duration/getDuration';
  49. import {FieldValueType, getFieldDefinition} from 'sentry/utils/fields';
  50. import type {Color, ColorOrAlias} from 'sentry/utils/theme';
  51. import {useLocation} from 'sentry/utils/useLocation';
  52. import {useNavigate} from 'sentry/utils/useNavigate';
  53. import useOrganization from 'sentry/utils/useOrganization';
  54. import {useParams} from 'sentry/utils/useParams';
  55. import {
  56. SENTRY_SPAN_NUMBER_TAGS,
  57. SENTRY_SPAN_STRING_TAGS,
  58. } from 'sentry/views/explore/constants';
  59. import {traceAnalytics} from '../../traceAnalytics';
  60. import {useTransaction} from '../../traceApi/useTransaction';
  61. import {useDrawerContainerRef} from '../../traceDrawer/details/drawerContainerRefContext';
  62. import {
  63. makeTraceContinuousProfilingLink,
  64. makeTransactionProfilingLink,
  65. } from '../../traceDrawer/traceProfilingLink';
  66. import {
  67. isAutogroupedNode,
  68. isMissingInstrumentationNode,
  69. isRootNode,
  70. isSpanNode,
  71. isTraceErrorNode,
  72. isTransactionNode,
  73. } from '../../traceGuards';
  74. import type {MissingInstrumentationNode} from '../../traceModels/missingInstrumentationNode';
  75. import type {ParentAutogroupNode} from '../../traceModels/parentAutogroupNode';
  76. import type {SiblingAutogroupNode} from '../../traceModels/siblingAutogroupNode';
  77. import {TraceTree} from '../../traceModels/traceTree';
  78. import type {TraceTreeNode} from '../../traceModels/traceTreeNode';
  79. import {useTraceState, useTraceStateDispatch} from '../../traceState/traceStateProvider';
  80. import {useHasTraceNewUi} from '../../useHasTraceNewUi';
  81. import {
  82. getSearchInExploreTarget,
  83. TraceDrawerActionKind,
  84. TraceDrawerActionValueKind,
  85. } from './utils';
  86. const BodyContainer = styled('div')<{hasNewTraceUi?: boolean}>`
  87. display: flex;
  88. flex-direction: column;
  89. gap: ${p => (p.hasNewTraceUi ? 0 : space(2))};
  90. padding: ${p => (p.hasNewTraceUi ? `${space(0.5)} ${space(2)}` : space(1))};
  91. height: calc(100% - 52px);
  92. overflow: auto;
  93. ${DataSection} {
  94. padding: 0;
  95. }
  96. `;
  97. const DetailContainer = styled('div')`
  98. height: 100%;
  99. overflow: hidden;
  100. padding-bottom: ${space(1)};
  101. `;
  102. const FlexBox = styled('div')`
  103. display: flex;
  104. align-items: center;
  105. `;
  106. const Actions = styled(FlexBox)`
  107. gap: ${space(0.5)};
  108. justify-content: end;
  109. width: 100%;
  110. `;
  111. const Title = styled(FlexBox)`
  112. gap: ${space(1)};
  113. flex-grow: 1;
  114. overflow: hidden;
  115. > span {
  116. min-width: 30px;
  117. }
  118. `;
  119. const LegacyTitleText = styled('div')`
  120. ${p => p.theme.overflowEllipsis}
  121. `;
  122. const TitleText = styled('div')`
  123. font-size: ${p => p.theme.fontSizeExtraLarge};
  124. font-weight: bold;
  125. `;
  126. function TitleWithTestId(props: PropsWithChildren<{}>) {
  127. return <Title data-test-id="trace-drawer-title">{props.children}</Title>;
  128. }
  129. function SubtitleWithCopyButton({
  130. text,
  131. hideCopyButton = false,
  132. }: {
  133. text: string;
  134. hideCopyButton?: boolean;
  135. }) {
  136. return (
  137. <SubTitleWrapper>
  138. <StyledSubTitleText>{text}</StyledSubTitleText>
  139. {!hideCopyButton ? (
  140. <CopyToClipboardButton
  141. borderless
  142. size="zero"
  143. iconSize="xs"
  144. text={text}
  145. tooltipProps={{disabled: true}}
  146. />
  147. ) : null}
  148. </SubTitleWrapper>
  149. );
  150. }
  151. const SubTitleWrapper = styled(FlexBox)`
  152. ${p => p.theme.overflowEllipsis}
  153. `;
  154. const StyledSubTitleText = styled('span')`
  155. font-size: ${p => p.theme.fontSizeMedium};
  156. color: ${p => p.theme.subText};
  157. `;
  158. function TitleOp({text}: {text: string}) {
  159. return (
  160. <Tooltip
  161. title={
  162. <Fragment>
  163. {text}
  164. <CopyToClipboardButton
  165. borderless
  166. size="zero"
  167. iconSize="xs"
  168. text={text}
  169. tooltipProps={{disabled: true}}
  170. />
  171. </Fragment>
  172. }
  173. showOnlyOnOverflow
  174. isHoverable
  175. >
  176. <TitleOpText>{text}</TitleOpText>
  177. </Tooltip>
  178. );
  179. }
  180. const Type = styled('div')`
  181. font-size: ${p => p.theme.fontSizeSmall};
  182. `;
  183. const TitleOpText = styled('div')`
  184. font-size: 15px;
  185. font-weight: ${p => p.theme.fontWeightBold};
  186. ${p => p.theme.overflowEllipsis}
  187. `;
  188. const Table = styled('table')`
  189. margin-bottom: 0 !important;
  190. td {
  191. overflow: hidden;
  192. }
  193. `;
  194. const IconTitleWrapper = styled(FlexBox)`
  195. gap: ${space(1)};
  196. min-width: 30px;
  197. `;
  198. const IconBorder = styled('div')<{backgroundColor: string; errored?: boolean}>`
  199. background-color: ${p => p.backgroundColor};
  200. border-radius: ${p => p.theme.borderRadius};
  201. padding: 0;
  202. display: flex;
  203. align-items: center;
  204. justify-content: center;
  205. width: 30px;
  206. height: 30px;
  207. min-width: 30px;
  208. svg {
  209. fill: ${p => p.theme.white};
  210. width: 14px;
  211. height: 14px;
  212. }
  213. `;
  214. const LegacyHeaderContainer = styled(FlexBox)`
  215. margin: ${space(1)};
  216. justify-content: space-between;
  217. gap: ${space(3)};
  218. container-type: inline-size;
  219. @container (max-width: 780px) {
  220. .DropdownMenu {
  221. display: block;
  222. }
  223. .Actions {
  224. display: none;
  225. }
  226. }
  227. @container (min-width: 781px) {
  228. .DropdownMenu {
  229. display: none;
  230. }
  231. }
  232. `;
  233. const HeaderContainer = styled(FlexBox)`
  234. align-items: baseline;
  235. justify-content: space-between;
  236. gap: ${space(3)};
  237. padding: ${space(0.25)} 0 ${space(0.5)} 0;
  238. margin: 0 ${space(2)} ${space(1)} ${space(2)};
  239. border-bottom: 1px solid ${p => p.theme.border};
  240. `;
  241. const DURATION_COMPARISON_STATUS_COLORS: {
  242. equal: {light: ColorOrAlias; normal: ColorOrAlias};
  243. faster: {light: ColorOrAlias; normal: ColorOrAlias};
  244. slower: {light: ColorOrAlias; normal: ColorOrAlias};
  245. } = {
  246. faster: {
  247. light: 'green100',
  248. normal: 'green300',
  249. },
  250. slower: {
  251. light: 'red100',
  252. normal: 'red300',
  253. },
  254. equal: {
  255. light: 'gray100',
  256. normal: 'gray300',
  257. },
  258. };
  259. const MIN_PCT_DURATION_DIFFERENCE = 10;
  260. type DurationComparison = {
  261. deltaPct: number;
  262. deltaText: JSX.Element;
  263. status: 'faster' | 'slower' | 'equal';
  264. } | null;
  265. const getDurationComparison = (
  266. baseline: number | undefined,
  267. duration: number,
  268. baseDescription?: string
  269. ): DurationComparison => {
  270. if (!baseline) {
  271. return null;
  272. }
  273. const delta = duration - baseline;
  274. const deltaPct = Math.round(Math.abs((delta / baseline) * 100));
  275. const status = delta > 0 ? 'slower' : delta < 0 ? 'faster' : 'equal';
  276. const formattedBaseDuration = (
  277. <Tooltip
  278. title={baseDescription}
  279. showUnderline
  280. underlineColor={DURATION_COMPARISON_STATUS_COLORS[status].normal}
  281. >
  282. {getDuration(baseline, 2, true)}
  283. </Tooltip>
  284. );
  285. const deltaText =
  286. status === 'equal'
  287. ? tct(`equal to avg [formattedBaseDuration]`, {
  288. formattedBaseDuration,
  289. })
  290. : status === 'faster'
  291. ? tct(`[deltaPct] faster than avg [formattedBaseDuration]`, {
  292. formattedBaseDuration,
  293. deltaPct: `${deltaPct}%`,
  294. })
  295. : tct(`[deltaPct] slower than avg [formattedBaseDuration]`, {
  296. formattedBaseDuration,
  297. deltaPct: `${deltaPct}%`,
  298. });
  299. return {deltaPct, status, deltaText};
  300. };
  301. type DurationProps = {
  302. baseline: number | undefined;
  303. duration: number;
  304. node: TraceTreeNode<TraceTree.NodeValue>;
  305. baseDescription?: string;
  306. ratio?: number;
  307. };
  308. function Duration(props: DurationProps) {
  309. if (typeof props.duration !== 'number' || Number.isNaN(props.duration)) {
  310. return <DurationContainer>{t('unknown')}</DurationContainer>;
  311. }
  312. // Since transactions have ms precision, we show 2 decimal places only if the duration is greater than 1 second.
  313. const precision = isTransactionNode(props.node) ? (props.duration > 1 ? 2 : 0) : 2;
  314. if (props.baseline === undefined || props.baseline === 0) {
  315. return (
  316. <DurationContainer>
  317. {getDuration(props.duration, precision, true)}
  318. </DurationContainer>
  319. );
  320. }
  321. const comparison = getDurationComparison(
  322. props.baseline,
  323. props.duration,
  324. props.baseDescription
  325. );
  326. return (
  327. <Fragment>
  328. <DurationContainer>
  329. {getDuration(props.duration, precision, true)}{' '}
  330. {props.ratio ? `(${(props.ratio * 100).toFixed()}%)` : null}
  331. </DurationContainer>
  332. {comparison && comparison.deltaPct >= MIN_PCT_DURATION_DIFFERENCE ? (
  333. <Comparison status={comparison.status}>{comparison.deltaText}</Comparison>
  334. ) : null}
  335. </Fragment>
  336. );
  337. }
  338. function TableRow({
  339. title,
  340. keep,
  341. children,
  342. prefix,
  343. extra = null,
  344. toolTipText,
  345. }: {
  346. children: React.ReactNode;
  347. title: JSX.Element | string | null;
  348. extra?: React.ReactNode;
  349. keep?: boolean;
  350. prefix?: JSX.Element;
  351. toolTipText?: string;
  352. }) {
  353. if (!keep && !children) {
  354. return null;
  355. }
  356. return (
  357. <tr>
  358. <td className="key">
  359. <Flex>
  360. {prefix}
  361. {title}
  362. {toolTipText ? <StyledQuestionTooltip size="xs" title={toolTipText} /> : null}
  363. </Flex>
  364. </td>
  365. <ValueTd className="value">
  366. <TableValueRow>
  367. <StyledPre>
  368. <span className="val-string">{children}</span>
  369. </StyledPre>
  370. <TableRowButtonContainer>{extra}</TableRowButtonContainer>
  371. </TableValueRow>
  372. </ValueTd>
  373. </tr>
  374. );
  375. }
  376. type HighlightProps = {
  377. avgDuration: number | undefined;
  378. bodyContent: React.ReactNode;
  379. headerContent: React.ReactNode;
  380. node: TraceTreeNode<TraceTree.NodeValue>;
  381. project: Project | undefined;
  382. transaction: EventTransaction | undefined;
  383. };
  384. function Highlights({
  385. node,
  386. transaction: event,
  387. avgDuration,
  388. project,
  389. headerContent,
  390. bodyContent,
  391. }: HighlightProps) {
  392. if (!isTransactionNode(node) && !isSpanNode(node)) {
  393. return null;
  394. }
  395. const startTimestamp = node.space[0];
  396. const endTimestamp = node.space[0] + node.space[1];
  397. const durationInSeconds = (endTimestamp - startTimestamp) / 1e3;
  398. const comparison = getDurationComparison(
  399. avgDuration,
  400. durationInSeconds,
  401. t('Average duration for this transaction over the last 24 hours')
  402. );
  403. return (
  404. <Fragment>
  405. <HighlightsWrapper>
  406. <HighlightsLeftColumn>
  407. <Tooltip title={node.value?.project_slug}>
  408. <ProjectBadge
  409. project={project ? project : {slug: node.value?.project_slug ?? ''}}
  410. avatarSize={18}
  411. hideName
  412. />
  413. </Tooltip>
  414. <VerticalLine />
  415. </HighlightsLeftColumn>
  416. <HighlightsRightColumn>
  417. <HighlightOp>
  418. {isTransactionNode(node) ? node.value?.['transaction.op'] : node.value?.op}
  419. </HighlightOp>
  420. <HighlightsDurationWrapper>
  421. <HighlightDuration>
  422. {getDuration(durationInSeconds, 2, true)}
  423. </HighlightDuration>
  424. {comparison && comparison.deltaPct >= MIN_PCT_DURATION_DIFFERENCE ? (
  425. <HiglightsDurationComparison status={comparison.status}>
  426. {comparison.deltaText}
  427. </HiglightsDurationComparison>
  428. ) : null}
  429. </HighlightsDurationWrapper>
  430. <StyledPanel>
  431. <StyledPanelHeader>{headerContent}</StyledPanelHeader>
  432. <PanelBody>{bodyContent}</PanelBody>
  433. </StyledPanel>
  434. {event ? <HighLightsOpsBreakdown event={event} /> : null}
  435. </HighlightsRightColumn>
  436. </HighlightsWrapper>
  437. <SectionDivider />
  438. </Fragment>
  439. );
  440. }
  441. const StyledPanel = styled(Panel)`
  442. margin-bottom: 0;
  443. `;
  444. function HighLightsOpsBreakdown({event}: {event: EventTransaction}) {
  445. const breakdown = generateStats(event, {type: 'no_filter'});
  446. return (
  447. <HighlightsOpsBreakdownWrapper>
  448. <HighlightsSpanCount>
  449. {t('Most frequent span ops for this transaction are')}
  450. </HighlightsSpanCount>
  451. <TopOpsList>
  452. {breakdown.slice(0, 3).map(currOp => {
  453. const {name, percentage} = currOp;
  454. const operationName = typeof name === 'string' ? name : t('Other');
  455. const color = pickBarColor(operationName);
  456. const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞';
  457. return (
  458. <HighlightsOpRow key={operationName}>
  459. <IconCircleFill size="xs" color={color as Color} />
  460. {operationName}
  461. <HighlightsOpPct>{pctLabel}%</HighlightsOpPct>
  462. </HighlightsOpRow>
  463. );
  464. })}
  465. </TopOpsList>
  466. </HighlightsOpsBreakdownWrapper>
  467. );
  468. }
  469. const TopOpsList = styled('div')`
  470. display: flex;
  471. flex-direction: row;
  472. gap: ${space(1)};
  473. `;
  474. const HighlightsOpPct = styled('div')`
  475. color: ${p => p.theme.subText};
  476. font-size: 14px;
  477. `;
  478. const HighlightsSpanCount = styled('div')`
  479. margin-bottom: ${space(0.25)};
  480. `;
  481. const HighlightsOpRow = styled(FlexBox)`
  482. font-size: 13px;
  483. gap: ${space(0.5)};
  484. `;
  485. const HighlightsOpsBreakdownWrapper = styled(FlexBox)`
  486. align-items: flex-start;
  487. flex-direction: column;
  488. gap: ${space(0.25)};
  489. margin-top: ${space(1.5)};
  490. `;
  491. const HiglightsDurationComparison = styled('div')<
  492. Pick<NonNullable<DurationComparison>, 'status'>
  493. >`
  494. white-space: nowrap;
  495. border-radius: 12px;
  496. color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].normal]};
  497. background-color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].light]};
  498. border: solid 1px ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].light]};
  499. font-size: ${p => p.theme.fontSizeExtraSmall};
  500. padding: ${space(0.25)} ${space(1)};
  501. display: inline-block;
  502. height: 21px;
  503. `;
  504. const HighlightsDurationWrapper = styled(FlexBox)`
  505. gap: ${space(1)};
  506. margin-bottom: ${space(1)};
  507. `;
  508. const HighlightDuration = styled('div')`
  509. font-size: ${p => p.theme.headerFontSize};
  510. font-weight: 400;
  511. `;
  512. const HighlightOp = styled('div')`
  513. font-weight: bold;
  514. font-size: ${p => p.theme.fontSizeMedium};
  515. line-height: normal;
  516. `;
  517. const StyledPanelHeader = styled(PanelHeader)`
  518. font-weight: normal;
  519. padding: 0;
  520. line-height: normal;
  521. text-transform: none;
  522. overflow: hidden;
  523. `;
  524. const SectionDivider = styled('hr')`
  525. border-color: ${p => p.theme.translucentBorder};
  526. margin: ${space(3)} 0 ${space(1.5)} 0;
  527. `;
  528. const VerticalLine = styled('div')`
  529. width: 1px;
  530. height: 100%;
  531. background-color: ${p => p.theme.border};
  532. margin-top: ${space(0.5)};
  533. `;
  534. const HighlightsWrapper = styled('div')`
  535. display: flex;
  536. align-items: stretch;
  537. gap: ${space(1)};
  538. width: 100%;
  539. margin: ${space(1)} 0;
  540. `;
  541. const HighlightsLeftColumn = styled('div')`
  542. display: flex;
  543. flex-direction: column;
  544. justify-content: center;
  545. align-items: center;
  546. `;
  547. const HighlightsRightColumn = styled('div')`
  548. display: flex;
  549. flex-direction: column;
  550. justify-content: left;
  551. height: 100%;
  552. flex: 1;
  553. overflow: hidden;
  554. `;
  555. function IssuesLink({
  556. node,
  557. children,
  558. }: {
  559. children: React.ReactNode;
  560. node: TraceTreeNode<TraceTree.NodeValue>;
  561. }) {
  562. const organization = useOrganization();
  563. const params = useParams<{traceSlug?: string}>();
  564. const traceSlug = params.traceSlug?.trim() ?? '';
  565. // Adding a buffer of 15mins for errors only traces, where there is no concept of
  566. // trace duration and start equals end timestamps.
  567. const buffer = node.space[1] > 0 ? 0 : 15 * 60 * 1000;
  568. return (
  569. <Link
  570. to={{
  571. pathname: `/organizations/${organization.slug}/issues/`,
  572. query: {
  573. query: `trace:${traceSlug}`,
  574. start: new Date(node.space[0] - buffer).toISOString(),
  575. end: new Date(node.space[0] + node.space[1] + buffer).toISOString(),
  576. // If we don't pass the project param, the issues page will filter by the last selected project.
  577. // Traces can have multiple projects, so we query issues by all projects and rely on our search query to filter the results.
  578. project: -1,
  579. },
  580. }}
  581. >
  582. {children}
  583. </Link>
  584. );
  585. }
  586. const LAZY_RENDER_PROPS: Partial<LazyRenderProps> = {
  587. observerOptions: {rootMargin: '50px'},
  588. };
  589. const DurationContainer = styled('span')`
  590. font-weight: ${p => p.theme.fontWeightBold};
  591. margin-right: ${space(1)};
  592. `;
  593. const Comparison = styled('span')<{status: 'faster' | 'slower' | 'equal'}>`
  594. color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].normal]};
  595. `;
  596. const Flex = styled('div')`
  597. display: flex;
  598. align-items: center;
  599. `;
  600. const TableValueRow = styled('div')`
  601. display: grid;
  602. grid-template-columns: auto min-content;
  603. gap: ${space(1)};
  604. border-radius: 4px;
  605. background-color: ${p => p.theme.surface200};
  606. margin: 2px;
  607. `;
  608. const StyledQuestionTooltip = styled(QuestionTooltip)`
  609. margin-left: ${space(0.5)};
  610. `;
  611. const StyledPre = styled('pre')`
  612. margin: 0 !important;
  613. background-color: transparent !important;
  614. `;
  615. const TableRowButtonContainer = styled('div')`
  616. padding: 8px 10px;
  617. `;
  618. const ValueTd = styled('td')`
  619. position: relative;
  620. `;
  621. function getThreadIdFromNode(
  622. node: TraceTreeNode<TraceTree.NodeValue>,
  623. transaction: EventTransaction | undefined
  624. ): string | undefined {
  625. if (isSpanNode(node) && node.value.data?.['thread.id']) {
  626. return node.value.data['thread.id'];
  627. }
  628. if (transaction) {
  629. return transaction.contexts?.trace?.data?.['thread.id'];
  630. }
  631. return undefined;
  632. }
  633. // Renders the dropdown menu list at the root trace drawer content container level, to prevent
  634. // being stacked under other content.
  635. function DropdownMenuWithPortal(props: DropdownMenuProps) {
  636. const drawerContainerRef = useDrawerContainerRef();
  637. return (
  638. <DropdownMenu
  639. {...props}
  640. usePortal={!!drawerContainerRef}
  641. portalContainerRef={drawerContainerRef}
  642. />
  643. );
  644. }
  645. type KeyValueActionProps = {
  646. rowKey: string;
  647. rowValue: React.ReactNode;
  648. kind?: TraceDrawerActionValueKind;
  649. };
  650. function KeyValueAction({
  651. rowKey,
  652. rowValue,
  653. kind = TraceDrawerActionValueKind.SENTRY_TAG,
  654. }: KeyValueActionProps) {
  655. const location = useLocation();
  656. const organization = useOrganization();
  657. const hasNewTraceUi = useHasTraceNewUi();
  658. const hasTraceDrawerAction = organization.features.includes('trace-drawer-action');
  659. const [isVisible, setIsVisible] = useState(false);
  660. if (
  661. !hasNewTraceUi ||
  662. !hasTraceDrawerAction ||
  663. !defined(rowValue) ||
  664. !defined(rowKey) ||
  665. !['string', 'number'].includes(typeof rowValue)
  666. ) {
  667. return null;
  668. }
  669. // We assume that tags, measurements and additional data (span.data) are dynamic lists of searchable keys in explore.
  670. // Any other key must exist in the static list of sentry tags to be deemed searchable.
  671. if (
  672. kind === TraceDrawerActionValueKind.SENTRY_TAG &&
  673. !(
  674. SENTRY_SPAN_NUMBER_TAGS.includes(rowKey) || SENTRY_SPAN_STRING_TAGS.includes(rowKey)
  675. )
  676. ) {
  677. return null;
  678. }
  679. const dropdownOptions = [
  680. {
  681. key: 'include',
  682. label: t('Find more samples with this value'),
  683. to: getSearchInExploreTarget(
  684. organization,
  685. location,
  686. rowKey,
  687. rowValue.toString(),
  688. TraceDrawerActionKind.INCLUDE
  689. ),
  690. },
  691. {
  692. key: 'exclude',
  693. label: t('Find samples excluding this value'),
  694. to: getSearchInExploreTarget(
  695. organization,
  696. location,
  697. rowKey,
  698. rowValue.toLocaleString(),
  699. TraceDrawerActionKind.EXCLUDE
  700. ),
  701. },
  702. ];
  703. const valueType = getFieldDefinition(rowKey)?.valueType;
  704. const isMeasurement =
  705. valueType &&
  706. [
  707. FieldValueType.DURATION,
  708. FieldValueType.NUMBER,
  709. FieldValueType.INTEGER,
  710. FieldValueType.PERCENTAGE,
  711. ].includes(valueType);
  712. if (isMeasurement) {
  713. dropdownOptions.push(
  714. {
  715. key: 'includeGreaterThan',
  716. label: t('Find samples with values greater than'),
  717. to: getSearchInExploreTarget(
  718. organization,
  719. location,
  720. rowKey,
  721. rowValue.toString(),
  722. TraceDrawerActionKind.GREATER_THAN
  723. ),
  724. },
  725. {
  726. key: 'includeLessThan',
  727. label: t('Find samples with values less than'),
  728. to: getSearchInExploreTarget(
  729. organization,
  730. location,
  731. rowKey,
  732. rowValue.toString(),
  733. TraceDrawerActionKind.LESS_THAN
  734. ),
  735. }
  736. );
  737. }
  738. return (
  739. <KeyValueActionDropdown
  740. preventOverflowOptions={{padding: 4}}
  741. className={isVisible ? '' : 'invisible'}
  742. position="bottom-end"
  743. size="xs"
  744. onOpenChange={isOpen => setIsVisible(isOpen)}
  745. triggerProps={{
  746. 'aria-label': t('Key Value Action Menu'),
  747. icon: <IconEllipsis />,
  748. showChevron: false,
  749. className: 'trigger-button',
  750. }}
  751. onAction={key => {
  752. traceAnalytics.trackExploreSearch(
  753. organization,
  754. rowKey,
  755. rowValue.toString(),
  756. key as TraceDrawerActionKind
  757. );
  758. }}
  759. items={dropdownOptions}
  760. />
  761. );
  762. }
  763. const KeyValueActionDropdown = styled(DropdownMenu)`
  764. display: block;
  765. margin: 1px;
  766. height: 20px;
  767. .trigger-button {
  768. height: 20px;
  769. min-height: 20px;
  770. padding: 0 ${space(0.75)};
  771. border-radius: ${space(0.5)};
  772. z-index: 1;
  773. }
  774. `;
  775. function TypeSafeBoolean<T>(value: T | null | undefined): value is NonNullable<T> {
  776. return value !== null && value !== undefined;
  777. }
  778. function PanelPositionDropDown({organization}: {organization: Organization}) {
  779. const traceState = useTraceState();
  780. const traceDispatch = useTraceStateDispatch();
  781. const options: MenuItemProps[] = [];
  782. const layoutOptions = traceState.preferences.drawer.layoutOptions;
  783. if (layoutOptions.includes('drawer left')) {
  784. options.push({
  785. key: 'drawer-left',
  786. onAction: () => {
  787. traceAnalytics.trackLayoutChange('drawer left', organization);
  788. traceDispatch({type: 'set layout', payload: 'drawer left'});
  789. },
  790. leadingItems: <IconPanel direction="left" size="xs" />,
  791. label: t('Left'),
  792. disabled: traceState.preferences.layout === 'drawer left',
  793. });
  794. }
  795. if (layoutOptions.includes('drawer right')) {
  796. options.push({
  797. key: 'drawer-right',
  798. onAction: () => {
  799. traceAnalytics.trackLayoutChange('drawer right', organization);
  800. traceDispatch({type: 'set layout', payload: 'drawer right'});
  801. },
  802. leadingItems: <IconPanel direction="right" size="xs" />,
  803. label: t('Right'),
  804. disabled: traceState.preferences.layout === 'drawer right',
  805. });
  806. }
  807. if (layoutOptions.includes('drawer bottom')) {
  808. options.push({
  809. key: 'drawer-bottom',
  810. onAction: () => {
  811. traceAnalytics.trackLayoutChange('drawer bottom', organization);
  812. traceDispatch({type: 'set layout', payload: 'drawer bottom'});
  813. },
  814. leadingItems: <IconPanel direction="down" size="xs" />,
  815. label: t('Bottom'),
  816. disabled: traceState.preferences.layout === 'drawer bottom',
  817. });
  818. }
  819. return (
  820. <DropdownMenu
  821. size="sm"
  822. items={options}
  823. menuTitle={<div>{t('Panel Position')}</div>}
  824. trigger={triggerProps => (
  825. <Tooltip title={t('Panel Position')}>
  826. <ActionButton
  827. {...triggerProps}
  828. size="xs"
  829. aria-label={t('Panel position')}
  830. icon={<IconPanel direction="right" size="xs" />}
  831. />
  832. </Tooltip>
  833. )}
  834. />
  835. );
  836. }
  837. function NodeActions(props: {
  838. node: TraceTreeNode<any>;
  839. onTabScrollToNode: (
  840. node:
  841. | TraceTreeNode<any>
  842. | ParentAutogroupNode
  843. | SiblingAutogroupNode
  844. | MissingInstrumentationNode
  845. ) => void;
  846. organization: Organization;
  847. eventSize?: number | undefined;
  848. }) {
  849. const hasNewTraceUi = useHasTraceNewUi();
  850. const organization = useOrganization();
  851. const params = useParams<{traceSlug?: string}>();
  852. const {data: transaction} = useTransaction({
  853. node: isTransactionNode(props.node) ? props.node : null,
  854. organization,
  855. });
  856. const transactionProfileTarget = useMemo(() => {
  857. const profileId = isTransactionNode(props.node)
  858. ? props.node.value.profile_id
  859. : isSpanNode(props.node)
  860. ? props.node.event?.contexts?.profile?.profile_id ?? ''
  861. : '';
  862. if (!profileId) {
  863. return null;
  864. }
  865. return makeTransactionProfilingLink(profileId, {
  866. orgSlug: props.organization.slug,
  867. projectSlug: props.node.metadata.project_slug ?? '',
  868. });
  869. }, [props.node, props.organization]);
  870. const continuousProfileTarget = useMemo(() => {
  871. const profilerId = isTransactionNode(props.node)
  872. ? props.node.value.profiler_id
  873. : isSpanNode(props.node)
  874. ? props.node.value.sentry_tags?.profiler_id ?? null
  875. : null;
  876. if (!profilerId) {
  877. return null;
  878. }
  879. return makeTraceContinuousProfilingLink(props.node, profilerId, {
  880. orgSlug: props.organization.slug,
  881. projectSlug: props.node.metadata.project_slug ?? '',
  882. traceId: params.traceSlug ?? '',
  883. threadId: getThreadIdFromNode(props.node, transaction),
  884. });
  885. }, [params.traceSlug, props.node, props.organization, transaction]);
  886. if (!hasNewTraceUi) {
  887. return (
  888. <LegacyNodeActions
  889. {...props}
  890. continuousProfileTarget={continuousProfileTarget}
  891. transactionProfileTarget={transactionProfileTarget}
  892. />
  893. );
  894. }
  895. return (
  896. <ActionWrapper>
  897. <Tooltip title={t('Show in view')}>
  898. <ActionButton
  899. onClick={_e => {
  900. traceAnalytics.trackShowInView(props.organization);
  901. props.onTabScrollToNode(props.node);
  902. }}
  903. size="xs"
  904. aria-label={t('Show in view')}
  905. icon={<IconFocus size="xs" />}
  906. />
  907. </Tooltip>
  908. {isTransactionNode(props.node) ? (
  909. <Tooltip title={t('JSON')}>
  910. <ActionButton
  911. onClick={() => traceAnalytics.trackViewEventJSON(props.organization)}
  912. href={`/api/0/projects/${props.organization.slug}/${props.node.value.project_slug}/events/${props.node.value.event_id}/json/`}
  913. size="xs"
  914. aria-label={t('JSON')}
  915. icon={<IconJson size="xs" />}
  916. />
  917. </Tooltip>
  918. ) : null}
  919. {continuousProfileTarget ? (
  920. <Tooltip title={t('Profile')}>
  921. <ActionButton
  922. onClick={() => traceAnalytics.trackViewContinuousProfile(props.organization)}
  923. to={continuousProfileTarget}
  924. size="xs"
  925. aria-label={t('Profile')}
  926. icon={<IconProfiling size="xs" />}
  927. />
  928. </Tooltip>
  929. ) : transactionProfileTarget ? (
  930. <Tooltip title={t('Profile')}>
  931. <ActionButton
  932. onClick={() => traceAnalytics.trackViewTransactionProfile(props.organization)}
  933. to={transactionProfileTarget}
  934. size="xs"
  935. aria-label={t('Profile')}
  936. icon={<IconProfiling size="xs" />}
  937. />
  938. </Tooltip>
  939. ) : null}
  940. <PanelPositionDropDown organization={organization} />
  941. </ActionWrapper>
  942. );
  943. }
  944. const ActionButton = styled(Button)`
  945. border: none;
  946. background-color: transparent;
  947. box-shadow: none;
  948. transition: none !important;
  949. opacity: 0.8;
  950. height: 24px;
  951. max-height: 24px;
  952. &:hover {
  953. border: none;
  954. background-color: transparent;
  955. box-shadow: none;
  956. opacity: 1;
  957. }
  958. `;
  959. const ActionWrapper = styled('div')`
  960. display: flex;
  961. align-items: center;
  962. gap: ${space(0.25)};
  963. `;
  964. function LegacyNodeActions(props: {
  965. continuousProfileTarget: LocationDescriptor | null;
  966. node: TraceTreeNode<any>;
  967. onTabScrollToNode: (
  968. node:
  969. | TraceTreeNode<any>
  970. | ParentAutogroupNode
  971. | SiblingAutogroupNode
  972. | MissingInstrumentationNode
  973. ) => void;
  974. organization: Organization;
  975. transactionProfileTarget: LocationDescriptor | null;
  976. eventSize?: number | undefined;
  977. }) {
  978. const navigate = useNavigate();
  979. const items = useMemo((): MenuItemProps[] => {
  980. const showInView: MenuItemProps = {
  981. key: 'show-in-view',
  982. label: t('Show in View'),
  983. onAction: () => {
  984. traceAnalytics.trackShowInView(props.organization);
  985. props.onTabScrollToNode(props.node);
  986. },
  987. };
  988. const eventId =
  989. props.node.metadata.event_id ??
  990. TraceTree.ParentTransaction(props.node)?.metadata.event_id;
  991. const projectSlug =
  992. props.node.metadata.project_slug ??
  993. TraceTree.ParentTransaction(props.node)?.metadata.project_slug;
  994. const eventSize = props.eventSize;
  995. const jsonDetails: MenuItemProps = {
  996. key: 'json-details',
  997. onAction: () => {
  998. traceAnalytics.trackViewEventJSON(props.organization);
  999. window.open(
  1000. `/api/0/projects/${props.organization.slug}/${projectSlug}/events/${eventId}/json/`,
  1001. '_blank'
  1002. );
  1003. },
  1004. label:
  1005. t('JSON') +
  1006. (typeof eventSize === 'number' ? ` (${formatBytesBase10(eventSize, 0)})` : ''),
  1007. };
  1008. const profileLink: MenuItemProps | null = props.continuousProfileTarget
  1009. ? {
  1010. key: 'profile',
  1011. onAction: () => {
  1012. traceAnalytics.trackViewContinuousProfile(props.organization);
  1013. navigate(props.continuousProfileTarget!);
  1014. },
  1015. label: t('View Profile'),
  1016. }
  1017. : props.transactionProfileTarget
  1018. ? {
  1019. key: 'profile',
  1020. onAction: () => {
  1021. traceAnalytics.trackViewTransactionProfile(props.organization);
  1022. navigate(props.transactionProfileTarget!);
  1023. },
  1024. label: t('View Profile'),
  1025. }
  1026. : null;
  1027. if (isTransactionNode(props.node)) {
  1028. return [showInView, jsonDetails, profileLink].filter(TypeSafeBoolean);
  1029. }
  1030. if (isSpanNode(props.node)) {
  1031. return [showInView, profileLink].filter(TypeSafeBoolean);
  1032. }
  1033. if (isMissingInstrumentationNode(props.node)) {
  1034. return [showInView, profileLink].filter(TypeSafeBoolean);
  1035. }
  1036. if (isTraceErrorNode(props.node)) {
  1037. return [showInView, profileLink].filter(TypeSafeBoolean);
  1038. }
  1039. if (isRootNode(props.node)) {
  1040. return [showInView];
  1041. }
  1042. if (isAutogroupedNode(props.node)) {
  1043. return [showInView];
  1044. }
  1045. return [showInView];
  1046. }, [props, navigate]);
  1047. return (
  1048. <ActionsContainer>
  1049. <Actions className="Actions">
  1050. {props.continuousProfileTarget ? (
  1051. <LinkButton size="xs" to={props.continuousProfileTarget}>
  1052. {t('View Profile')}
  1053. </LinkButton>
  1054. ) : props.transactionProfileTarget ? (
  1055. <LinkButton size="xs" to={props.transactionProfileTarget}>
  1056. {t('View Profile')}
  1057. </LinkButton>
  1058. ) : null}
  1059. <Button
  1060. size="xs"
  1061. onClick={_e => {
  1062. traceAnalytics.trackShowInView(props.organization);
  1063. props.onTabScrollToNode(props.node);
  1064. }}
  1065. >
  1066. {t('Show in view')}
  1067. </Button>
  1068. {isTransactionNode(props.node) ? (
  1069. <LinkButton
  1070. size="xs"
  1071. icon={<IconOpen />}
  1072. onClick={() => traceAnalytics.trackViewEventJSON(props.organization)}
  1073. href={`/api/0/projects/${props.organization.slug}/${props.node.value.project_slug}/events/${props.node.value.event_id}/json/`}
  1074. external
  1075. >
  1076. {t('JSON')} (<FileSize bytes={props.eventSize ?? 0} />)
  1077. </LinkButton>
  1078. ) : null}
  1079. </Actions>
  1080. <DropdownMenuWithPortal
  1081. items={items}
  1082. className="DropdownMenu"
  1083. position="bottom-end"
  1084. trigger={triggerProps => (
  1085. <ActionsButtonTrigger size="xs" {...triggerProps}>
  1086. {t('Actions')}
  1087. <IconChevron direction="down" size="xs" />
  1088. </ActionsButtonTrigger>
  1089. )}
  1090. />
  1091. </ActionsContainer>
  1092. );
  1093. }
  1094. const ActionsButtonTrigger = styled(Button)`
  1095. svg {
  1096. margin-left: ${space(0.5)};
  1097. width: 10px;
  1098. height: 10px;
  1099. }
  1100. `;
  1101. const ActionsContainer = styled('div')`
  1102. display: flex;
  1103. justify-content: end;
  1104. align-items: center;
  1105. gap: ${space(1)};
  1106. `;
  1107. function EventTags({projectSlug, event}: {event: Event; projectSlug: string}) {
  1108. const hasNewTraceUi = useHasTraceNewUi();
  1109. if (!hasNewTraceUi) {
  1110. return <LegacyEventTags event={event} projectSlug={projectSlug} />;
  1111. }
  1112. return <EventTagsDataSection event={event} projectSlug={projectSlug} />;
  1113. }
  1114. function LegacyEventTags({projectSlug, event}: {event: Event; projectSlug: string}) {
  1115. return (
  1116. <LazyRender {...TraceDrawerComponents.LAZY_RENDER_PROPS} containerHeight={200}>
  1117. <TagsWrapper>
  1118. <EventTagsDataSection event={event} projectSlug={projectSlug} />
  1119. </TagsWrapper>
  1120. </LazyRender>
  1121. );
  1122. }
  1123. const TagsWrapper = styled('div')`
  1124. h3 {
  1125. color: ${p => p.theme.textColor};
  1126. }
  1127. `;
  1128. export type SectionCardKeyValueList = KeyValueListData;
  1129. function SectionCard({
  1130. items,
  1131. title,
  1132. disableTruncate,
  1133. sortAlphabetically = false,
  1134. itemProps = {},
  1135. }: {
  1136. items: SectionCardKeyValueList;
  1137. title: React.ReactNode;
  1138. disableTruncate?: boolean;
  1139. itemProps?: Partial<KeyValueDataContentProps>;
  1140. sortAlphabetically?: boolean;
  1141. }) {
  1142. const contentItems = items.map(item => ({item, ...itemProps}));
  1143. return (
  1144. <CardWrapper>
  1145. <KeyValueData.Card
  1146. title={title}
  1147. contentItems={contentItems}
  1148. sortAlphabetically={sortAlphabetically}
  1149. truncateLength={disableTruncate ? Infinity : 5}
  1150. />
  1151. </CardWrapper>
  1152. );
  1153. }
  1154. // This is trace-view specific styling. The card is rendered in a number of different places
  1155. // with tests failing otherwise, since @container queries are not supported by the version of
  1156. // jsdom currently used by jest.
  1157. const CardWrapper = styled('div')`
  1158. ${CardPanel} {
  1159. container-type: inline-size;
  1160. }
  1161. ${Subject} {
  1162. display: flex;
  1163. align-items: center;
  1164. @container (width < 350px) {
  1165. max-width: 200px;
  1166. }
  1167. }
  1168. ${ValueSection} {
  1169. align-items: center;
  1170. }
  1171. `;
  1172. function SectionCardGroup({children}: {children: React.ReactNode}) {
  1173. return <KeyValueData.Container>{children}</KeyValueData.Container>;
  1174. }
  1175. function CopyableCardValueWithLink({
  1176. value,
  1177. linkTarget,
  1178. linkText,
  1179. onClick,
  1180. }: {
  1181. value: React.ReactNode;
  1182. linkTarget?: LocationDescriptor;
  1183. linkText?: string;
  1184. onClick?: () => void;
  1185. }) {
  1186. return (
  1187. <CardValueContainer>
  1188. <CardValueText>
  1189. {value}
  1190. {typeof value === 'string' ? (
  1191. <StyledCopyToClipboardButton
  1192. borderless
  1193. size="zero"
  1194. iconSize="xs"
  1195. text={value}
  1196. />
  1197. ) : null}
  1198. </CardValueText>
  1199. {linkTarget && linkTarget ? (
  1200. <Link to={linkTarget} onClick={onClick}>
  1201. {linkText}
  1202. </Link>
  1203. ) : null}
  1204. </CardValueContainer>
  1205. );
  1206. }
  1207. function TraceDataSection({event}: {event: EventTransaction}) {
  1208. const traceData = event.contexts.trace?.data;
  1209. if (!traceData) {
  1210. return null;
  1211. }
  1212. return (
  1213. <SectionCard
  1214. items={Object.entries(traceData).map(([key, value]) => ({
  1215. key,
  1216. subject: key,
  1217. value,
  1218. }))}
  1219. title={t('Trace Data')}
  1220. />
  1221. );
  1222. }
  1223. const StyledCopyToClipboardButton = styled(CopyToClipboardButton)`
  1224. transform: translateY(2px);
  1225. `;
  1226. const CardValueContainer = styled(FlexBox)`
  1227. justify-content: space-between;
  1228. gap: ${space(1)};
  1229. flex-wrap: wrap;
  1230. `;
  1231. const CardValueText = styled('span')`
  1232. overflow-wrap: anywhere;
  1233. `;
  1234. export const CardContentSubject = styled('div')`
  1235. grid-column: span 1;
  1236. font-family: ${p => p.theme.text.familyMono};
  1237. word-wrap: break-word;
  1238. `;
  1239. const TraceDrawerComponents = {
  1240. DetailContainer,
  1241. BodyContainer,
  1242. FlexBox,
  1243. Title: TitleWithTestId,
  1244. Type,
  1245. TitleOp,
  1246. HeaderContainer,
  1247. LegacyHeaderContainer,
  1248. Highlights,
  1249. Actions,
  1250. NodeActions,
  1251. KeyValueAction,
  1252. Table,
  1253. IconTitleWrapper,
  1254. IconBorder,
  1255. TitleText,
  1256. LegacyTitleText,
  1257. Duration,
  1258. TableRow,
  1259. LAZY_RENDER_PROPS,
  1260. TableRowButtonContainer,
  1261. TableValueRow,
  1262. IssuesLink,
  1263. SectionCard,
  1264. CopyableCardValueWithLink,
  1265. EventTags,
  1266. SubtitleWithCopyButton,
  1267. TraceDataSection,
  1268. SectionCardGroup,
  1269. DropdownMenuWithPortal,
  1270. };
  1271. export {TraceDrawerComponents};