123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- import {useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import {PlatformIcon} from 'platformicons';
- import {Button} from 'sentry/components/button';
- import {Flex} from 'sentry/components/container/flex';
- 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 {
- 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 type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {getAggregateAlias} from 'sentry/utils/discover/fields';
- import type {
- 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 {isPending} = profilingTransactionsQuery;
- const hasProfilingTransactions =
- !isPending && 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>
- {(isPending || !hasProfilingTransactions) && (
- <Flex column align="center" justify="center" h="100%">
- {isPending ? (
- <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.isPending) {
- 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};
- `;
|