import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {PlatformIcon} from 'platformicons'; import {Button} from 'sentry/components/button'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PerformanceDuration from 'sentry/components/performanceDuration'; import {Flex} from 'sentry/components/profiling/flex'; import { FunctionsMiniGrid, FunctionsMiniGridEmptyState, FunctionsMiniGridLoading, } from 'sentry/components/profiling/functionsMiniGrid'; import {TextTruncateOverflow} from 'sentry/components/profiling/textTruncateOverflow'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {Organization, Project} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getAggregateAlias} from 'sentry/utils/discover/fields'; import {EventsResults, EventsResultsDataRow} from 'sentry/utils/profiling/hooks/types'; import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents'; import {useProfilingTransactionQuickSummary} from 'sentry/utils/profiling/hooks/useProfilingTransactionQuickSummary'; import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes'; import {makeFormatTo} from 'sentry/utils/profiling/units/units'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; const fields = ['transaction', 'project.id', 'last_seen()', 'p95()', 'count()'] as const; type SlowestTransactionsFields = (typeof fields)[number]; export function ProfilingSlowestTransactionsPanel() { const profilingTransactionsQuery = useProfileEvents({ fields, sort: { key: 'p95()', order: 'desc', }, limit: 3, query: 'count():>3', referrer: 'api.profiling.landing-slowest-transaction-panel', }); const [openPanel, setOpenPanel] = useState<null | string>(null); const profilingTransactions = useMemo( () => profilingTransactionsQuery.data?.data ?? [], [profilingTransactionsQuery.data] ); const transactionNames = useMemo( () => profilingTransactions.map(txn => txn.transaction), [profilingTransactions] ); if (transactionNames.length > 0 && !transactionNames.includes(openPanel)) { const firstTransaction = transactionNames[0]; setOpenPanel(firstTransaction as string); } const {isLoading} = profilingTransactionsQuery; const hasProfilingTransactions = !isLoading && profilingTransactions && profilingTransactions.length > 0; return ( <FlexPanel> <Flex column h="100%"> <Flex column p={space(1.5)}> <PanelHeading>{t('Slowest Transactions')}</PanelHeading> <PanelSubheading> {t('Slowest transactions that could use some optimization.')} </PanelSubheading> </Flex> {(isLoading || !hasProfilingTransactions) && ( <Flex column align="center" justify="center" h="100%"> {isLoading ? ( <LoadingIndicator /> ) : ( !hasProfilingTransactions && ( <Flex.Item> <EmptyStateWarning> <p>{t('No results found')}</p> <EmptyStateDescription> {t( 'Transactions may not be listed due to the filters above or a low number of profiles.' )} </EmptyStateDescription> </EmptyStateWarning> </Flex.Item> ) )} </Flex> )} {profilingTransactions?.map(transaction => { return ( <SlowestTransactionPanelItem key={transaction.transaction as string} transaction={transaction} open={transaction.transaction === openPanel} onOpen={() => setOpenPanel(transaction.transaction as string)} units={profilingTransactionsQuery.data?.meta.units} /> ); })} </Flex> </FlexPanel> ); } interface SlowestTransactionPanelItemProps { onOpen: () => void; open: boolean; transaction: EventsResultsDataRow<SlowestTransactionsFields>; units?: EventsResults<SlowestTransactionsFields>['meta']['units']; } function SlowestTransactionPanelItem({ transaction, open, onOpen, units, }: SlowestTransactionPanelItemProps) { const {query} = useLocation(); const organization = useOrganization(); const projects = useProjects(); const transactionProject = useMemo( () => projects.projects.find(p => p.id === String(transaction['project.id'])), [projects.projects, transaction] ); if (!transactionProject && !projects.fetching && projects.projects.length > 0) { return null; } const key: SlowestTransactionsFields = 'p95()'; const formatter = makeFormatTo( units?.[key] ?? units?.[getAggregateAlias(key)] ?? 'nanoseconds', 'milliseconds' ); return ( <PanelItem key={transaction.transaction as string}> <Flex justify="space-between" gap={space(1)}> <PlatformIcon platform={transactionProject?.platform ?? 'default'} /> <Flex.Item grow={1} onClick={onOpen} css={{ cursor: 'pointer', }} > <div css={{ maxWidth: 'fit-content', }} > <Link to={generateProfileSummaryRouteWithQuery({ query, orgSlug: organization.slug, projectSlug: transactionProject?.slug!, transaction: transaction.transaction as string, })} onClick={() => { trackAnalytics('profiling_views.go_to_transaction', { source: 'slowest_transaction_panel', organization, }); }} > <TextTruncateOverflow> {transaction.transaction as string} </TextTruncateOverflow> </Link> </div> </Flex.Item> <PerformanceDuration milliseconds={formatter(transaction[key] as number)} abbreviation /> <Button borderless size="zero" onClick={onOpen}> <IconChevron direction={open ? 'up' : 'down'} size="xs" /> </Button> </Flex> <PanelItemBody style={{ height: open ? 160 : 0, }} > {open && transactionProject && ( <PanelItemFunctionsMiniGrid transaction={String(transaction.transaction)} organization={organization} project={transactionProject} /> )} </PanelItemBody> </PanelItem> ); } interface PanelItemFunctionsMiniGridProps { organization: Organization; project: Project; transaction: string; } function PanelItemFunctionsMiniGrid(props: PanelItemFunctionsMiniGridProps) { const {transaction, project, organization} = props; const {functionsQuery, functions} = useProfilingTransactionQuickSummary({ transaction, project, referrer: 'api.profiling.landing-slowest-transaction-panel', skipLatestProfile: true, skipSlowestProfile: true, }); if (functionsQuery.isLoading) { return <FunctionsMiniGridLoading />; } if (!functions || (functions && functions.length === 0)) { return <FunctionsMiniGridEmptyState />; } return ( <PanelItemBodyInner> <FunctionsMiniGrid functions={functions} organization={organization} project={project} onLinkClick={() => trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'slowest_transaction_panel', }) } /> </PanelItemBodyInner> ); } const FlexPanel = styled(Panel)` display: flex; flex-direction: column; `; const PanelHeading = styled('span')` font-size: ${p => p.theme.text.cardTitle.fontSize}; font-weight: ${p => p.theme.text.cardTitle.fontWeight}; line-height: ${p => p.theme.text.cardTitle.lineHeight}; `; const PanelSubheading = styled('span')` color: ${p => p.theme.subText}; `; const PanelItem = styled('div')` padding: ${space(1)} ${space(1.5)}; border-top: 1px solid ${p => p.theme.border}; `; const PanelItemBody = styled('div')` transition: height 0.1s ease; width: 100%; overflow: hidden; `; // TODO: simple layout stuff like this should come from a primitive component and we should really stop this `styled` nonsense const PanelItemBodyInner = styled('div')` padding-top: ${space(1.5)}; `; const EmptyStateDescription = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; `;