profilingTransactionHovercard.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import {Fragment, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Hovercard} from 'sentry/components/hovercard';
  4. import {Flex} from 'sentry/components/profiling/flex';
  5. import {
  6. FunctionsMiniGrid,
  7. FunctionsMiniGridEmptyState,
  8. FunctionsMiniGridLoading,
  9. } from 'sentry/components/profiling/functionsMiniGrid';
  10. import {TextTruncateOverflow} from 'sentry/components/profiling/textTruncateOverflow';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {Organization} from 'sentry/types/organization';
  14. import type {Project} from 'sentry/types/project';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import {getShortEventId} from 'sentry/utils/events';
  17. import {useProfilingTransactionQuickSummary} from 'sentry/utils/profiling/hooks/useProfilingTransactionQuickSummary';
  18. import {
  19. generateProfileFlamechartRouteWithQuery,
  20. generateProfileSummaryRouteWithQuery,
  21. } from 'sentry/utils/profiling/routes';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import {Button} from '../button';
  24. import Link from '../links/link';
  25. import LoadingIndicator from '../loadingIndicator';
  26. import PerformanceDuration from '../performanceDuration';
  27. interface ProfilingTransactionHovercardProps {
  28. organization: Organization;
  29. project: Project;
  30. transaction: string;
  31. }
  32. export function ProfilingTransactionHovercard(props: ProfilingTransactionHovercardProps) {
  33. const {project, transaction, organization} = props;
  34. const {query} = useLocation();
  35. const linkToSummary = generateProfileSummaryRouteWithQuery({
  36. query,
  37. orgSlug: organization.slug,
  38. projectSlug: project.slug,
  39. transaction,
  40. });
  41. const triggerLink = (
  42. <Link
  43. to={linkToSummary}
  44. onClick={() =>
  45. trackAnalytics('profiling_views.go_to_transaction', {
  46. organization,
  47. source: 'transaction_hovercard.trigger',
  48. })
  49. }
  50. >
  51. {transaction}
  52. </Link>
  53. );
  54. return (
  55. <StyledHovercard
  56. delay={250}
  57. header={
  58. <Flex justify="space-between" align="center">
  59. <TextTruncateOverflow>{transaction}</TextTruncateOverflow>
  60. <Button to={linkToSummary} size="xs">
  61. {t('View Profiles')}
  62. </Button>
  63. </Flex>
  64. }
  65. body={
  66. <ProfilingTransactionHovercardBody
  67. transaction={transaction}
  68. project={project}
  69. organization={organization}
  70. />
  71. }
  72. showUnderline
  73. >
  74. {triggerLink}
  75. </StyledHovercard>
  76. );
  77. }
  78. export function ProfilingTransactionHovercardBody({
  79. transaction,
  80. project,
  81. organization,
  82. }: ProfilingTransactionHovercardProps) {
  83. const {
  84. slowestProfile,
  85. slowestProfileQuery,
  86. slowestProfileDurationMultiplier,
  87. latestProfileQuery,
  88. latestProfile,
  89. functionsQuery,
  90. functions,
  91. } = useProfilingTransactionQuickSummary({
  92. transaction,
  93. project,
  94. referrer: 'api.profiling.transaction-hovercard',
  95. });
  96. const linkToFlamechartRoute = (
  97. profileId: string,
  98. query?: {frameName: string; framePackage: string}
  99. ) => {
  100. return generateProfileFlamechartRouteWithQuery({
  101. orgSlug: organization.slug,
  102. projectSlug: project.slug,
  103. profileId,
  104. query,
  105. });
  106. };
  107. useEffect(() => {
  108. trackAnalytics('profiling_ui_events.transaction_hovercard_view', {
  109. organization,
  110. });
  111. }, [organization]);
  112. return (
  113. <Flex gap={space(3)} column>
  114. <Flex justify="space-between">
  115. <ContextDetail
  116. title={t('Latest profile')}
  117. isLoading={latestProfileQuery.isLoading}
  118. >
  119. {latestProfile ? (
  120. <Link
  121. to={linkToFlamechartRoute(String(latestProfile['profile.id']))}
  122. onClick={() =>
  123. trackAnalytics('profiling_views.go_to_flamegraph', {
  124. organization,
  125. source: 'transaction_hovercard.latest_profile',
  126. })
  127. }
  128. >
  129. {getShortEventId(String(latestProfile!['profile.id']))}
  130. </Link>
  131. ) : (
  132. '-'
  133. )}
  134. </ContextDetail>
  135. <ContextDetail
  136. title={t('Slowest profile')}
  137. isLoading={slowestProfileQuery.isLoading}
  138. >
  139. {slowestProfile ? (
  140. <Flex gap={space(1)}>
  141. <PerformanceDuration
  142. milliseconds={
  143. slowestProfileDurationMultiplier *
  144. (slowestProfile['transaction.duration'] as number)
  145. }
  146. abbreviation
  147. />
  148. <Link
  149. to={linkToFlamechartRoute(String(slowestProfile['profile.id']))}
  150. onClick={() =>
  151. trackAnalytics('profiling_views.go_to_flamegraph', {
  152. organization,
  153. source: 'transaction_hovercard.slowest_profile',
  154. })
  155. }
  156. >
  157. ({getShortEventId(String(slowestProfile['profile.id']))})
  158. </Link>
  159. </Flex>
  160. ) : (
  161. '-'
  162. )}
  163. </ContextDetail>
  164. </Flex>
  165. <Flex column h={125}>
  166. <ProfilingTransactionHovercardFunctions
  167. isLoading={functionsQuery.isLoading}
  168. functions={functions ?? []}
  169. organization={organization}
  170. project={project}
  171. onLinkClick={() =>
  172. trackAnalytics('profiling_views.go_to_flamegraph', {
  173. organization,
  174. source: 'transaction_hovercard.suspect_function',
  175. })
  176. }
  177. />
  178. </Flex>
  179. </Flex>
  180. );
  181. }
  182. type ProfilingTransactionHovercardFunctionsProps = React.ComponentProps<
  183. typeof FunctionsMiniGrid
  184. > & {isLoading: boolean};
  185. function ProfilingTransactionHovercardFunctions(
  186. props: ProfilingTransactionHovercardFunctionsProps
  187. ) {
  188. if (props.isLoading) {
  189. return <FunctionsMiniGridLoading />;
  190. }
  191. if (!props.functions || props.functions?.length === 0) {
  192. return <FunctionsMiniGridEmptyState />;
  193. }
  194. return <FunctionsMiniGrid {...props} />;
  195. }
  196. interface ContextDetailProps {
  197. children: React.ReactNode;
  198. isLoading: boolean;
  199. title?: React.ReactNode;
  200. }
  201. function ContextDetail(props: ContextDetailProps) {
  202. const {title, children, isLoading} = props;
  203. return (
  204. <Flex column gap={space(1)}>
  205. {title && <UppercaseTitle>{title}</UppercaseTitle>}
  206. <Fragment>
  207. {isLoading ? (
  208. <Flex align="center" justify="center" h="1em">
  209. <LoadingIndicator mini />
  210. </Flex>
  211. ) : (
  212. children
  213. )}
  214. </Fragment>
  215. </Flex>
  216. );
  217. }
  218. const UppercaseTitle = styled('span')`
  219. text-transform: uppercase;
  220. font-size: ${p => p.theme.fontSizeExtraSmall};
  221. font-weight: ${p => p.theme.fontWeightBold};
  222. color: ${p => p.theme.subText};
  223. `;
  224. const StyledHovercard = styled(Hovercard)`
  225. width: 400px;
  226. `;