traceVitals.tsx 4.5 KB

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