traceVitals.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import styled from '@emotion/styled';
  2. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  3. import Panel from 'sentry/components/panels/panel';
  4. import {Tooltip} from 'sentry/components/tooltip';
  5. import {IconFire} from 'sentry/icons';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {Measurement} from 'sentry/types/event';
  9. import getDuration from 'sentry/utils/duration/getDuration';
  10. import type {Vital} from 'sentry/utils/performance/vitals/types';
  11. import type {IconSize} from 'sentry/utils/theme';
  12. import useProjects from 'sentry/utils/useProjects';
  13. import {TraceDrawerComponents} from '../../traceDrawer/details/styles';
  14. import {isTransactionNode} from '../../traceGuards';
  15. import type {TraceTree} from '../../traceModels/traceTree';
  16. import {TRACE_MEASUREMENT_LOOKUP} from '../../traceModels/traceTree.measurements';
  17. interface TraceVitalsProps {
  18. trace: TraceTree;
  19. }
  20. export function TraceVitals(props: TraceVitalsProps) {
  21. const {projects} = useProjects();
  22. const measurements = Array.from(props.trace.vitals.entries());
  23. return (
  24. <TraceDrawerComponents.DetailContainer>
  25. {measurements.map(([node, vital]) => {
  26. const op = isTransactionNode(node) ? node.value['transaction.op'] : '';
  27. const transaction = isTransactionNode(node) ? node.value.transaction : '';
  28. const project = projects.find(p => p.slug === node.metadata.project_slug);
  29. return (
  30. <div key="">
  31. <TraceDrawerComponents.LegacyHeaderContainer>
  32. <TraceDrawerComponents.Title>
  33. <Tooltip title={node.metadata.project_slug}>
  34. <ProjectBadge
  35. project={project ? project : {slug: node.metadata.project_slug ?? ''}}
  36. avatarSize={30}
  37. hideName
  38. />
  39. </Tooltip>
  40. <div>
  41. <div>{t('transaction')}</div>
  42. <TraceDrawerComponents.TitleOp
  43. text={
  44. transaction && op
  45. ? `${op} - ${transaction}`
  46. : transaction
  47. ? transaction
  48. : op
  49. }
  50. />
  51. </div>
  52. </TraceDrawerComponents.Title>
  53. </TraceDrawerComponents.LegacyHeaderContainer>
  54. <VitalsContainer>
  55. {vital.map((v, i) => {
  56. return <EventVital key={i} vital={v} value={v.measurement} />;
  57. })}
  58. </VitalsContainer>
  59. </div>
  60. );
  61. })}
  62. </TraceDrawerComponents.DetailContainer>
  63. );
  64. }
  65. const VitalsContainer = styled('div')`
  66. display: grid;
  67. grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  68. gap: ${space(1)};
  69. margin-top: ${space(2)};
  70. `;
  71. interface EventVitalProps {
  72. value: Measurement;
  73. vital: TraceTree.CollectedVital;
  74. }
  75. function formatVitalDuration(vital: Vital, value: number) {
  76. if (vital?.type === 'duration') {
  77. return getDuration(value / 1000, 2, true);
  78. }
  79. if (vital?.type === 'integer') {
  80. return value.toFixed(0);
  81. }
  82. return value.toFixed(2);
  83. }
  84. function EventVital(props: EventVitalProps) {
  85. const vital = TRACE_MEASUREMENT_LOOKUP[props.vital.key];
  86. if (!vital) {
  87. return null;
  88. }
  89. const failedThreshold =
  90. vital.poorThreshold !== undefined && props.value.value >= vital.poorThreshold;
  91. const currentValue = formatVitalDuration(vital, props.value.value);
  92. const thresholdValue = formatVitalDuration(vital, vital?.poorThreshold ?? 0);
  93. return (
  94. <StyledPanel failedThreshold={failedThreshold}>
  95. <div>{vital.name ?? name}</div>
  96. <ValueRow>
  97. {failedThreshold ? (
  98. <FireIconContainer data-test-id="threshold-failed-warning" size="sm">
  99. <Tooltip
  100. title={t('Fails threshold at %s.', thresholdValue)}
  101. position="top"
  102. containerDisplayMode="inline-block"
  103. >
  104. <IconFire size="sm" />
  105. </Tooltip>
  106. </FireIconContainer>
  107. ) : null}
  108. <Value failedThreshold={failedThreshold}>{currentValue}</Value>
  109. </ValueRow>
  110. </StyledPanel>
  111. );
  112. }
  113. const StyledPanel = styled(Panel)<{failedThreshold: boolean}>`
  114. padding: ${space(1)} ${space(1.5)};
  115. margin-bottom: ${space(1)};
  116. ${p => p.failedThreshold && `border: 1px solid ${p.theme.red300};`}
  117. `;
  118. const ValueRow = styled('div')`
  119. display: flex;
  120. align-items: center;
  121. `;
  122. const FireIconContainer = styled('span')<{size: IconSize | string}>`
  123. display: inline-block;
  124. height: ${p => p.theme.iconSizes[p.size] ?? p.size};
  125. line-height: ${p => p.theme.iconSizes[p.size] ?? p.size};
  126. margin-right: ${space(0.5)};
  127. color: ${p => p.theme.errorText};
  128. `;
  129. const Value = styled('span')<{failedThreshold: boolean}>`
  130. font-size: ${p => p.theme.fontSizeExtraLarge};
  131. ${p => p.failedThreshold && `color: ${p.theme.errorText};`}
  132. `;