content.tsx 12 KB

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