eventVitals.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import styled from '@emotion/styled';
  2. import {SectionHeading} from 'app/components/charts/styles';
  3. import {Panel} from 'app/components/panels';
  4. import Tooltip from 'app/components/tooltip';
  5. import {IconFire, IconWarning} from 'app/icons';
  6. import {t} from 'app/locale';
  7. import space from 'app/styles/space';
  8. import {Event} from 'app/types/event';
  9. import {formattedValue} from 'app/utils/measurements/index';
  10. import {WEB_VITAL_DETAILS} from 'app/utils/performance/vitals/constants';
  11. import {IconSize} from 'app/utils/theme';
  12. type Props = {
  13. event: Event;
  14. showSectionHeader?: boolean;
  15. };
  16. function isOutdatedSdk(event: Event): boolean {
  17. if (!event.sdk?.version) {
  18. return false;
  19. }
  20. const sdkVersion = event.sdk.version;
  21. return (
  22. sdkVersion.startsWith('5.26.') ||
  23. sdkVersion.startsWith('5.27.0') ||
  24. sdkVersion.startsWith('5.27.1') ||
  25. sdkVersion.startsWith('5.27.2')
  26. );
  27. }
  28. export default function EventVitals({event, showSectionHeader = true}: Props) {
  29. const measurementNames = Object.keys(event.measurements ?? {})
  30. .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
  31. .sort();
  32. if (measurementNames.length === 0) {
  33. return null;
  34. }
  35. const component = (
  36. <Measurements>
  37. {measurementNames.map(name => (
  38. <EventVital key={name} event={event} name={name} />
  39. ))}
  40. </Measurements>
  41. );
  42. if (showSectionHeader) {
  43. return (
  44. <Container>
  45. <SectionHeading>
  46. {t('Web Vitals')}
  47. {isOutdatedSdk(event) && (
  48. <WarningIconContainer size="sm">
  49. <Tooltip
  50. title={t(
  51. 'These vitals were collected using an outdated SDK version and may not be accurate. To ensure accurate web vitals in new transaction events, please update your SDK to the latest version.'
  52. )}
  53. position="top"
  54. containerDisplayMode="inline-block"
  55. >
  56. <IconWarning size="sm" />
  57. </Tooltip>
  58. </WarningIconContainer>
  59. )}
  60. </SectionHeading>
  61. {component}
  62. </Container>
  63. );
  64. }
  65. return component;
  66. }
  67. type EventVitalProps = Props & {
  68. name: string;
  69. };
  70. function EventVital({event, name}: EventVitalProps) {
  71. const value = event.measurements?.[name].value ?? null;
  72. if (value === null) {
  73. return null;
  74. }
  75. // Measurements are referred to by their full name `measurements.<name>`
  76. // here but are stored using their abbreviated name `<name>`. Make sure
  77. // to convert it appropriately.
  78. const record = WEB_VITAL_DETAILS[`measurements.${name}`];
  79. if (!record) {
  80. return null;
  81. }
  82. const failedThreshold = value >= record.poorThreshold;
  83. const currentValue = formattedValue(record, value);
  84. const thresholdValue = formattedValue(record, record?.poorThreshold ?? 0);
  85. return (
  86. <EventVitalContainer>
  87. <StyledPanel failedThreshold={failedThreshold}>
  88. <Name>{record.name ?? name}</Name>
  89. <ValueRow>
  90. {failedThreshold ? (
  91. <FireIconContainer 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. </EventVitalContainer>
  105. );
  106. }
  107. const Measurements = styled('div')`
  108. display: grid;
  109. grid-column-gap: ${space(1)};
  110. `;
  111. const Container = styled('div')`
  112. font-size: ${p => p.theme.fontSizeMedium};
  113. margin-bottom: ${space(4)};
  114. `;
  115. const StyledPanel = styled(Panel)<{failedThreshold: boolean}>`
  116. padding: ${space(1)} ${space(1.5)};
  117. margin-bottom: ${space(1)};
  118. ${p => p.failedThreshold && `border: 1px solid ${p.theme.red300};`}
  119. `;
  120. const Name = styled('div')``;
  121. const ValueRow = styled('div')`
  122. display: flex;
  123. align-items: center;
  124. `;
  125. const WarningIconContainer = styled('span')<{size: IconSize | string}>`
  126. display: inline-block;
  127. height: ${p => p.theme.iconSizes[p.size] ?? p.size};
  128. line-height: ${p => p.theme.iconSizes[p.size] ?? p.size};
  129. margin-left: ${space(0.5)};
  130. color: ${p => p.theme.red300};
  131. `;
  132. const FireIconContainer = styled('span')<{size: IconSize | string}>`
  133. display: inline-block;
  134. height: ${p => p.theme.iconSizes[p.size] ?? p.size};
  135. line-height: ${p => p.theme.iconSizes[p.size] ?? p.size};
  136. margin-right: ${space(0.5)};
  137. color: ${p => p.theme.red300};
  138. `;
  139. const Value = styled('span')<{failedThreshold: boolean}>`
  140. font-size: ${p => p.theme.fontSizeExtraLarge};
  141. ${p => p.failedThreshold && `color: ${p.theme.red300};`}
  142. `;
  143. export const EventVitalContainer = styled('div')``;