webVitalDescription.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import {useTheme} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import {COUNTRY_CODE_TO_NAME_MAP} from 'sentry/data/countryCodesMap';
  6. import {IconCheckmark} from 'sentry/icons/iconCheckmark';
  7. import {IconClose} from 'sentry/icons/iconClose';
  8. import {t, tct} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {Tag} from 'sentry/types/group';
  11. import {WebVital} from 'sentry/utils/fields';
  12. import {Browser} from 'sentry/utils/performance/vitals/constants';
  13. import {ORDER} from 'sentry/views/insights/browser/webVitals/components/charts/performanceScoreChart';
  14. import {Dot} from 'sentry/views/insights/browser/webVitals/components/webVitalMeters';
  15. import type {
  16. ProjectScore,
  17. WebVitals,
  18. } from 'sentry/views/insights/browser/webVitals/types';
  19. import {PERFORMANCE_SCORE_COLORS} from 'sentry/views/insights/browser/webVitals/utils/performanceScoreColors';
  20. import {
  21. scoreToStatus,
  22. STATUS_TEXT,
  23. } from 'sentry/views/insights/browser/webVitals/utils/scoreToStatus';
  24. import {vitalSupportedBrowsers} from 'sentry/views/performance/vitalDetail/utils';
  25. import PerformanceScoreRingWithTooltips from './performanceScoreRingWithTooltips';
  26. type Props = {
  27. webVital: WebVitals;
  28. score?: number;
  29. value?: string;
  30. };
  31. const WEB_VITAL_FULL_NAME_MAP = {
  32. cls: t('Cumulative Layout Shift'),
  33. fcp: t('First Contentful Paint'),
  34. inp: t('Interaction to Next Paint'),
  35. lcp: t('Largest Contentful Paint'),
  36. ttfb: t('Time to First Byte'),
  37. };
  38. const VITAL_DESCRIPTIONS: Partial<Record<WebVital, string>> = {
  39. [WebVital.FCP]: t(
  40. 'First Contentful Paint (FCP) measures the amount of time the first content takes to render in the viewport. Like FP, this could also show up in any form from the document object model (DOM), such as images, SVGs, or text blocks.'
  41. ),
  42. [WebVital.CLS]: t(
  43. 'Cumulative Layout Shift (CLS) is the sum of individual layout shift scores for every unexpected element shift during the rendering process. Imagine navigating to an article and trying to click a link before the page finishes loading. Before your cursor even gets there, the link may have shifted down due to an image rendering. Rather than using duration for this Web Vital, the CLS score represents the degree of disruptive and visually unstable shifts.'
  44. ),
  45. [WebVital.LCP]: t(
  46. 'Largest Contentful Paint (LCP) measures the render time for the largest content to appear in the viewport. This may be in any form from the document object model (DOM), such as images, SVGs, or text blocks. It’s the largest pixel area in the viewport, thus most visually defining. LCP helps developers understand how long it takes to see the main content on the page.'
  47. ),
  48. [WebVital.TTFB]: t(
  49. 'Time to First Byte (TTFB) is a foundational metric for measuring connection setup time and web server responsiveness in both the lab and the field. It helps identify when a web server is too slow to respond to requests. In the case of navigation requests—that is, requests for an HTML document—it precedes every other meaningful loading performance metric.'
  50. ),
  51. [WebVital.INP]: t(
  52. "Interaction to Next Paint (INP) is a metric that assesses a page's overall responsiveness to user interactions by observing the latency of all click, tap, and keyboard interactions that occur throughout the lifespan of a user's visit to a page. The final INP value is the longest interaction observed, ignoring outliers."
  53. ),
  54. };
  55. type WebVitalDetailHeaderProps = {
  56. isProjectScoreCalculated: boolean;
  57. projectScore: ProjectScore;
  58. tag: Tag;
  59. value: React.ReactNode;
  60. };
  61. export function WebVitalDetailHeader({score, value, webVital}: Props) {
  62. const theme = useTheme();
  63. const colors = theme.charts.getColorPalette(3);
  64. const dotColor = colors[ORDER.indexOf(webVital)];
  65. const status = score !== undefined ? scoreToStatus(score) : undefined;
  66. return (
  67. <Header>
  68. <span>
  69. <WebVitalName>{`${WEB_VITAL_FULL_NAME_MAP[webVital]} (P75)`}</WebVitalName>
  70. <Value>
  71. <Dot color={dotColor} />
  72. {value ?? ' \u2014 '}
  73. </Value>
  74. </span>
  75. {status && score && (
  76. <ScoreBadge status={status}>
  77. <StatusText>{STATUS_TEXT[status]}</StatusText>
  78. <StatusScore>{score}</StatusScore>
  79. </ScoreBadge>
  80. )}
  81. </Header>
  82. );
  83. }
  84. export function WebVitalTagsDetailHeader({
  85. projectScore,
  86. value,
  87. tag,
  88. isProjectScoreCalculated,
  89. }: WebVitalDetailHeaderProps) {
  90. const theme = useTheme();
  91. const ringSegmentColors = theme.charts.getColorPalette(3);
  92. const ringBackgroundColors = ringSegmentColors.map(color => `${color}50`);
  93. const title =
  94. tag.key === 'geo.country_code' ? COUNTRY_CODE_TO_NAME_MAP[tag.name] : tag.name;
  95. return (
  96. <Header>
  97. <span>
  98. <TitleWrapper>
  99. <WebVitalName>{title}</WebVitalName>
  100. <StyledCopyToClipboardButton
  101. borderless
  102. text={`${tag.key}:${tag.name}`}
  103. size="sm"
  104. iconSize="sm"
  105. />
  106. </TitleWrapper>
  107. <Value>{value}</Value>
  108. </span>
  109. {isProjectScoreCalculated && projectScore ? (
  110. <PerformanceScoreRingWithTooltips
  111. hideWebVitalLabels
  112. projectScore={projectScore}
  113. text={projectScore.totalScore}
  114. width={100}
  115. height={100}
  116. ringBackgroundColors={ringBackgroundColors}
  117. ringSegmentColors={ringSegmentColors}
  118. size={100}
  119. x={0}
  120. y={0}
  121. />
  122. ) : (
  123. <StyledLoadingIndicator size={50} />
  124. )}
  125. </Header>
  126. );
  127. }
  128. export function WebVitalDescription({score, value, webVital}: Props) {
  129. const description: string = VITAL_DESCRIPTIONS[WebVital[webVital.toUpperCase()]];
  130. return (
  131. <div>
  132. <WebVitalDetailHeader score={score} value={value} webVital={webVital} />
  133. <p>{description}</p>
  134. <p>
  135. <b>
  136. {tct(
  137. `At the moment, there is support for [webVital] in the following browsers:`,
  138. {webVital: webVital.toUpperCase()}
  139. )}
  140. </b>
  141. </p>
  142. <SupportedBrowsers>
  143. {Object.values(Browser).map(browser => (
  144. <BrowserItem key={browser}>
  145. {vitalSupportedBrowsers[WebVital[webVital.toUpperCase()]]?.includes(
  146. browser
  147. ) ? (
  148. <IconCheckmark color="successText" size="sm" />
  149. ) : (
  150. <IconClose color="dangerText" size="sm" />
  151. )}
  152. {browser}
  153. </BrowserItem>
  154. ))}
  155. </SupportedBrowsers>
  156. </div>
  157. );
  158. }
  159. const SupportedBrowsers = styled('div')`
  160. display: inline-flex;
  161. gap: ${space(2)};
  162. margin-bottom: ${space(3)};
  163. `;
  164. const BrowserItem = styled('div')`
  165. display: flex;
  166. align-items: center;
  167. gap: ${space(1)};
  168. `;
  169. const Header = styled('span')`
  170. display: flex;
  171. justify-content: space-between;
  172. margin-bottom: ${space(3)};
  173. `;
  174. const Value = styled('h2')`
  175. display: flex;
  176. align-items: center;
  177. font-weight: ${p => p.theme.fontWeightNormal};
  178. margin-bottom: ${space(1)};
  179. `;
  180. const WebVitalName = styled('h4')`
  181. margin-bottom: ${space(1)};
  182. margin-top: 40px;
  183. max-width: 400px;
  184. ${p => p.theme.overflowEllipsis}
  185. `;
  186. const TitleWrapper = styled('div')`
  187. display: flex;
  188. align-items: baseline;
  189. `;
  190. const StyledCopyToClipboardButton = styled(CopyToClipboardButton)`
  191. padding-left: ${space(0.5)};
  192. `;
  193. const StyledLoadingIndicator = styled(LoadingIndicator)`
  194. margin: 20px 65px;
  195. `;
  196. const ScoreBadge = styled('div')<{status: string}>`
  197. display: flex;
  198. justify-content: center;
  199. align-items: center;
  200. flex-direction: column;
  201. color: ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].normal]};
  202. background-color: ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].light]};
  203. border: solid 1px ${p => p.theme[PERFORMANCE_SCORE_COLORS[p.status].light]};
  204. padding: ${space(0.5)};
  205. text-align: center;
  206. height: 60px;
  207. width: 60px;
  208. border-radius: 60px;
  209. `;
  210. const StatusText = styled('span')`
  211. padding-top: ${space(0.5)};
  212. font-size: ${p => p.theme.fontSizeSmall};
  213. `;
  214. const StatusScore = styled('span')`
  215. font-weight: ${p => p.theme.fontWeightBold};
  216. font-size: ${p => p.theme.fontSizeLarge};
  217. `;