content.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 ButtonBar from 'sentry/components/buttonBar';
  8. import DatePageFilter from 'sentry/components/datePageFilter';
  9. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  10. import SearchBar from 'sentry/components/events/searchBar';
  11. import FeatureBadge from 'sentry/components/featureBadge';
  12. import * as Layout from 'sentry/components/layouts/thirds';
  13. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  14. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  15. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  16. import Pagination from 'sentry/components/pagination';
  17. import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
  18. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  19. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  20. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  21. import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  22. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  23. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  24. import {t} from 'sentry/locale';
  25. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  26. import {space} from 'sentry/styles/space';
  27. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  28. import EventView from 'sentry/utils/discover/eventView';
  29. import {
  30. formatError,
  31. formatSort,
  32. useProfileEvents,
  33. } from 'sentry/utils/profiling/hooks/useProfileEvents';
  34. import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
  35. import {decodeScalar} from 'sentry/utils/queryString';
  36. import useOrganization from 'sentry/utils/useOrganization';
  37. import usePageFilters from 'sentry/utils/usePageFilters';
  38. import useProjects from 'sentry/utils/useProjects';
  39. import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
  40. import {ProfileCharts} from './landing/profileCharts';
  41. import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
  42. import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
  43. interface ProfilingContentProps {
  44. location: Location;
  45. }
  46. function ProfilingContent({location}: ProfilingContentProps) {
  47. const organization = useOrganization();
  48. const {selection} = usePageFilters();
  49. const cursor = decodeScalar(location.query.cursor);
  50. const query = decodeScalar(location.query.query, '');
  51. const profilingUsingTransactions = organization.features.includes(
  52. 'profiling-using-transactions'
  53. );
  54. const fields = profilingUsingTransactions ? ALL_FIELDS : BASE_FIELDS;
  55. const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
  56. key: 'p99()',
  57. order: 'desc',
  58. });
  59. const profileFilters = useProfileFilters({
  60. query: '',
  61. selection,
  62. disabled: profilingUsingTransactions,
  63. });
  64. const {projects} = useProjects();
  65. const transactions = useProfileEvents<FieldType>({
  66. cursor,
  67. fields,
  68. query,
  69. sort,
  70. referrer: 'api.profiling.landing-table',
  71. });
  72. const transactionsError =
  73. transactions.status === 'error' ? formatError(transactions.error) : null;
  74. useEffect(() => {
  75. trackAdvancedAnalyticsEvent('profiling_views.landing', {
  76. organization,
  77. });
  78. }, [organization]);
  79. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  80. (searchQuery: string) => {
  81. browserHistory.push({
  82. ...location,
  83. query: {
  84. ...location.query,
  85. cursor: undefined,
  86. query: searchQuery || undefined,
  87. },
  88. });
  89. },
  90. [location]
  91. );
  92. // Open the modal on demand
  93. const onSetupProfilingClick = useCallback(() => {
  94. trackAdvancedAnalyticsEvent('profiling_views.onboarding', {
  95. organization,
  96. });
  97. SidebarPanelStore.activatePanel(SidebarPanelKey.ProfilingOnboarding);
  98. }, [organization]);
  99. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  100. // if it's My Projects or All projects, only show onboarding if we can't
  101. // find any projects with profiles
  102. if (
  103. selection.projects.length === 0 ||
  104. selection.projects[0] === ALL_ACCESS_PROJECTS
  105. ) {
  106. return projects.every(project => !project.hasProfiles);
  107. }
  108. // otherwise, only show onboarding if we can't find any projects with profiles
  109. // from those that were selected
  110. const projectsWithProfiles = new Set(
  111. projects.filter(project => project.hasProfiles).map(project => project.id)
  112. );
  113. return selection.projects.every(
  114. project => !projectsWithProfiles.has(String(project))
  115. );
  116. }, [selection.projects, projects]);
  117. const eventView = useMemo(() => {
  118. const _eventView = EventView.fromNewQueryWithLocation(
  119. {
  120. id: undefined,
  121. version: 2,
  122. name: t('Profiling'),
  123. fields: [],
  124. query,
  125. projects: selection.projects,
  126. },
  127. location
  128. );
  129. _eventView.additionalConditions.setFilterValues('has', ['profile.id']);
  130. return _eventView;
  131. }, [location, query, selection.projects]);
  132. return (
  133. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  134. <PageFiltersContainer
  135. defaultSelection={
  136. profilingUsingTransactions
  137. ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
  138. : undefined
  139. }
  140. >
  141. <Layout.Page>
  142. <Layout.Header>
  143. <Layout.HeaderContent>
  144. <Layout.Title>
  145. {t('Profiling')}
  146. <PageHeadingQuestionTooltip
  147. docsUrl="https://docs.sentry.io/product/profiling/"
  148. title={t(
  149. 'A view of how your application performs in a variety of environments, based off of the performance profiles collected from real user devices in production.'
  150. )}
  151. />
  152. <FeatureBadge type="beta" />
  153. </Layout.Title>
  154. </Layout.HeaderContent>
  155. <Layout.HeaderActions>
  156. <ButtonBar gap={1}>
  157. <Button size="sm" onClick={onSetupProfilingClick}>
  158. {t('Set Up Profiling')}
  159. </Button>
  160. <Button
  161. size="sm"
  162. priority="primary"
  163. href="https://discord.gg/zrMjKA4Vnz"
  164. external
  165. onClick={() => {
  166. trackAdvancedAnalyticsEvent('profiling_views.visit_discord_channel', {
  167. organization,
  168. });
  169. }}
  170. >
  171. {t('Join Discord')}
  172. </Button>
  173. </ButtonBar>
  174. </Layout.HeaderActions>
  175. </Layout.Header>
  176. <Layout.Body>
  177. <Layout.Main fullWidth>
  178. {transactionsError && (
  179. <Alert type="error" showIcon>
  180. {transactionsError}
  181. </Alert>
  182. )}
  183. <ActionBar>
  184. <PageFilterBar condensed>
  185. <ProjectPageFilter />
  186. <EnvironmentPageFilter />
  187. <DatePageFilter alignDropdown="left" />
  188. </PageFilterBar>
  189. {profilingUsingTransactions ? (
  190. <SearchBar
  191. searchSource="profile_summary"
  192. organization={organization}
  193. projectIds={eventView.project}
  194. query={query}
  195. onSearch={handleSearch}
  196. maxQueryLength={MAX_QUERY_LENGTH}
  197. />
  198. ) : (
  199. <SmartSearchBar
  200. organization={organization}
  201. hasRecentSearches
  202. searchSource="profile_landing"
  203. supportedTags={profileFilters}
  204. query={query}
  205. onSearch={handleSearch}
  206. maxQueryLength={MAX_QUERY_LENGTH}
  207. />
  208. )}
  209. </ActionBar>
  210. {shouldShowProfilingOnboardingPanel ? (
  211. <ProfilingOnboardingPanel>
  212. <Button onClick={onSetupProfilingClick} priority="primary">
  213. {t('Set Up Profiling')}
  214. </Button>
  215. <Button href="https://docs.sentry.io/product/profiling/" external>
  216. {t('Read Docs')}
  217. </Button>
  218. </ProfilingOnboardingPanel>
  219. ) : (
  220. <Fragment>
  221. <PanelsGrid>
  222. <ProfilingSlowestTransactionsPanel />
  223. <ProfileCharts query={query} selection={selection} hideCount />
  224. </PanelsGrid>
  225. <ProfileEventsTable
  226. columns={fields.slice()}
  227. data={transactions.status === 'success' ? transactions.data[0] : null}
  228. error={
  229. transactions.status === 'error'
  230. ? t('Unable to load profiles')
  231. : null
  232. }
  233. isLoading={transactions.status === 'loading'}
  234. sort={sort}
  235. sortableColumns={new Set(fields)}
  236. />
  237. <Pagination
  238. pageLinks={
  239. transactions.status === 'success'
  240. ? transactions.data?.[2]?.getResponseHeader('Link') ?? null
  241. : null
  242. }
  243. />
  244. </Fragment>
  245. )}
  246. </Layout.Main>
  247. </Layout.Body>
  248. </Layout.Page>
  249. </PageFiltersContainer>
  250. </SentryDocumentTitle>
  251. );
  252. }
  253. const BASE_FIELDS = [
  254. 'transaction',
  255. 'project.id',
  256. 'last_seen()',
  257. 'p75()',
  258. 'p95()',
  259. 'p99()',
  260. 'count()',
  261. ] as const;
  262. // user misery is only available with the profiling-using-transactions feature
  263. const ALL_FIELDS = [...BASE_FIELDS, 'user_misery()'] as const;
  264. type FieldType = (typeof ALL_FIELDS)[number];
  265. const ActionBar = styled('div')`
  266. display: grid;
  267. gap: ${space(2)};
  268. grid-template-columns: min-content auto;
  269. margin-bottom: ${space(2)};
  270. `;
  271. // TODO: another simple primitive that can easily be <Grid columns={2} />
  272. const PanelsGrid = styled('div')`
  273. display: grid;
  274. grid-template-columns: minmax(0, 1fr) 1fr;
  275. gap: ${space(2)};
  276. @media (max-width: ${p => p.theme.breakpoints.small}) {
  277. grid-template-columns: minmax(0, 1fr);
  278. }
  279. `;
  280. export default ProfilingContent;