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 ( {t('Performance Score')} {t('The overall performance rating of this page.')}
{t('How is this calculated?')} } />
{!projectScoreIsLoading && projectScore && ( )} {projectScoreIsLoading && } {t('Page Loads')} {currentCount ? formatAbbreviatedNumber(currentCount) : null} {initialCount && currentCount && countDiff && shouldDoublePeriod ? ( {getChartSubText( countDiff, formatAbbreviatedNumber(initialCount), formatAbbreviatedNumber(currentCount) )} ) : null} {zoomRenderProps => ( formatAbbreviatedNumber(number)}}} tooltip={{valueFormatter: number => formatAbbreviatedNumber(number)}} /> )} {!shouldReplaceFidWithInp && ( {t('Aggregate Spans')} )} {shouldReplaceFidWithInp ? ( {t('Interactions')} {currentInpCount ? formatAbbreviatedNumber(currentInpCount) : null} {initialInpCount && currentInpCount && inpCountDiff && shouldDoublePeriod ? ( {getChartSubText( inpCountDiff, formatAbbreviatedNumber(initialInpCount), formatAbbreviatedNumber(currentInpCount) )} ) : null} {zoomRenderProps => ( formatAbbreviatedNumber(number)}, }} tooltip={{valueFormatter: number => formatAbbreviatedNumber(number)}} /> )} ) : null}
); } 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; `;