pageOverviewSidebar.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 {ProjectScore} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/calculatePerformanceScore';
  19. import {useProjectRawWebVitalsValuesTimeseriesQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/useProjectRawWebVitalsValuesTimeseriesQuery';
  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. return (
  106. <Fragment>
  107. <SectionHeading>
  108. {t('Performance Score')}
  109. <QuestionTooltip
  110. isHoverable
  111. size="sm"
  112. title={
  113. <span>
  114. {t('The overall performance rating of this page.')}
  115. <br />
  116. <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/#performance-score">
  117. {t('How is this calculated?')}
  118. </ExternalLink>
  119. </span>
  120. }
  121. />
  122. </SectionHeading>
  123. <SidebarPerformanceScoreRingContainer>
  124. {!projectScoreIsLoading && projectScore && (
  125. <PerformanceScoreRingWithTooltips
  126. projectScore={projectScore}
  127. text={projectScore.totalScore}
  128. width={220}
  129. height={160}
  130. ringBackgroundColors={ringBackgroundColors}
  131. ringSegmentColors={ringSegmentColors}
  132. />
  133. )}
  134. {projectScoreIsLoading && <ProjectScoreEmptyLoadingElement />}
  135. </SidebarPerformanceScoreRingContainer>
  136. <SidebarSpacer />
  137. <SectionHeading>
  138. {t('Page Loads')}
  139. <QuestionTooltip
  140. size="sm"
  141. title={t(
  142. 'The total number of times that users have loaded this page. This number does not include any page navigations beyond initial page loads.'
  143. )}
  144. />
  145. </SectionHeading>
  146. <ChartValue>
  147. {currentCount ? formatAbbreviatedNumber(currentCount) : null}
  148. </ChartValue>
  149. {initialCount && currentCount && countDiff && shouldDoublePeriod ? (
  150. <ChartSubText color={diffToColor(countDiff)}>
  151. {getChartSubText(
  152. countDiff,
  153. formatAbbreviatedNumber(initialCount),
  154. formatAbbreviatedNumber(currentCount)
  155. )}
  156. </ChartSubText>
  157. ) : null}
  158. <ChartZoom router={router} period={period} start={start} end={end} utc={utc}>
  159. {zoomRenderProps => (
  160. <LineChart
  161. {...zoomRenderProps}
  162. height={CHART_HEIGHTS}
  163. series={throughtputData}
  164. xAxis={{show: false}}
  165. grid={{
  166. left: 0,
  167. right: 15,
  168. top: 10,
  169. bottom: -10,
  170. }}
  171. yAxis={{axisLabel: {formatter: number => formatAbbreviatedNumber(number)}}}
  172. tooltip={{valueFormatter: number => formatAbbreviatedNumber(number)}}
  173. />
  174. )}
  175. </ChartZoom>
  176. <SidebarSpacer />
  177. <SectionHeading>
  178. {t('Aggregate Spans')}
  179. <QuestionTooltip
  180. size="sm"
  181. title={t('A synthesized span waterfall for this page.')}
  182. />
  183. </SectionHeading>
  184. <MiniAggregateWaterfallContainer>
  185. <MiniAggregateWaterfall transaction={transaction} />
  186. </MiniAggregateWaterfallContainer>
  187. <SidebarSpacer />
  188. </Fragment>
  189. );
  190. }
  191. const getChartSubText = (
  192. diff?: number,
  193. value?: string | number,
  194. newValue?: string | number
  195. ) => {
  196. if (diff === undefined || value === undefined) {
  197. return null;
  198. }
  199. if (diff > 1) {
  200. const relativeDiff = Math.round((diff - 1) * 1000) / 10;
  201. if (relativeDiff === Infinity) {
  202. return `Up from ${value} to ${newValue}`;
  203. }
  204. return `Up ${relativeDiff}% from ${value}`;
  205. }
  206. if (diff < 1) {
  207. const relativeDiff = Math.round((1 - diff) * 1000) / 10;
  208. return `Down ${relativeDiff}% from ${value}`;
  209. }
  210. return t('No Change');
  211. };
  212. const SidebarPerformanceScoreRingContainer = styled('div')`
  213. display: flex;
  214. justify-content: center;
  215. align-items: center;
  216. margin-bottom: ${space(1)};
  217. `;
  218. const ChartValue = styled('div')`
  219. font-size: ${p => p.theme.fontSizeExtraLarge};
  220. `;
  221. const ChartSubText = styled('div')<{color?: string}>`
  222. font-size: ${p => p.theme.fontSizeMedium};
  223. color: ${p => p.color ?? p.theme.subText};
  224. `;
  225. const SectionHeading = styled('h4')`
  226. display: inline-grid;
  227. grid-auto-flow: column;
  228. gap: ${space(1)};
  229. align-items: center;
  230. color: ${p => p.theme.subText};
  231. font-size: ${p => p.theme.fontSizeMedium};
  232. margin: 0;
  233. `;
  234. const MiniAggregateWaterfallContainer = styled('div')`
  235. margin-top: ${space(1)};
  236. margin-bottom: ${space(1)};
  237. `;
  238. const ProjectScoreEmptyLoadingElement = styled('div')`
  239. width: 220px;
  240. height: 160px;
  241. `;