content.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import {Alert} from 'sentry/components/alert';
  6. import {Button} from 'sentry/components/button';
  7. import DatePageFilter from 'sentry/components/datePageFilter';
  8. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  9. import SearchBar from 'sentry/components/events/searchBar';
  10. import FeatureBadge from 'sentry/components/featureBadge';
  11. import * as Layout from 'sentry/components/layouts/thirds';
  12. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  13. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  14. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  15. import Pagination from 'sentry/components/pagination';
  16. import {
  17. ProfilingAM1OrMMXUpgrade,
  18. ProfilingBetaAlertBanner,
  19. ProfilingUpgradeButton,
  20. } from 'sentry/components/profiling/billing/alerts';
  21. import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
  22. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  23. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  24. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  25. import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  26. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  27. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  28. import {t} from 'sentry/locale';
  29. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  30. import {space} from 'sentry/styles/space';
  31. import {Organization} from 'sentry/types';
  32. import {trackAnalytics} from 'sentry/utils/analytics';
  33. import EventView from 'sentry/utils/discover/eventView';
  34. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  35. import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
  36. import {formatError, formatSort} from 'sentry/utils/profiling/hooks/utils';
  37. import {decodeScalar} from 'sentry/utils/queryString';
  38. import useOrganization from 'sentry/utils/useOrganization';
  39. import usePageFilters from 'sentry/utils/usePageFilters';
  40. import useProjects from 'sentry/utils/useProjects';
  41. import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
  42. import {ProfileCharts} from './landing/profileCharts';
  43. import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
  44. import {SlowestFunctionsWidget} from './landing/slowestFunctionsWidget';
  45. import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
  46. interface ProfilingContentProps {
  47. location: Location;
  48. }
  49. function ProfilingContent({location}: ProfilingContentProps) {
  50. const organization = useOrganization();
  51. const {selection} = usePageFilters();
  52. const cursor = decodeScalar(location.query.cursor);
  53. const query = decodeScalar(location.query.query, '');
  54. const profilingUsingTransactions = organization.features.includes(
  55. 'profiling-using-transactions'
  56. );
  57. const fields = profilingUsingTransactions ? ALL_FIELDS : BASE_FIELDS;
  58. const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
  59. key: 'p95()',
  60. order: 'desc',
  61. });
  62. const profileFilters = useProfileFilters({
  63. query: '',
  64. selection,
  65. disabled: profilingUsingTransactions,
  66. });
  67. const {projects} = useProjects();
  68. const transactions = useProfileEvents<FieldType>({
  69. cursor,
  70. fields,
  71. query,
  72. sort,
  73. referrer: 'api.profiling.landing-table',
  74. });
  75. const transactionsError =
  76. transactions.status === 'error' ? formatError(transactions.error) : null;
  77. useEffect(() => {
  78. trackAnalytics('profiling_views.landing', {
  79. organization,
  80. });
  81. }, [organization]);
  82. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  83. (searchQuery: string) => {
  84. browserHistory.push({
  85. ...location,
  86. query: {
  87. ...location.query,
  88. cursor: undefined,
  89. query: searchQuery || undefined,
  90. },
  91. });
  92. },
  93. [location]
  94. );
  95. // Open the modal on demand
  96. const onSetupProfilingClick = useCallback(() => {
  97. trackAnalytics('profiling_views.onboarding', {
  98. organization,
  99. });
  100. SidebarPanelStore.activatePanel(SidebarPanelKey.ProfilingOnboarding);
  101. }, [organization]);
  102. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  103. // if it's My Projects or All projects, only show onboarding if we can't
  104. // find any projects with profiles
  105. if (
  106. selection.projects.length === 0 ||
  107. selection.projects[0] === ALL_ACCESS_PROJECTS
  108. ) {
  109. return projects.every(project => !project.hasProfiles);
  110. }
  111. // otherwise, only show onboarding if we can't find any projects with profiles
  112. // from those that were selected
  113. const projectsWithProfiles = new Set(
  114. projects.filter(project => project.hasProfiles).map(project => project.id)
  115. );
  116. return selection.projects.every(
  117. project => !projectsWithProfiles.has(String(project))
  118. );
  119. }, [selection.projects, projects]);
  120. const eventView = useMemo(() => {
  121. const _eventView = EventView.fromNewQueryWithLocation(
  122. {
  123. id: undefined,
  124. version: 2,
  125. name: t('Profiling'),
  126. fields: [],
  127. query,
  128. projects: selection.projects,
  129. },
  130. location
  131. );
  132. _eventView.additionalConditions.setFilterValues('has', ['profile.id']);
  133. return _eventView;
  134. }, [location, query, selection.projects]);
  135. const isProfilingGA = organization.features.includes('profiling-ga');
  136. return (
  137. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  138. <PageFiltersContainer
  139. defaultSelection={
  140. profilingUsingTransactions
  141. ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
  142. : undefined
  143. }
  144. >
  145. <Layout.Page>
  146. {isProfilingGA ? (
  147. <ProfilingBetaAlertBanner organization={organization} />
  148. ) : (
  149. <ProfilingBetaEndAlertBanner organization={organization} />
  150. )}
  151. <Layout.Header>
  152. <Layout.HeaderContent>
  153. <Layout.Title>
  154. {t('Profiling')}
  155. <PageHeadingQuestionTooltip
  156. docsUrl="https://docs.sentry.io/product/profiling/"
  157. title={t(
  158. 'Profiling collects detailed information in production about the functions executing in your application and how long they take to run, giving you code-level visibility into your hot paths.'
  159. )}
  160. />
  161. {isProfilingGA ? (
  162. <FeatureBadge type="new" />
  163. ) : (
  164. <FeatureBadge type="beta" />
  165. )}
  166. </Layout.Title>
  167. </Layout.HeaderContent>
  168. </Layout.Header>
  169. <Layout.Body>
  170. <Layout.Main fullWidth>
  171. {transactionsError && (
  172. <Alert type="error" showIcon>
  173. {transactionsError}
  174. </Alert>
  175. )}
  176. <ActionBar>
  177. <PageFilterBar condensed>
  178. <ProjectPageFilter />
  179. <EnvironmentPageFilter />
  180. <DatePageFilter alignDropdown="left" />
  181. </PageFilterBar>
  182. {profilingUsingTransactions ? (
  183. <SearchBar
  184. searchSource="profile_summary"
  185. organization={organization}
  186. projectIds={eventView.project}
  187. query={query}
  188. onSearch={handleSearch}
  189. maxQueryLength={MAX_QUERY_LENGTH}
  190. />
  191. ) : (
  192. <SmartSearchBar
  193. organization={organization}
  194. hasRecentSearches
  195. searchSource="profile_landing"
  196. supportedTags={profileFilters}
  197. query={query}
  198. onSearch={handleSearch}
  199. maxQueryLength={MAX_QUERY_LENGTH}
  200. />
  201. )}
  202. </ActionBar>
  203. {shouldShowProfilingOnboardingPanel ? (
  204. isProfilingGA ? (
  205. // If user is on m2, show default
  206. <ProfilingOnboardingPanel
  207. content={
  208. <ProfilingAM1OrMMXUpgrade
  209. organization={organization}
  210. fallback={
  211. <Fragment>
  212. <h3>{t('Function level insights')}</h3>
  213. <p>
  214. {t(
  215. 'Discover slow-to-execute or resource intensive functions within your application'
  216. )}
  217. </p>
  218. </Fragment>
  219. }
  220. />
  221. }
  222. >
  223. <ProfilingUpgradeButton
  224. organization={organization}
  225. priority="primary"
  226. fallback={
  227. <Button onClick={onSetupProfilingClick} priority="primary">
  228. {t('Set Up Profiling')}
  229. </Button>
  230. }
  231. >
  232. {t('Update plan')}
  233. </ProfilingUpgradeButton>
  234. <Button href="https://docs.sentry.io/product/profiling/" external>
  235. {t('Read Docs')}
  236. </Button>
  237. </ProfilingOnboardingPanel>
  238. ) : (
  239. // show previous state
  240. <ProfilingOnboardingPanel>
  241. <Button onClick={onSetupProfilingClick} priority="primary">
  242. {t('Set Up Profiling')}
  243. </Button>
  244. <Button href="https://docs.sentry.io/product/profiling/" external>
  245. {t('Read Docs')}
  246. </Button>
  247. </ProfilingOnboardingPanel>
  248. )
  249. ) : (
  250. <Fragment>
  251. <PanelsGrid>
  252. {organization.features.includes(
  253. 'profiling-global-suspect-functions'
  254. ) ? (
  255. <SlowestFunctionsWidget />
  256. ) : (
  257. <ProfilingSlowestTransactionsPanel />
  258. )}
  259. <ProfileCharts
  260. referrer="api.profiling.landing-chart"
  261. query={query}
  262. selection={selection}
  263. hideCount
  264. />
  265. </PanelsGrid>
  266. <ProfileEventsTable
  267. columns={fields.slice()}
  268. data={transactions.status === 'success' ? transactions.data : null}
  269. error={
  270. transactions.status === 'error'
  271. ? t('Unable to load profiles')
  272. : null
  273. }
  274. isLoading={transactions.status === 'loading'}
  275. sort={sort}
  276. sortableColumns={new Set(fields)}
  277. />
  278. <Pagination
  279. pageLinks={
  280. transactions.status === 'success'
  281. ? transactions.getResponseHeader?.('Link') ?? null
  282. : null
  283. }
  284. />
  285. </Fragment>
  286. )}
  287. </Layout.Main>
  288. </Layout.Body>
  289. </Layout.Page>
  290. </PageFiltersContainer>
  291. </SentryDocumentTitle>
  292. );
  293. }
  294. function ProfilingBetaEndAlertBanner({organization}: {organization: Organization}) {
  295. // beta users will continue to have access
  296. if (organization.features.includes('profiling-beta')) {
  297. return null;
  298. }
  299. return (
  300. <StyledAlert system type="info">
  301. {t(
  302. "The beta program for Profiling is now closed, but Profiling will become generally available soon. If you weren't part of the beta program, any Profiles sent during this time won't appear in your dashboard. Check out the What’s New tab for updates."
  303. )}
  304. </StyledAlert>
  305. );
  306. }
  307. const BASE_FIELDS = [
  308. 'transaction',
  309. 'project.id',
  310. 'last_seen()',
  311. 'p75()',
  312. 'p95()',
  313. 'p99()',
  314. 'count()',
  315. ] as const;
  316. // user misery is only available with the profiling-using-transactions feature
  317. const ALL_FIELDS = [...BASE_FIELDS, 'user_misery()'] as const;
  318. type FieldType = (typeof ALL_FIELDS)[number];
  319. const ActionBar = styled('div')`
  320. display: grid;
  321. gap: ${space(2)};
  322. grid-template-columns: min-content auto;
  323. margin-bottom: ${space(2)};
  324. `;
  325. // TODO: another simple primitive that can easily be <Grid columns={2} />
  326. const PanelsGrid = styled('div')`
  327. display: grid;
  328. grid-template-columns: minmax(0, 1fr) 1fr;
  329. gap: ${space(2)};
  330. @media (max-width: ${p => p.theme.breakpoints.small}) {
  331. grid-template-columns: minmax(0, 1fr);
  332. }
  333. `;
  334. const StyledAlert = styled(Alert)`
  335. margin: 0;
  336. `;
  337. export default ProfilingContent;