pageOverviewSidebar.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import ChartZoom from 'sentry/components/charts/chartZoom';
  5. import {LineChart, LineChartSeries} from 'sentry/components/charts/lineChart';
  6. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import QuestionTooltip from 'sentry/components/questionTooltip';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {PageFilters} from 'sentry/types';
  12. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  13. import {getPeriod} from 'sentry/utils/getPeriod';
  14. import usePageFilters from 'sentry/utils/usePageFilters';
  15. import useRouter from 'sentry/utils/useRouter';
  16. import {MiniAggregateWaterfall} from 'sentry/views/performance/browser/webVitals/components/miniAggregateWaterfall';
  17. import PerformanceScoreRingWithTooltips from 'sentry/views/performance/browser/webVitals/components/performanceScoreRingWithTooltips';
  18. import {useProjectRawWebVitalsValuesTimeseriesQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/useProjectRawWebVitalsValuesTimeseriesQuery';
  19. import {ProjectScore} from 'sentry/views/performance/browser/webVitals/utils/types';
  20. import {SidebarSpacer} from 'sentry/views/performance/transactionSummary/utils';
  21. const CHART_HEIGHTS = 100;
  22. type Props = {
  23. transaction: string;
  24. projectScore?: ProjectScore;
  25. projectScoreIsLoading?: boolean;
  26. search?: string;
  27. };
  28. export function PageOverviewSidebar({
  29. projectScore,
  30. transaction,
  31. projectScoreIsLoading,
  32. }: Props) {
  33. const theme = useTheme();
  34. const router = useRouter();
  35. const pageFilters = usePageFilters();
  36. const {period, start, end, utc} = pageFilters.selection.datetime;
  37. const shouldDoublePeriod = shouldFetchPreviousPeriod({
  38. includePrevious: true,
  39. period,
  40. start,
  41. end,
  42. });
  43. const doubledPeriod = getPeriod({period, start, end}, {shouldDoublePeriod});
  44. const doubledDatetime: PageFilters['datetime'] = {
  45. period: doubledPeriod.statsPeriod ?? null,
  46. start: doubledPeriod.start ?? null,
  47. end: doubledPeriod.end ?? null,
  48. utc,
  49. };
  50. const {data, isLoading: isLoading} = useProjectRawWebVitalsValuesTimeseriesQuery({
  51. transaction,
  52. datetime: doubledDatetime,
  53. });
  54. let seriesData = !isLoading
  55. ? data?.count.map(({name, value}) => ({
  56. name,
  57. value,
  58. }))
  59. : [];
  60. // Trim off last data point since it's incomplete
  61. if (seriesData.length > 0 && period && !start && !end) {
  62. seriesData = seriesData.slice(0, -1);
  63. }
  64. const dataMiddleIndex = Math.floor(seriesData.length / 2);
  65. const currentSeries = shouldDoublePeriod
  66. ? seriesData.slice(dataMiddleIndex)
  67. : seriesData;
  68. const previousSeries = seriesData.slice(0, dataMiddleIndex);
  69. const initialCount = !isLoading
  70. ? previousSeries.reduce((acc, {value}) => acc + value, 0)
  71. : undefined;
  72. const currentCount = !isLoading
  73. ? currentSeries.reduce((acc, {value}) => acc + value, 0)
  74. : undefined;
  75. const countDiff =
  76. !isLoading && currentCount !== undefined && initialCount !== undefined
  77. ? currentCount / initialCount
  78. : undefined;
  79. const throughtputData: LineChartSeries[] = [
  80. {
  81. data: currentSeries,
  82. seriesName: t('Page Loads'),
  83. },
  84. ];
  85. const diffToColor = (diff?: number, reverse?: boolean) => {
  86. if (diff === undefined) {
  87. return undefined;
  88. }
  89. if (diff > 1) {
  90. if (reverse) {
  91. return theme.red300;
  92. }
  93. return theme.green300;
  94. }
  95. if (diff < 1) {
  96. if (reverse) {
  97. return theme.green300;
  98. }
  99. return theme.red300;
  100. }
  101. return undefined;
  102. };
  103. const ringSegmentColors = theme.charts.getColorPalette(3);
  104. const ringBackgroundColors = ringSegmentColors.map(color => `${color}50`);
  105. // Gets weights to dynamically size the performance score ring segments
  106. const weights = projectScore
  107. ? {
  108. cls: projectScore.clsWeight,
  109. fcp: projectScore.fcpWeight,
  110. fid: projectScore.fidWeight,
  111. lcp: projectScore.lcpWeight,
  112. ttfb: projectScore.ttfbWeight,
  113. }
  114. : undefined;
  115. return (
  116. <Fragment>
  117. <SectionHeading>
  118. {t('Performance Score')}
  119. <QuestionTooltip
  120. isHoverable
  121. size="sm"
  122. title={
  123. <span>
  124. {t('The overall performance rating of this page.')}
  125. <br />
  126. <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/#performance-score">
  127. {t('How is this calculated?')}
  128. </ExternalLink>
  129. </span>
  130. }
  131. />
  132. </SectionHeading>
  133. <SidebarPerformanceScoreRingContainer>
  134. {!projectScoreIsLoading && projectScore && (
  135. <PerformanceScoreRingWithTooltips
  136. projectScore={projectScore}
  137. text={projectScore.totalScore}
  138. width={220}
  139. height={180}
  140. ringBackgroundColors={ringBackgroundColors}
  141. ringSegmentColors={ringSegmentColors}
  142. weights={weights}
  143. />
  144. )}
  145. {projectScoreIsLoading && <ProjectScoreEmptyLoadingElement />}
  146. </SidebarPerformanceScoreRingContainer>
  147. <SidebarSpacer />
  148. <SectionHeading>
  149. {t('Page Loads')}
  150. <QuestionTooltip
  151. size="sm"
  152. title={t(
  153. 'The total number of times that users have loaded this page. This number does not include any page navigations beyond initial page loads.'
  154. )}
  155. />
  156. </SectionHeading>
  157. <ChartValue>
  158. {currentCount ? formatAbbreviatedNumber(currentCount) : null}
  159. </ChartValue>
  160. {initialCount && currentCount && countDiff && shouldDoublePeriod ? (
  161. <ChartSubText color={diffToColor(countDiff)}>
  162. {getChartSubText(
  163. countDiff,
  164. formatAbbreviatedNumber(initialCount),
  165. formatAbbreviatedNumber(currentCount)
  166. )}
  167. </ChartSubText>
  168. ) : null}
  169. <ChartZoom router={router} period={period} start={start} end={end} utc={utc}>
  170. {zoomRenderProps => (
  171. <LineChart
  172. {...zoomRenderProps}
  173. height={CHART_HEIGHTS}
  174. series={throughtputData}
  175. xAxis={{show: false}}
  176. grid={{
  177. left: 0,
  178. right: 15,
  179. top: 10,
  180. bottom: -10,
  181. }}
  182. yAxis={{axisLabel: {formatter: number => formatAbbreviatedNumber(number)}}}
  183. tooltip={{valueFormatter: number => formatAbbreviatedNumber(number)}}
  184. />
  185. )}
  186. </ChartZoom>
  187. <SidebarSpacer />
  188. <SectionHeading>
  189. {t('Aggregate Spans')}
  190. <QuestionTooltip
  191. size="sm"
  192. title={t('A synthesized span waterfall for this page.')}
  193. />
  194. </SectionHeading>
  195. <MiniAggregateWaterfallContainer>
  196. <MiniAggregateWaterfall transaction={transaction} />
  197. </MiniAggregateWaterfallContainer>
  198. <SidebarSpacer />
  199. </Fragment>
  200. );
  201. }
  202. const getChartSubText = (
  203. diff?: number,
  204. value?: string | number,
  205. newValue?: string | number
  206. ) => {
  207. if (diff === undefined || value === undefined) {
  208. return null;
  209. }
  210. if (diff > 1) {
  211. const relativeDiff = Math.round((diff - 1) * 1000) / 10;
  212. if (relativeDiff === Infinity) {
  213. return `Up from ${value} to ${newValue}`;
  214. }
  215. return `Up ${relativeDiff}% from ${value}`;
  216. }
  217. if (diff < 1) {
  218. const relativeDiff = Math.round((1 - diff) * 1000) / 10;
  219. return `Down ${relativeDiff}% from ${value}`;
  220. }
  221. return t('No Change');
  222. };
  223. const SidebarPerformanceScoreRingContainer = styled('div')`
  224. display: flex;
  225. justify-content: center;
  226. align-items: center;
  227. margin-bottom: ${space(1)};
  228. `;
  229. const ChartValue = styled('div')`
  230. font-size: ${p => p.theme.fontSizeExtraLarge};
  231. `;
  232. const ChartSubText = styled('div')<{color?: string}>`
  233. font-size: ${p => p.theme.fontSizeMedium};
  234. color: ${p => p.color ?? p.theme.subText};
  235. `;
  236. const SectionHeading = styled('h4')`
  237. display: inline-grid;
  238. grid-auto-flow: column;
  239. gap: ${space(1)};
  240. align-items: center;
  241. color: ${p => p.theme.subText};
  242. font-size: ${p => p.theme.fontSizeMedium};
  243. margin: 0;
  244. `;
  245. const MiniAggregateWaterfallContainer = styled('div')`
  246. margin-top: ${space(1)};
  247. margin-bottom: ${space(1)};
  248. `;
  249. const ProjectScoreEmptyLoadingElement = styled('div')`
  250. width: 220px;
  251. height: 160px;
  252. `;