fieldRenderers.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import {useState} from 'react';
  2. import {css, type Theme, useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import Tag from 'sentry/components/badge/tag';
  6. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  7. import Link from 'sentry/components/links/link';
  8. import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
  9. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  10. import PerformanceDuration from 'sentry/components/performanceDuration';
  11. import TimeSince from 'sentry/components/timeSince';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {t, tn} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  16. import {getShortEventId} from 'sentry/utils/events';
  17. import Projects from 'sentry/utils/projects';
  18. import {useLocation} from 'sentry/utils/useLocation';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import usePageFilters from 'sentry/utils/usePageFilters';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import type {SpanIndexedField, SpanIndexedResponse} from 'sentry/views/insights/types';
  23. import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
  24. import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
  25. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  26. import type {TraceResult} from '../../hooks/useTraces';
  27. import {BREAKDOWN_SLICES} from '../../hooks/useTraces';
  28. import type {SpanResult} from '../../hooks/useTraceSpans';
  29. import type {Field} from './data';
  30. import {getShortenedSdkName, getStylingSliceName} from './utils';
  31. export const ProjectBadgeWrapper = styled('span')`
  32. /**
  33. * Max of 2 visible projects, 16px each, 2px border, 8px overlap.
  34. */
  35. width: 32px;
  36. min-width: 32px;
  37. `;
  38. export function SpanDescriptionRenderer({span}: {span: SpanResult<Field>}) {
  39. return (
  40. <Description data-test-id="span-description">
  41. <ProjectBadgeWrapper>
  42. <ProjectRenderer projectSlug={span.project} hideName />
  43. </ProjectBadgeWrapper>
  44. <strong>{span['span.op']}</strong>
  45. <em>{'\u2014'}</em>
  46. <WrappingText>{span['span.description']}</WrappingText>
  47. {<StatusTag status={span['span.status']} />}
  48. </Description>
  49. );
  50. }
  51. interface ProjectsRendererProps {
  52. projectSlugs: string[];
  53. maxVisibleProjects?: number;
  54. visibleAvatarSize?: number;
  55. }
  56. export function ProjectsRenderer({
  57. projectSlugs,
  58. visibleAvatarSize,
  59. maxVisibleProjects = 2,
  60. }: ProjectsRendererProps) {
  61. const organization = useOrganization();
  62. const {projects} = useProjects({slugs: projectSlugs, orgId: organization.slug});
  63. const projectAvatars =
  64. projects.length > 0 ? projects : projectSlugs.map(slug => ({slug}));
  65. const numProjects = projectAvatars.length;
  66. const numVisibleProjects =
  67. maxVisibleProjects - numProjects >= 0 ? numProjects : maxVisibleProjects - 1;
  68. const visibleProjectAvatars = projectAvatars.slice(0, numVisibleProjects).reverse();
  69. const collapsedProjectAvatars = projectAvatars.slice(numVisibleProjects);
  70. const numCollapsedProjects = collapsedProjectAvatars.length;
  71. return (
  72. <ProjectList>
  73. {numCollapsedProjects > 0 && (
  74. <Tooltip
  75. skipWrapper
  76. title={
  77. <CollapsedProjects>
  78. {tn(
  79. 'This trace contains %s more project.',
  80. 'This trace contains %s more projects.',
  81. numCollapsedProjects
  82. )}
  83. {collapsedProjectAvatars.map(project => (
  84. <ProjectBadge key={project.slug} project={project} avatarSize={16} />
  85. ))}
  86. </CollapsedProjects>
  87. }
  88. >
  89. <CollapsedBadge size={20} fontSize={10} data-test-id="collapsed-projects-badge">
  90. +{numCollapsedProjects}
  91. </CollapsedBadge>
  92. </Tooltip>
  93. )}
  94. {visibleProjectAvatars.map(project => (
  95. <StyledProjectBadge
  96. key={project.slug}
  97. hideName
  98. project={project}
  99. avatarSize={visibleAvatarSize ?? 16}
  100. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  101. />
  102. ))}
  103. </ProjectList>
  104. );
  105. }
  106. const ProjectList = styled('div')`
  107. display: flex;
  108. align-items: center;
  109. flex-direction: row-reverse;
  110. justify-content: flex-end;
  111. padding-right: 8px;
  112. `;
  113. const CollapsedProjects = styled('div')`
  114. width: 200px;
  115. display: flex;
  116. flex-direction: column;
  117. gap: ${space(0.5)};
  118. `;
  119. const AvatarStyle = p => css`
  120. border: 2px solid ${p.theme.background};
  121. margin-right: -8px;
  122. cursor: default;
  123. &:hover {
  124. z-index: 1;
  125. }
  126. `;
  127. const StyledProjectBadge = styled(ProjectBadge)`
  128. overflow: hidden;
  129. z-index: 0;
  130. ${AvatarStyle}
  131. `;
  132. const CollapsedBadge = styled('div')<{fontSize: number; size: number}>`
  133. display: flex;
  134. align-items: center;
  135. justify-content: center;
  136. position: relative;
  137. text-align: center;
  138. font-weight: ${p => p.theme.fontWeightBold};
  139. background-color: ${p => p.theme.gray200};
  140. color: ${p => p.theme.gray300};
  141. font-size: ${p => p.fontSize}px;
  142. width: ${p => p.size}px;
  143. height: ${p => p.size}px;
  144. border-radius: ${p => p.theme.borderRadius};
  145. ${AvatarStyle}
  146. `;
  147. interface ProjectRendererProps {
  148. projectSlug: string;
  149. hideName?: boolean;
  150. }
  151. export function ProjectRenderer({projectSlug, hideName}: ProjectRendererProps) {
  152. const organization = useOrganization();
  153. return (
  154. <Projects orgId={organization.slug} slugs={[projectSlug]}>
  155. {({projects}) => {
  156. const project = projects.find(p => p.slug === projectSlug);
  157. return (
  158. <ProjectBadge
  159. hideName={hideName}
  160. project={project ? project : {slug: projectSlug}}
  161. avatarSize={16}
  162. avatarProps={{hasTooltip: true, tooltip: projectSlug}}
  163. />
  164. );
  165. }}
  166. </Projects>
  167. );
  168. }
  169. const WrappingText = styled('div')`
  170. ${p => p.theme.overflowEllipsis};
  171. width: auto;
  172. `;
  173. export const TraceBreakdownContainer = styled('div')<{hoveredIndex?: number}>`
  174. position: relative;
  175. display: flex;
  176. min-width: 200px;
  177. height: 15px;
  178. background-color: ${p => p.theme.gray100};
  179. ${p => `--hoveredSlice-${p.hoveredIndex ?? -1}-translateY: translateY(-3px)`};
  180. `;
  181. const RectangleTraceBreakdown = styled(RowRectangle)<{
  182. sliceColor: string;
  183. sliceName: string | null;
  184. offset?: number;
  185. }>`
  186. background-color: ${p => p.sliceColor};
  187. position: relative;
  188. width: 100%;
  189. height: 15px;
  190. ${p => `
  191. filter: var(--highlightedSlice-${p.sliceName}-saturate, var(--defaultSlice-saturate));
  192. `}
  193. ${p => `
  194. opacity: var(--highlightedSlice-${p.sliceName ?? ''}-opacity, var(--defaultSlice-opacity, 1.0));
  195. `}
  196. ${p => `
  197. transform: var(--hoveredSlice-${p.offset}-translateY, var(--highlightedSlice-${p.sliceName ?? ''}-transform, var(--defaultSlice-transform, 1.0)));
  198. `}
  199. transition: filter,opacity,transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  200. `;
  201. export function TraceBreakdownRenderer({
  202. trace,
  203. setHighlightedSliceName,
  204. }: {
  205. setHighlightedSliceName: (sliceName: string) => void;
  206. trace: TraceResult;
  207. }) {
  208. const theme = useTheme();
  209. const [hoveredIndex, setHoveredIndex] = useState(-1);
  210. return (
  211. <TraceBreakdownContainer
  212. data-test-id="relative-ops-breakdown"
  213. hoveredIndex={hoveredIndex}
  214. onMouseLeave={() => setHoveredIndex(-1)}
  215. >
  216. {trace.breakdowns.map((breakdown, index) => {
  217. return (
  218. <SpanBreakdownSliceRenderer
  219. key={breakdown.start + (breakdown.project ?? t('missing instrumentation'))}
  220. sliceName={breakdown.project}
  221. sliceStart={breakdown.start}
  222. sliceEnd={breakdown.end}
  223. sliceDurationReal={breakdown.duration}
  224. sliceSecondaryName={breakdown.sdkName}
  225. sliceNumberStart={breakdown.sliceStart}
  226. sliceNumberWidth={breakdown.sliceWidth}
  227. trace={trace}
  228. theme={theme}
  229. offset={index}
  230. onMouseEnter={() => {
  231. setHoveredIndex(index);
  232. breakdown.project
  233. ? setHighlightedSliceName(
  234. getStylingSliceName(breakdown.project, breakdown.sdkName) ?? ''
  235. )
  236. : null;
  237. }}
  238. />
  239. );
  240. })}
  241. </TraceBreakdownContainer>
  242. );
  243. }
  244. const BREAKDOWN_SIZE_PX = 200;
  245. /**
  246. * This renders slices in two different ways;
  247. * - Slices in the breakdown for the trace. These have slice numbers returned for quantization from the backend.
  248. * - Slices derived from span timings. Spans aren't quantized into slices.
  249. */
  250. export function SpanBreakdownSliceRenderer({
  251. trace,
  252. theme,
  253. sliceName,
  254. sliceStart,
  255. sliceEnd,
  256. sliceNumberStart,
  257. sliceNumberWidth,
  258. sliceDurationReal,
  259. sliceSecondaryName,
  260. onMouseEnter,
  261. offset,
  262. }: {
  263. onMouseEnter: () => void;
  264. sliceEnd: number;
  265. sliceName: string | null;
  266. sliceSecondaryName: string | null;
  267. sliceStart: number;
  268. theme: Theme;
  269. trace: TraceResult;
  270. offset?: number;
  271. sliceDurationReal?: number;
  272. sliceNumberStart?: number;
  273. sliceNumberWidth?: number;
  274. }) {
  275. const traceDuration = trace.end - trace.start;
  276. const sliceDuration = sliceEnd - sliceStart;
  277. const pixelsPerSlice = BREAKDOWN_SIZE_PX / BREAKDOWN_SLICES;
  278. const relativeSliceStart = sliceStart - trace.start;
  279. const stylingSliceName = getStylingSliceName(sliceName, sliceSecondaryName);
  280. const sliceColor = stylingSliceName ? pickBarColor(stylingSliceName) : theme.gray100;
  281. const sliceWidth =
  282. sliceNumberWidth !== undefined
  283. ? pixelsPerSlice * sliceNumberWidth
  284. : pixelsPerSlice * Math.ceil(BREAKDOWN_SLICES * (sliceDuration / traceDuration));
  285. const sliceOffset =
  286. sliceNumberStart !== undefined
  287. ? pixelsPerSlice * sliceNumberStart
  288. : pixelsPerSlice *
  289. Math.floor((BREAKDOWN_SLICES * relativeSliceStart) / traceDuration);
  290. return (
  291. <BreakdownSlice
  292. sliceName={sliceName}
  293. sliceOffset={sliceOffset}
  294. sliceWidth={sliceWidth}
  295. onMouseEnter={onMouseEnter}
  296. >
  297. <Tooltip
  298. title={
  299. <div>
  300. <FlexContainer>
  301. {sliceName ? <ProjectRenderer projectSlug={sliceName} hideName /> : null}
  302. <strong>{sliceName}</strong>
  303. <Subtext>({getShortenedSdkName(sliceSecondaryName)})</Subtext>
  304. </FlexContainer>
  305. <div>
  306. <PerformanceDuration
  307. milliseconds={sliceDurationReal ?? sliceDuration}
  308. abbreviation
  309. />
  310. </div>
  311. </div>
  312. }
  313. containerDisplayMode="block"
  314. >
  315. <RectangleTraceBreakdown
  316. sliceColor={sliceColor}
  317. sliceName={stylingSliceName}
  318. offset={offset}
  319. />
  320. </Tooltip>
  321. </BreakdownSlice>
  322. );
  323. }
  324. const Subtext = styled('span')`
  325. font-weight: ${p => p.theme.fontWeightNormal};
  326. color: ${p => p.theme.gray300};
  327. `;
  328. const FlexContainer = styled('div')`
  329. display: flex;
  330. flex-direction: row;
  331. align-items: center;
  332. gap: ${space(0.5)};
  333. padding-bottom: ${space(0.5)};
  334. `;
  335. const BreakdownSlice = styled('div')<{
  336. sliceName: string | null;
  337. sliceOffset: number;
  338. sliceWidth: number;
  339. }>`
  340. position: absolute;
  341. width: max(3px, ${p => p.sliceWidth}px);
  342. left: ${p => p.sliceOffset}px;
  343. ${p => (p.sliceName ? null : 'z-index: -1;')}
  344. `;
  345. interface SpanIdRendererProps {
  346. projectSlug: string;
  347. spanId: string;
  348. timestamp: string;
  349. traceId: string;
  350. transactionId: string;
  351. onClick?: () => void;
  352. }
  353. export function SpanIdRenderer({
  354. projectSlug,
  355. spanId,
  356. timestamp,
  357. traceId,
  358. transactionId,
  359. onClick,
  360. }: SpanIdRendererProps) {
  361. const location = useLocation();
  362. const organization = useOrganization();
  363. const target = generateLinkToEventInTraceView({
  364. projectSlug,
  365. traceSlug: traceId,
  366. timestamp,
  367. eventId: transactionId,
  368. organization,
  369. location,
  370. spanId,
  371. source: TraceViewSources.TRACES,
  372. });
  373. return (
  374. <Link to={target} onClick={onClick}>
  375. {getShortEventId(spanId)}
  376. </Link>
  377. );
  378. }
  379. interface TraceIdRendererProps {
  380. location: Location;
  381. timestamp: number; // in milliseconds
  382. traceId: string;
  383. onClick?: () => void;
  384. transactionId?: string;
  385. }
  386. export function TraceIdRenderer({
  387. traceId,
  388. timestamp,
  389. transactionId,
  390. location,
  391. onClick,
  392. }: TraceIdRendererProps) {
  393. const organization = useOrganization();
  394. const {selection} = usePageFilters();
  395. const target = getTraceDetailsUrl({
  396. organization,
  397. traceSlug: traceId,
  398. dateSelection: {
  399. start: selection.datetime.start,
  400. end: selection.datetime.end,
  401. statsPeriod: selection.datetime.period,
  402. },
  403. timestamp: timestamp / 1000,
  404. eventId: transactionId,
  405. location,
  406. source: TraceViewSources.TRACES,
  407. });
  408. return (
  409. <Link to={target} style={{minWidth: '66px', textAlign: 'right'}} onClick={onClick}>
  410. {getShortEventId(traceId)}
  411. </Link>
  412. );
  413. }
  414. interface TransactionRendererProps {
  415. projectSlug: string;
  416. transaction: string;
  417. }
  418. export function TransactionRenderer({
  419. projectSlug,
  420. transaction,
  421. }: TransactionRendererProps) {
  422. const location = useLocation();
  423. const organization = useOrganization();
  424. const {projects} = useProjects({slugs: [projectSlug]});
  425. const target = transactionSummaryRouteWithQuery({
  426. orgSlug: organization.slug,
  427. transaction,
  428. query: {
  429. ...location.query,
  430. query: undefined,
  431. },
  432. projectID: String(projects[0]?.id ?? ''),
  433. });
  434. return <Link to={target}>{transaction}</Link>;
  435. }
  436. export function SpanTimeRenderer({
  437. timestamp,
  438. tooltipShowSeconds,
  439. }: {
  440. timestamp: number;
  441. tooltipShowSeconds?: boolean;
  442. }) {
  443. const date = new Date(timestamp);
  444. return (
  445. <TimeSince
  446. unitStyle="extraShort"
  447. date={date}
  448. tooltipShowSeconds={tooltipShowSeconds}
  449. />
  450. );
  451. }
  452. type SpanStatus = SpanIndexedResponse[SpanIndexedField.SPAN_STATUS];
  453. const STATUS_TO_TAG_TYPE: Record<SpanStatus, keyof Theme['tag']> = {
  454. ok: 'success',
  455. cancelled: 'warning',
  456. unknown: 'info',
  457. invalid_argument: 'warning',
  458. deadline_exceeded: 'error',
  459. not_found: 'warning',
  460. already_exists: 'warning',
  461. permission_denied: 'warning',
  462. resource_exhausted: 'warning',
  463. failed_precondition: 'warning',
  464. aborted: 'warning',
  465. out_of_range: 'warning',
  466. unimplemented: 'error',
  467. internal_error: 'error',
  468. unavailable: 'error',
  469. data_loss: 'error',
  470. unauthenticated: 'warning',
  471. };
  472. function statusToTagType(status: string) {
  473. return STATUS_TO_TAG_TYPE[status];
  474. }
  475. const OMITTED_SPAN_STATUS = ['unknown'];
  476. /**
  477. * This display a tag for the status (not to be confused with 'status_code' which has values like '200', '429').
  478. */
  479. export function StatusTag({status, onClick}: {status: string; onClick?: () => void}) {
  480. const tagType = statusToTagType(status);
  481. if (!tagType) {
  482. return null;
  483. }
  484. if (OMITTED_SPAN_STATUS.includes(status)) {
  485. return null;
  486. }
  487. return (
  488. <StyledTag type={tagType} onClick={onClick} borderStyle="solid">
  489. {status}
  490. </StyledTag>
  491. );
  492. }
  493. const StyledTag = styled(Tag)`
  494. cursor: ${p => (p.onClick ? 'pointer' : 'default')};
  495. `;
  496. export const Description = styled('div')`
  497. ${p => p.theme.overflowEllipsis};
  498. display: flex;
  499. flex-direction: row;
  500. align-items: center;
  501. gap: ${space(1)};
  502. `;