import {useMemo} from 'react'; import {Link} from 'react-router'; import styled from '@emotion/styled'; import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; import {Button, LinkButton} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import SearchBar from 'sentry/components/events/searchBar'; 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 {Tooltip} from 'sentry/components/tooltip'; import {IconChevron, IconPlay, IconProfiling} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import {Sort} from 'sentry/utils/discover/fields'; import {generateEventSlug} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; import {getDuration} from 'sentry/utils/formatters'; import {getTransactionDetailsUrl} from 'sentry/utils/performance/urls'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; 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 useRouter from 'sentry/utils/useRouter'; import {useRoutes} from 'sentry/utils/useRoutes'; import {PerformanceBadge} from 'sentry/views/performance/browser/webVitals/components/performanceBadge'; import {useTransactionSamplesWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/useTransactionSamplesWebVitalsQuery'; import { DEFAULT_INDEXED_SORT, SORTABLE_INDEXED_FIELDS, TransactionSampleRow, } from 'sentry/views/performance/browser/webVitals/utils/types'; import {useWebVitalsSort} from 'sentry/views/performance/browser/webVitals/utils/useWebVitalsSort'; import {generateReplayLink} from 'sentry/views/performance/transactionSummary/utils'; export type TransactionSampleRowWithScoreAndExtra = TransactionSampleRow & { score: number; }; type Column = GridColumnHeader; export const COLUMN_ORDER: GridColumnOrder< keyof TransactionSampleRowWithScoreAndExtra >[] = [ {key: 'user.display', width: COL_WIDTH_UNDEFINED, name: 'User'}, {key: 'transaction.duration', width: COL_WIDTH_UNDEFINED, name: 'Duration'}, {key: 'measurements.lcp', width: COL_WIDTH_UNDEFINED, name: 'LCP'}, {key: 'measurements.fcp', width: COL_WIDTH_UNDEFINED, name: 'FCP'}, {key: 'measurements.fid', width: COL_WIDTH_UNDEFINED, name: 'FID'}, {key: 'measurements.cls', width: COL_WIDTH_UNDEFINED, name: 'CLS'}, {key: 'measurements.ttfb', width: COL_WIDTH_UNDEFINED, name: 'TTFB'}, {key: 'score', width: COL_WIDTH_UNDEFINED, name: 'Score'}, ]; type Props = { transaction: string; columnOrder?: GridColumnOrder[]; limit?: number; search?: string; }; export function PageSamplePerformanceTable({ transaction, columnOrder, search, limit = 9, }: Props) { const location = useLocation(); const {projects} = useProjects(); const organization = useOrganization(); const routes = useRoutes(); const router = useRouter(); const sort = useWebVitalsSort({ defaultSort: DEFAULT_INDEXED_SORT, sortableFields: SORTABLE_INDEXED_FIELDS as unknown as string[], }); const replayLinkGenerator = generateReplayLink(routes); const project = useMemo( () => projects.find(p => p.id === String(location.query.project)), [projects, location.query.project] ); const query = decodeScalar(location.query.query); // Do 3 queries filtering on LCP to get a spread of good, meh, and poor events // We can't query by performance score yet, so we're using LCP as a best estimate const {data, isLoading, pageLinks} = useTransactionSamplesWebVitalsQuery({ limit, transaction, query: search, withProfiles: true, }); const tableData: TransactionSampleRowWithScoreAndExtra[] = data.map(row => ({ ...row, view: null, })); 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_INDEXED_FIELDS as ReadonlyArray).includes(col.key); if ( [ 'measurements.fcp', 'measurements.lcp', 'measurements.ttfb', 'measurements.fid', 'measurements.cls', 'transaction.duration', ].includes(col.key) ) { if (canSort) { return ( ); } return ( {col.name} ); } if (col.key === 'score') { return ( {t('The overall performance rating of this page.')}
{t('How is this calculated?')} } > {t('Perf Score')}
); } if (col.key === 'replayId' || col.key === 'profile.id') { return ( {col.name} ); } return {col.name}; } function renderBodyCell(col: Column, row: TransactionSampleRowWithScoreAndExtra) { const {key} = col; if (key === 'score') { return ( ); } if (key === 'transaction') { return ( {project && ( )} {row.transaction} ); } if ( [ 'measurements.fcp', 'measurements.lcp', 'measurements.ttfb', 'measurements.fid', 'transaction.duration', ].includes(key) ) { return ( {row[key] === null ? ( {' \u2014 '} ) : ( getFormattedDuration((row[key] as number) / 1000) )} ); } if (['measurements.cls', 'opportunity'].includes(key)) { return {Math.round((row[key] as number) * 100) / 100}; } if (key === 'profile.id') { const profileTarget = defined(row.projectSlug) && defined(row['profile.id']) ? generateProfileFlamechartRoute({ orgSlug: organization.slug, projectSlug: row.projectSlug, profileId: String(row['profile.id']), }) : null; return ( {profileTarget && ( )} ); } if (key === 'replayId') { const replayTarget = row['transaction.duration'] !== null && replayLinkGenerator( organization, { replayId: row.replayId, id: row.id, 'transaction.duration': row['transaction.duration'], timestamp: row.timestamp, }, undefined ); return ( {replayTarget && Object.keys(replayTarget).length > 0 && ( )} ); } if (key === 'id') { const eventSlug = generateEventSlug({...row, project: row.projectSlug}); const eventTarget = getTransactionDetailsUrl(organization.slug, eventSlug); return ( {getShortEventId(row.id)} ); } return {row[key]}; } return ( router.replace({ ...location, query: {...location.query, query: queryString}, }) } /> {/* 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 && (