profilingSlowestTransactionsPanel.tsx 8.8 KB

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