import {useMemo} from 'react'; import {browserHistory, Link} from 'react-router'; import styled from '@emotion/styled'; import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import GridEditable, { COL_WIDTH_UNDEFINED, GridColumnHeader, GridColumnOrder, } from 'sentry/components/gridEditable'; import SortLink from 'sentry/components/gridEditable/sortLink'; import ExternalLink from 'sentry/components/links/externalLink'; import Pagination from 'sentry/components/pagination'; import SearchBar from 'sentry/components/searchBar'; import {Tooltip} from 'sentry/components/tooltip'; import {IconChevron} from 'sentry/icons/iconChevron'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {Sort} from 'sentry/utils/discover/fields'; import {formatAbbreviatedNumber, getDuration} from 'sentry/utils/formatters'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import {PerformanceBadge} from 'sentry/views/performance/browser/webVitals/components/performanceBadge'; import {USE_STORED_SCORES} from 'sentry/views/performance/browser/webVitals/settings'; import {calculateOpportunity} from 'sentry/views/performance/browser/webVitals/utils/calculateOpportunity'; import {calculatePerformanceScoreFromTableDataRow} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/calculatePerformanceScore'; import {useProjectRawWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/useProjectRawWebVitalsQuery'; import {calculatePerformanceScoreFromStoredTableDataRow} from 'sentry/views/performance/browser/webVitals/utils/queries/storedScoreQueries/calculatePerformanceScoreFromStored'; import {useProjectWebVitalsScoresQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/storedScoreQueries/useProjectWebVitalsScoresQuery'; import {useTransactionWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/useTransactionWebVitalsQuery'; import { Row, SORTABLE_FIELDS, } from 'sentry/views/performance/browser/webVitals/utils/types'; import {useWebVitalsSort} from 'sentry/views/performance/browser/webVitals/utils/useWebVitalsSort'; type RowWithScoreAndOpportunity = Row & {score: number; opportunity?: number}; type Column = GridColumnHeader; const columnOrder: GridColumnOrder[] = [ {key: 'transaction', width: COL_WIDTH_UNDEFINED, name: 'Pages'}, {key: 'count()', width: COL_WIDTH_UNDEFINED, name: 'Pageloads'}, {key: 'p75(measurements.lcp)', width: COL_WIDTH_UNDEFINED, name: 'LCP'}, {key: 'p75(measurements.fcp)', width: COL_WIDTH_UNDEFINED, name: 'FCP'}, {key: 'p75(measurements.fid)', width: COL_WIDTH_UNDEFINED, name: 'FID'}, {key: 'p75(measurements.cls)', width: COL_WIDTH_UNDEFINED, name: 'CLS'}, {key: 'p75(measurements.ttfb)', width: COL_WIDTH_UNDEFINED, name: 'TTFB'}, {key: 'score', width: COL_WIDTH_UNDEFINED, name: 'Score'}, {key: 'opportunity', width: COL_WIDTH_UNDEFINED, name: 'Opportunity'}, ]; const MAX_ROWS = 25; export function PagePerformanceTable() { const organization = useOrganization(); const location = useLocation(); const {projects} = useProjects(); const query = decodeScalar(location.query.query, ''); const project = useMemo( () => projects.find(p => p.id === String(location.query.project)), [projects, location.query.project] ); const sort = useWebVitalsSort(); const {data: projectData, isLoading: isProjectWebVitalsQueryLoading} = useProjectRawWebVitalsQuery({transaction: query}); const {data: projectScoresData, isLoading: isProjectScoresLoading} = useProjectWebVitalsScoresQuery({ transaction: query, enabled: USE_STORED_SCORES, }); const projectScore = USE_STORED_SCORES ? calculatePerformanceScoreFromStoredTableDataRow(projectScoresData?.data?.[0]) : calculatePerformanceScoreFromTableDataRow(projectData?.data?.[0]); const { data, pageLinks, isLoading: isTransactionWebVitalsQueryLoading, } = useTransactionWebVitalsQuery({limit: MAX_ROWS, transaction: query}); const count = projectData?.data?.[0]?.['count()'] as number; const tableData: RowWithScoreAndOpportunity[] = data.map(row => ({ ...row, opportunity: count !== undefined ? calculateOpportunity( projectScore.totalScore ?? 0, count, row.score, row['count()'] ) : undefined, })); const getFormattedDuration = (value: number) => { return getDuration(value, value < 1 ? 0 : 2, true); }; function renderHeadCell(col: Column) { function generateSortLink() { let newSortDirection: Sort['kind'] = 'desc'; if (sort?.field === col.key) { if (sort.kind === 'desc') { newSortDirection = 'asc'; } } const newSort = `${newSortDirection === 'desc' ? '-' : ''}${col.key}`; return { ...location, query: {...location.query, sort: newSort}, }; } const canSort = (SORTABLE_FIELDS as unknown as string[]).includes(col.key); if (canSort) { return ( ); } if (col.key === 'score') { return ( {t('The overall performance rating of this page.')}
{t('How is this calculated?')} } > {t('Perf Score')}
); } if (col.key === 'opportunity') { return ( {t( "A number rating how impactful a performance improvement on this page would be to your application's overall Performance Score." )}
{t('How is this calculated?')} } > {col.name}
); } return {col.name}; } function renderBodyCell(col: Column, row: RowWithScoreAndOpportunity) { const {key} = col; if (key === 'score') { return ( ); } if (key === 'count()') { return {formatAbbreviatedNumber(row['count()'])}; } if (key === 'transaction') { return ( {project && ( )} {row.transaction} ); } if ( [ 'p75(measurements.fcp)', 'p75(measurements.lcp)', 'p75(measurements.ttfb)', 'p75(measurements.fid)', ].includes(key) ) { return {getFormattedDuration((row[key] as number) / 1000)}; } if (key === 'p75(measurements.cls)') { return {Math.round((row[key] as number) * 100) / 100}; } if (key === 'opportunity') { if (row.opportunity !== undefined) { return ( {Math.round((row.opportunity as number) * 100) / 100} ); } return null; } return {row[key]}; } const handleSearch = (newQuery: string) => { browserHistory.push({ ...location, query: { ...location.query, query: newQuery === '' ? undefined : `*${newQuery}*`, cursor: undefined, }, }); }; return ( {/* The Pagination component disappears if pageLinks is not defined, which happens any time the table data is loading. So we render a disabled button bar if pageLinks is not defined to minimize ui shifting */} {!pageLinks && (