index.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  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 {Button} from 'sentry/components/button';
  6. import DatePageFilter from 'sentry/components/datePageFilter';
  7. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  8. import SearchBar from 'sentry/components/events/searchBar';
  9. import IdBadge from 'sentry/components/idBadge';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  12. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  13. import {
  14. ProfilingBreadcrumbs,
  15. ProfilingBreadcrumbsProps,
  16. } from 'sentry/components/profiling/profilingBreadcrumbs';
  17. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  18. import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  19. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import {PageFilters, Project} from 'sentry/types';
  23. import {defined, generateQueryWithTag} from 'sentry/utils';
  24. import {trackAnalytics} from 'sentry/utils/analytics';
  25. import EventView from 'sentry/utils/discover/eventView';
  26. import {formatTagKey, isAggregateField} from 'sentry/utils/discover/fields';
  27. import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
  28. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  29. import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
  30. import {decodeScalar} from 'sentry/utils/queryString';
  31. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  32. import useOrganization from 'sentry/utils/useOrganization';
  33. import withPageFilters from 'sentry/utils/withPageFilters';
  34. import Tags from 'sentry/views/discover/tags';
  35. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  36. import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
  37. import {ProfileSummaryContent} from './content';
  38. interface ProfileSummaryPageProps {
  39. location: Location;
  40. params: {
  41. projectId?: Project['slug'];
  42. };
  43. selection: PageFilters;
  44. }
  45. function ProfileSummaryPage(props: ProfileSummaryPageProps) {
  46. const organization = useOrganization();
  47. const project = useCurrentProjectFromRouteParam();
  48. const profilingUsingTransactions = organization.features.includes(
  49. 'profiling-using-transactions'
  50. );
  51. useEffect(() => {
  52. trackAnalytics('profiling_views.profile_summary', {
  53. organization,
  54. project_platform: project?.platform,
  55. project_id: project?.id,
  56. });
  57. // ignore currentProject so we don't block the analytics event
  58. // or fire more than once unnecessarily
  59. // eslint-disable-next-line react-hooks/exhaustive-deps
  60. }, [organization]);
  61. const transaction = decodeScalar(props.location.query.transaction);
  62. const rawQuery = useMemo(
  63. () => decodeScalar(props.location.query.query, ''),
  64. [props.location.query.query]
  65. );
  66. const query = useMemo(() => {
  67. const search = new MutableSearch(rawQuery);
  68. if (defined(transaction)) {
  69. search.setFilterValues('transaction', [transaction]);
  70. }
  71. // there are no aggregations happening on this page,
  72. // so remove any aggregate filters
  73. Object.keys(search.filters).forEach(field => {
  74. if (isAggregateField(field)) {
  75. search.removeFilter(field);
  76. }
  77. });
  78. return search.formatString();
  79. }, [rawQuery, transaction]);
  80. const profilesAggregateQuery = useProfileEvents<'count()'>({
  81. fields: ['count()'],
  82. sort: {key: 'count()', order: 'desc'},
  83. referrer: 'api.profiling.profile-summary-totals',
  84. query,
  85. enabled: profilingUsingTransactions,
  86. });
  87. const profilesCount = useMemo(() => {
  88. if (profilesAggregateQuery.status !== 'success') {
  89. return null;
  90. }
  91. return (profilesAggregateQuery.data?.data?.[0]?.['count()'] as number) || null;
  92. }, [profilesAggregateQuery]);
  93. const filtersQuery = useMemo(() => {
  94. // To avoid querying for the filters each time the query changes,
  95. // do not pass the user query to get the filters.
  96. const search = new MutableSearch('');
  97. if (defined(transaction)) {
  98. search.setFilterValues('transaction_name', [transaction]);
  99. }
  100. return search.formatString();
  101. }, [transaction]);
  102. const profileFilters = useProfileFilters({
  103. query: filtersQuery,
  104. selection: props.selection,
  105. disabled: profilingUsingTransactions,
  106. });
  107. const transactionSummaryTarget =
  108. project &&
  109. transaction &&
  110. transactionSummaryRouteWithQuery({
  111. orgSlug: organization.slug,
  112. transaction,
  113. projectID: project.id,
  114. query: {query},
  115. });
  116. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  117. (searchQuery: string) => {
  118. browserHistory.push({
  119. ...props.location,
  120. query: {
  121. ...props.location.query,
  122. query: searchQuery || undefined,
  123. cursor: undefined,
  124. },
  125. });
  126. },
  127. [props.location]
  128. );
  129. const breadcrumbTrails: ProfilingBreadcrumbsProps['trails'] = useMemo(() => {
  130. return [
  131. {
  132. type: 'landing',
  133. payload: {
  134. query: props.location.query,
  135. },
  136. },
  137. {
  138. type: 'profile summary',
  139. payload: {
  140. projectSlug: project?.slug ?? '',
  141. query: props.location.query,
  142. transaction: transaction ?? '',
  143. },
  144. },
  145. ];
  146. }, [props.location.query, project?.slug, transaction]);
  147. const eventView = useMemo(() => {
  148. const _eventView = EventView.fromNewQueryWithLocation(
  149. {
  150. id: undefined,
  151. version: 2,
  152. name: transaction || '',
  153. fields: [],
  154. query,
  155. projects: project ? [parseInt(project.id, 10)] : [],
  156. },
  157. props.location
  158. );
  159. _eventView.additionalConditions.setFilterValues('has', ['profile.id']);
  160. return _eventView;
  161. }, [props.location, project, query, transaction]);
  162. function generateTagUrl(key: string, value: string) {
  163. return {
  164. ...props.location,
  165. query: generateQueryWithTag(props.location.query, {key: formatTagKey(key), value}),
  166. };
  167. }
  168. return (
  169. <SentryDocumentTitle
  170. title={t('Profiling \u2014 Profile Summary')}
  171. orgSlug={organization.slug}
  172. >
  173. <PageFiltersContainer
  174. shouldForceProject={defined(project)}
  175. forceProject={project}
  176. specificProjectSlugs={defined(project) ? [project.slug] : []}
  177. defaultSelection={
  178. profilingUsingTransactions
  179. ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
  180. : undefined
  181. }
  182. >
  183. <Layout.Page>
  184. {project && transaction && (
  185. <Fragment>
  186. <Layout.Header>
  187. <Layout.HeaderContent>
  188. <ProfilingBreadcrumbs
  189. organization={organization}
  190. trails={breadcrumbTrails}
  191. />
  192. <Layout.Title>
  193. {project ? (
  194. <IdBadge
  195. project={project}
  196. avatarSize={28}
  197. hideName
  198. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  199. />
  200. ) : null}
  201. {transaction}
  202. </Layout.Title>
  203. </Layout.HeaderContent>
  204. {transactionSummaryTarget && (
  205. <Layout.HeaderActions>
  206. <Button to={transactionSummaryTarget} size="sm">
  207. {t('View Transaction Summary')}
  208. </Button>
  209. </Layout.HeaderActions>
  210. )}
  211. </Layout.Header>
  212. <Layout.Body>
  213. <Layout.Main fullWidth={!profilingUsingTransactions}>
  214. <ActionBar>
  215. <PageFilterBar condensed>
  216. <EnvironmentPageFilter />
  217. <DatePageFilter alignDropdown="left" />
  218. </PageFilterBar>
  219. {profilingUsingTransactions ? (
  220. <SearchBar
  221. searchSource="profile_summary"
  222. organization={organization}
  223. projectIds={eventView.project}
  224. query={rawQuery}
  225. onSearch={handleSearch}
  226. maxQueryLength={MAX_QUERY_LENGTH}
  227. />
  228. ) : (
  229. <SmartSearchBar
  230. organization={organization}
  231. hasRecentSearches
  232. searchSource="profile_summary"
  233. supportedTags={profileFilters}
  234. query={rawQuery}
  235. onSearch={handleSearch}
  236. maxQueryLength={MAX_QUERY_LENGTH}
  237. />
  238. )}
  239. </ActionBar>
  240. <ProfileSummaryContent
  241. location={props.location}
  242. project={project}
  243. selection={props.selection}
  244. transaction={transaction}
  245. query={query}
  246. />
  247. </Layout.Main>
  248. {profilingUsingTransactions && (
  249. <Layout.Side>
  250. <Tags
  251. generateUrl={generateTagUrl}
  252. totalValues={profilesCount}
  253. eventView={eventView}
  254. organization={organization}
  255. location={props.location}
  256. />
  257. </Layout.Side>
  258. )}
  259. </Layout.Body>
  260. </Fragment>
  261. )}
  262. </Layout.Page>
  263. </PageFiltersContainer>
  264. </SentryDocumentTitle>
  265. );
  266. }
  267. const ActionBar = styled('div')`
  268. display: grid;
  269. gap: ${space(2)};
  270. grid-template-columns: min-content auto;
  271. margin-bottom: ${space(2)};
  272. `;
  273. export default withPageFilters(ProfileSummaryPage);