profilingSlowestTransactionsPanel.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import {useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import {Button} from 'sentry/components/button';
  5. import {Flex} from 'sentry/components/container/flex';
  6. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  7. import Link from 'sentry/components/links/link';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import Panel from 'sentry/components/panels/panel';
  10. import PerformanceDuration from 'sentry/components/performanceDuration';
  11. import {
  12. FunctionsMiniGrid,
  13. FunctionsMiniGridEmptyState,
  14. FunctionsMiniGridLoading,
  15. } from 'sentry/components/profiling/functionsMiniGrid';
  16. import {TextTruncateOverflow} from 'sentry/components/profiling/textTruncateOverflow';
  17. import {IconChevron} from 'sentry/icons';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {Organization} from 'sentry/types/organization';
  21. import type {Project} from 'sentry/types/project';
  22. import {trackAnalytics} from 'sentry/utils/analytics';
  23. import {getAggregateAlias} from 'sentry/utils/discover/fields';
  24. import type {
  25. EventsResults,
  26. EventsResultsDataRow,
  27. } from 'sentry/utils/profiling/hooks/types';
  28. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  29. import {useProfilingTransactionQuickSummary} from 'sentry/utils/profiling/hooks/useProfilingTransactionQuickSummary';
  30. import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes';
  31. import {makeFormatTo} from 'sentry/utils/profiling/units/units';
  32. import {useLocation} from 'sentry/utils/useLocation';
  33. import useOrganization from 'sentry/utils/useOrganization';
  34. import useProjects from 'sentry/utils/useProjects';
  35. const fields = ['transaction', 'project.id', 'last_seen()', 'p95()', 'count()'] as const;
  36. type SlowestTransactionsFields = (typeof fields)[number];
  37. export function ProfilingSlowestTransactionsPanel() {
  38. const profilingTransactionsQuery = useProfileEvents({
  39. fields,
  40. sort: {
  41. key: 'p95()',
  42. order: 'desc',
  43. },
  44. limit: 3,
  45. query: 'count():>3',
  46. referrer: 'api.profiling.landing-slowest-transaction-panel',
  47. });
  48. const [openPanel, setOpenPanel] = useState<null | string>(null);
  49. const profilingTransactions = useMemo(
  50. () => profilingTransactionsQuery.data?.data ?? [],
  51. [profilingTransactionsQuery.data]
  52. );
  53. const transactionNames = useMemo(
  54. () => profilingTransactions.map(txn => txn.transaction),
  55. [profilingTransactions]
  56. );
  57. if (transactionNames.length > 0 && !transactionNames.includes(openPanel)) {
  58. const firstTransaction = transactionNames[0];
  59. setOpenPanel(firstTransaction as string);
  60. }
  61. const {isPending} = profilingTransactionsQuery;
  62. const hasProfilingTransactions =
  63. !isPending && profilingTransactions && profilingTransactions.length > 0;
  64. return (
  65. <FlexPanel>
  66. <Flex column h="100%">
  67. <Flex column p={space(1.5)}>
  68. <PanelHeading>{t('Slowest Transactions')}</PanelHeading>
  69. <PanelSubheading>
  70. {t('Slowest transactions that could use some optimization.')}
  71. </PanelSubheading>
  72. </Flex>
  73. {(isPending || !hasProfilingTransactions) && (
  74. <Flex column align="center" justify="center" h="100%">
  75. {isPending ? (
  76. <LoadingIndicator />
  77. ) : (
  78. !hasProfilingTransactions && (
  79. <Flex.Item>
  80. <EmptyStateWarning>
  81. <p>{t('No results found')}</p>
  82. <EmptyStateDescription>
  83. {t(
  84. 'Transactions may not be listed due to the filters above or a low number of profiles.'
  85. )}
  86. </EmptyStateDescription>
  87. </EmptyStateWarning>
  88. </Flex.Item>
  89. )
  90. )}
  91. </Flex>
  92. )}
  93. {profilingTransactions?.map(transaction => {
  94. return (
  95. <SlowestTransactionPanelItem
  96. key={transaction.transaction as string}
  97. transaction={transaction}
  98. open={transaction.transaction === openPanel}
  99. onOpen={() => setOpenPanel(transaction.transaction as string)}
  100. units={profilingTransactionsQuery.data?.meta.units}
  101. />
  102. );
  103. })}
  104. </Flex>
  105. </FlexPanel>
  106. );
  107. }
  108. interface SlowestTransactionPanelItemProps {
  109. onOpen: () => void;
  110. open: boolean;
  111. transaction: EventsResultsDataRow<SlowestTransactionsFields>;
  112. units?: EventsResults<SlowestTransactionsFields>['meta']['units'];
  113. }
  114. function SlowestTransactionPanelItem({
  115. transaction,
  116. open,
  117. onOpen,
  118. units,
  119. }: SlowestTransactionPanelItemProps) {
  120. const {query} = useLocation();
  121. const organization = useOrganization();
  122. const projects = useProjects();
  123. const transactionProject = useMemo(
  124. () => projects.projects.find(p => p.id === String(transaction['project.id'])),
  125. [projects.projects, transaction]
  126. );
  127. if (!transactionProject && !projects.fetching && projects.projects.length > 0) {
  128. return null;
  129. }
  130. const key: SlowestTransactionsFields = 'p95()';
  131. const formatter = makeFormatTo(
  132. units?.[key] ?? units?.[getAggregateAlias(key)] ?? 'nanoseconds',
  133. 'milliseconds'
  134. );
  135. return (
  136. <PanelItem key={transaction.transaction as string}>
  137. <Flex justify="space-between" gap={space(1)}>
  138. <PlatformIcon platform={transactionProject?.platform ?? 'default'} />
  139. <Flex.Item
  140. grow={1}
  141. onClick={onOpen}
  142. css={{
  143. cursor: 'pointer',
  144. }}
  145. >
  146. <div
  147. css={{
  148. maxWidth: 'fit-content',
  149. }}
  150. >
  151. <Link
  152. to={generateProfileSummaryRouteWithQuery({
  153. query,
  154. orgSlug: organization.slug,
  155. projectSlug: transactionProject?.slug!,
  156. transaction: transaction.transaction as string,
  157. })}
  158. onClick={() => {
  159. trackAnalytics('profiling_views.go_to_transaction', {
  160. source: 'slowest_transaction_panel',
  161. organization,
  162. });
  163. }}
  164. >
  165. <TextTruncateOverflow>
  166. {transaction.transaction as string}
  167. </TextTruncateOverflow>
  168. </Link>
  169. </div>
  170. </Flex.Item>
  171. <PerformanceDuration
  172. milliseconds={formatter(transaction[key] as number)}
  173. abbreviation
  174. />
  175. <Button borderless size="zero" onClick={onOpen}>
  176. <IconChevron direction={open ? 'up' : 'down'} size="xs" />
  177. </Button>
  178. </Flex>
  179. <PanelItemBody
  180. style={{
  181. height: open ? 160 : 0,
  182. }}
  183. >
  184. {open && transactionProject && (
  185. <PanelItemFunctionsMiniGrid
  186. transaction={String(transaction.transaction)}
  187. organization={organization}
  188. project={transactionProject}
  189. />
  190. )}
  191. </PanelItemBody>
  192. </PanelItem>
  193. );
  194. }
  195. interface PanelItemFunctionsMiniGridProps {
  196. organization: Organization;
  197. project: Project;
  198. transaction: string;
  199. }
  200. function PanelItemFunctionsMiniGrid(props: PanelItemFunctionsMiniGridProps) {
  201. const {transaction, project, organization} = props;
  202. const {functionsQuery, functions} = useProfilingTransactionQuickSummary({
  203. transaction,
  204. project,
  205. referrer: 'api.profiling.landing-slowest-transaction-panel',
  206. skipLatestProfile: true,
  207. skipSlowestProfile: true,
  208. });
  209. if (functionsQuery.isPending) {
  210. return <FunctionsMiniGridLoading />;
  211. }
  212. if (!functions || (functions && functions.length === 0)) {
  213. return <FunctionsMiniGridEmptyState />;
  214. }
  215. return (
  216. <PanelItemBodyInner>
  217. <FunctionsMiniGrid
  218. functions={functions}
  219. organization={organization}
  220. project={project}
  221. onLinkClick={() =>
  222. trackAnalytics('profiling_views.go_to_flamegraph', {
  223. organization,
  224. source: 'slowest_transaction_panel',
  225. })
  226. }
  227. />
  228. </PanelItemBodyInner>
  229. );
  230. }
  231. const FlexPanel = styled(Panel)`
  232. display: flex;
  233. flex-direction: column;
  234. `;
  235. const PanelHeading = styled('span')`
  236. font-size: ${p => p.theme.text.cardTitle.fontSize};
  237. font-weight: ${p => p.theme.text.cardTitle.fontWeight};
  238. line-height: ${p => p.theme.text.cardTitle.lineHeight};
  239. `;
  240. const PanelSubheading = styled('span')`
  241. color: ${p => p.theme.subText};
  242. `;
  243. const PanelItem = styled('div')`
  244. padding: ${space(1)} ${space(1.5)};
  245. border-top: 1px solid ${p => p.theme.border};
  246. `;
  247. const PanelItemBody = styled('div')`
  248. transition: height 0.1s ease;
  249. width: 100%;
  250. overflow: hidden;
  251. `;
  252. // TODO: simple layout stuff like this should come from a primitive component and we should really stop this `styled` nonsense
  253. const PanelItemBodyInner = styled('div')`
  254. padding-top: ${space(1.5)};
  255. `;
  256. const EmptyStateDescription = styled('div')`
  257. font-size: ${p => p.theme.fontSizeMedium};
  258. `;