123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- import {Fragment} from 'react';
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import ChartZoom from 'sentry/components/charts/chartZoom';
- import type {LineChartSeries} from 'sentry/components/charts/lineChart';
- import {LineChart} from 'sentry/components/charts/lineChart';
- import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
- import ExternalLink from 'sentry/components/links/externalLink';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {PageFilters} from 'sentry/types';
- import type {SeriesDataUnit} from 'sentry/types/echarts';
- import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
- import {getPeriod} from 'sentry/utils/getPeriod';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import useRouter from 'sentry/utils/useRouter';
- import {MiniAggregateWaterfall} from 'sentry/views/performance/browser/webVitals/components/miniAggregateWaterfall';
- import PerformanceScoreRingWithTooltips from 'sentry/views/performance/browser/webVitals/components/performanceScoreRingWithTooltips';
- import {useProjectRawWebVitalsValuesTimeseriesQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/useProjectRawWebVitalsValuesTimeseriesQuery';
- import type {ProjectScore} from 'sentry/views/performance/browser/webVitals/utils/types';
- import {useReplaceFidWithInpSetting} from 'sentry/views/performance/browser/webVitals/utils/useReplaceFidWithInpSetting';
- import {SidebarSpacer} from 'sentry/views/performance/transactionSummary/utils';
- const CHART_HEIGHTS = 100;
- type Props = {
- transaction: string;
- projectScore?: ProjectScore;
- projectScoreIsLoading?: boolean;
- search?: string;
- };
- export function PageOverviewSidebar({
- projectScore,
- transaction,
- projectScoreIsLoading,
- }: Props) {
- const theme = useTheme();
- const router = useRouter();
- const pageFilters = usePageFilters();
- const {period, start, end, utc} = pageFilters.selection.datetime;
- const shouldDoublePeriod = shouldFetchPreviousPeriod({
- includePrevious: true,
- period,
- start,
- end,
- });
- const doubledPeriod = getPeriod({period, start, end}, {shouldDoublePeriod});
- const doubledDatetime: PageFilters['datetime'] = {
- period: doubledPeriod.statsPeriod ?? null,
- start: doubledPeriod.start ?? null,
- end: doubledPeriod.end ?? null,
- utc,
- };
- const shouldReplaceFidWithInp = useReplaceFidWithInpSetting();
- const {data, isLoading: isLoading} = useProjectRawWebVitalsValuesTimeseriesQuery({
- transaction,
- datetime: doubledDatetime,
- });
- const {countDiff, currentSeries, currentCount, initialCount} = processSeriesData(
- data?.count,
- isLoading,
- pageFilters.selection.datetime,
- shouldDoublePeriod
- );
- const throughtputData: LineChartSeries[] = [
- {
- data: currentSeries,
- seriesName: t('Page Loads'),
- },
- ];
- const {
- countDiff: inpCountDiff,
- currentSeries: currentInpSeries,
- currentCount: currentInpCount,
- initialCount: initialInpCount,
- } = processSeriesData(
- data.countInp,
- isLoading,
- pageFilters.selection.datetime,
- shouldDoublePeriod
- );
- const inpThroughtputData: LineChartSeries[] = [
- {
- data: currentInpSeries,
- seriesName: t('Interactions'),
- },
- ];
- const diffToColor = (diff?: number, reverse?: boolean) => {
- if (diff === undefined) {
- return undefined;
- }
- if (diff > 1) {
- if (reverse) {
- return theme.red300;
- }
- return theme.green300;
- }
- if (diff < 1) {
- if (reverse) {
- return theme.green300;
- }
- return theme.red300;
- }
- return undefined;
- };
- const ringSegmentColors = theme.charts.getColorPalette(3);
- const ringBackgroundColors = ringSegmentColors.map(color => `${color}50`);
- // Gets weights to dynamically size the performance score ring segments
- const weights = projectScore
- ? {
- lcp: projectScore.lcpWeight,
- fcp: projectScore.fcpWeight,
- fid: shouldReplaceFidWithInp ? 0 : projectScore.fidWeight,
- inp: shouldReplaceFidWithInp ? projectScore.inpWeight : 0,
- cls: projectScore.clsWeight,
- ttfb: projectScore.ttfbWeight,
- }
- : undefined;
- return (
- <Fragment>
- <SectionHeading>
- {t('Performance Score')}
- <QuestionTooltip
- isHoverable
- size="sm"
- title={
- <span>
- {t('The overall performance rating of this page.')}
- <br />
- <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/#performance-score">
- {t('How is this calculated?')}
- </ExternalLink>
- </span>
- }
- />
- </SectionHeading>
- <SidebarPerformanceScoreRingContainer>
- {!projectScoreIsLoading && projectScore && (
- <PerformanceScoreRingWithTooltips
- projectScore={projectScore}
- text={projectScore.totalScore}
- width={220}
- height={200}
- ringBackgroundColors={ringBackgroundColors}
- ringSegmentColors={ringSegmentColors}
- weights={weights}
- />
- )}
- {projectScoreIsLoading && <ProjectScoreEmptyLoadingElement />}
- </SidebarPerformanceScoreRingContainer>
- <SidebarSpacer />
- <SectionHeading>
- {t('Page Loads')}
- <QuestionTooltip
- size="sm"
- title={t(
- 'The total number of times that users have loaded this page. This number does not include any page navigations beyond initial page loads.'
- )}
- />
- </SectionHeading>
- <ChartValue>
- {currentCount ? formatAbbreviatedNumber(currentCount) : null}
- </ChartValue>
- {initialCount && currentCount && countDiff && shouldDoublePeriod ? (
- <ChartSubText color={diffToColor(countDiff)}>
- {getChartSubText(
- countDiff,
- formatAbbreviatedNumber(initialCount),
- formatAbbreviatedNumber(currentCount)
- )}
- </ChartSubText>
- ) : null}
- <ChartZoom router={router} period={period} start={start} end={end} utc={utc}>
- {zoomRenderProps => (
- <LineChart
- {...zoomRenderProps}
- height={CHART_HEIGHTS}
- series={throughtputData}
- xAxis={{show: false}}
- grid={{
- left: 0,
- right: 15,
- top: 10,
- bottom: -10,
- }}
- yAxis={{axisLabel: {formatter: number => formatAbbreviatedNumber(number)}}}
- tooltip={{valueFormatter: number => formatAbbreviatedNumber(number)}}
- />
- )}
- </ChartZoom>
- {!shouldReplaceFidWithInp && (
- <Fragment>
- <SidebarSpacer />
- <SectionHeading>
- {t('Aggregate Spans')}
- <QuestionTooltip
- size="sm"
- title={t('A synthesized span waterfall for this page.')}
- />
- </SectionHeading>
- </Fragment>
- )}
- <MiniAggregateWaterfallContainer>
- <MiniAggregateWaterfall transaction={transaction} />
- </MiniAggregateWaterfallContainer>
- <SidebarSpacer />
- {shouldReplaceFidWithInp ? (
- <Fragment>
- <SidebarSpacer />
- <SectionHeading>
- {t('Interactions')}
- <QuestionTooltip
- size="sm"
- title={t(
- 'The total number of times that users performed an INP on this page.'
- )}
- />
- </SectionHeading>
- <ChartValue>
- {currentInpCount ? formatAbbreviatedNumber(currentInpCount) : null}
- </ChartValue>
- {initialInpCount && currentInpCount && inpCountDiff && shouldDoublePeriod ? (
- <ChartSubText color={diffToColor(inpCountDiff)}>
- {getChartSubText(
- inpCountDiff,
- formatAbbreviatedNumber(initialInpCount),
- formatAbbreviatedNumber(currentInpCount)
- )}
- </ChartSubText>
- ) : null}
- <ChartZoom router={router} period={period} start={start} end={end} utc={utc}>
- {zoomRenderProps => (
- <LineChart
- {...zoomRenderProps}
- height={CHART_HEIGHTS}
- series={inpThroughtputData}
- xAxis={{show: false}}
- grid={{
- left: 0,
- right: 15,
- top: 10,
- bottom: -10,
- }}
- yAxis={{
- axisLabel: {formatter: number => formatAbbreviatedNumber(number)},
- }}
- tooltip={{valueFormatter: number => formatAbbreviatedNumber(number)}}
- />
- )}
- </ChartZoom>
- <SidebarSpacer />
- </Fragment>
- ) : null}
- </Fragment>
- );
- }
- const getChartSubText = (
- diff?: number,
- value?: string | number,
- newValue?: string | number
- ) => {
- if (diff === undefined || value === undefined) {
- return null;
- }
- if (diff > 1) {
- const relativeDiff = Math.round((diff - 1) * 1000) / 10;
- if (relativeDiff === Infinity) {
- return `Up from ${value} to ${newValue}`;
- }
- return `Up ${relativeDiff}% from ${value}`;
- }
- if (diff < 1) {
- const relativeDiff = Math.round((1 - diff) * 1000) / 10;
- return `Down ${relativeDiff}% from ${value}`;
- }
- return t('No Change');
- };
- const processSeriesData = (
- count: SeriesDataUnit[],
- isLoading: boolean,
- {period, start, end}: PageFilters['datetime'],
- shouldDoublePeriod: boolean
- ) => {
- let seriesData = !isLoading
- ? count.map(({name, value}) => ({
- name,
- value,
- }))
- : [];
- // Trim off last data point since it's incomplete
- if (seriesData.length > 0 && period && !start && !end) {
- seriesData = seriesData.slice(0, -1);
- }
- const dataMiddleIndex = Math.floor(seriesData.length / 2);
- const currentSeries = shouldDoublePeriod
- ? seriesData.slice(dataMiddleIndex)
- : seriesData;
- const previousSeries = seriesData.slice(0, dataMiddleIndex);
- const initialCount = !isLoading
- ? previousSeries.reduce((acc, {value}) => acc + value, 0)
- : undefined;
- const currentCount = !isLoading
- ? currentSeries.reduce((acc, {value}) => acc + value, 0)
- : undefined;
- const countDiff =
- !isLoading && currentCount !== undefined && initialCount !== undefined
- ? currentCount / initialCount
- : undefined;
- return {countDiff, currentSeries, currentCount, initialCount};
- };
- const SidebarPerformanceScoreRingContainer = styled('div')`
- display: flex;
- justify-content: center;
- align-items: center;
- margin-bottom: ${space(1)};
- `;
- const ChartValue = styled('div')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- `;
- const ChartSubText = styled('div')<{color?: string}>`
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.color ?? p.theme.subText};
- `;
- const SectionHeading = styled('h4')`
- display: inline-grid;
- grid-auto-flow: column;
- gap: ${space(1)};
- align-items: center;
- color: ${p => p.theme.subText};
- font-size: ${p => p.theme.fontSizeMedium};
- margin: 0;
- `;
- const MiniAggregateWaterfallContainer = styled('div')`
- margin-top: ${space(1)};
- margin-bottom: ${space(1)};
- `;
- const ProjectScoreEmptyLoadingElement = styled('div')`
- width: 220px;
- height: 160px;
- `;
|