index.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import SearchBar from 'sentry/components/events/searchBar';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  6. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  7. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  8. import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
  9. import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
  10. import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  11. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Organization} from 'sentry/types/organization';
  15. import {browserHistory} from 'sentry/utils/browserHistory';
  16. import EventView from 'sentry/utils/discover/eventView';
  17. import {isAggregateField} from 'sentry/utils/discover/fields';
  18. import type {ProfilingFieldType} from 'sentry/utils/profiling/hooks/useProfileEvents';
  19. import {
  20. getProfilesTableFields,
  21. useProfileEvents,
  22. } from 'sentry/utils/profiling/hooks/useProfileEvents';
  23. import {formatSort} from 'sentry/utils/profiling/hooks/utils';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  26. import {useLocation} from 'sentry/utils/useLocation';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import useProjects from 'sentry/utils/useProjects';
  29. import Tab from 'sentry/views/performance/transactionSummary/tabs';
  30. import PageLayout, {redirectToPerformanceHomepage} from '../pageLayout';
  31. import {TransactionProfilesContent} from './content';
  32. interface ProfilesProps {
  33. organization: Organization;
  34. transaction: string;
  35. }
  36. function ProfilesLegacy({organization, transaction}: ProfilesProps) {
  37. const location = useLocation();
  38. const {projects} = useProjects();
  39. const profilesCursor = useMemo(
  40. () => decodeScalar(location.query.cursor),
  41. [location.query.cursor]
  42. );
  43. const project = projects.find(p => p.id === location.query.project);
  44. const fields = getProfilesTableFields(project?.platform);
  45. const sortableFields = useMemo(() => new Set(fields), [fields]);
  46. const sort = formatSort<ProfilingFieldType>(decodeScalar(location.query.sort), fields, {
  47. key: 'timestamp',
  48. order: 'desc',
  49. });
  50. const rawQuery = useMemo(
  51. () => decodeScalar(location.query.query, ''),
  52. [location.query.query]
  53. );
  54. const query = useMemo(() => {
  55. const conditions = new MutableSearch(rawQuery);
  56. conditions.setFilterValues('event.type', ['transaction']);
  57. conditions.setFilterValues('transaction', [transaction]);
  58. Object.keys(conditions.filters).forEach(field => {
  59. if (isAggregateField(field)) {
  60. conditions.removeFilter(field);
  61. }
  62. });
  63. return conditions.formatString();
  64. }, [transaction, rawQuery]);
  65. const profiles = useProfileEvents<ProfilingFieldType>({
  66. cursor: profilesCursor,
  67. fields,
  68. query,
  69. sort,
  70. limit: 30,
  71. referrer: 'api.profiling.transactions-profiles-table',
  72. });
  73. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  74. (searchQuery: string) => {
  75. browserHistory.push({
  76. ...location,
  77. query: {
  78. ...location.query,
  79. cursor: undefined,
  80. query: searchQuery || undefined,
  81. },
  82. });
  83. },
  84. [location]
  85. );
  86. const projectIds = useMemo(
  87. () => (project ? [parseInt(project?.id, 10)] : undefined),
  88. [project]
  89. );
  90. return (
  91. <PageLayout
  92. location={location}
  93. organization={organization}
  94. projects={projects}
  95. tab={Tab.PROFILING}
  96. generateEventView={() => EventView.fromLocation(location)}
  97. getDocumentTitle={() => t(`Profile: %s`, transaction)}
  98. childComponent={() => {
  99. return (
  100. <Layout.Main fullWidth>
  101. <FilterActions>
  102. <PageFilterBar condensed>
  103. <EnvironmentPageFilter />
  104. <DatePageFilter />
  105. </PageFilterBar>
  106. {organization.features.includes('search-query-builder-performance') ? (
  107. <TransactionSearchQueryBuilder
  108. projects={projectIds}
  109. initialQuery={rawQuery}
  110. onSearch={handleSearch}
  111. searchSource="transaction_profiles"
  112. />
  113. ) : (
  114. <SearchBar
  115. searchSource="transaction_profiles"
  116. organization={organization}
  117. projectIds={projects.map(p => parseInt(p.id, 10))}
  118. query={rawQuery}
  119. onSearch={handleSearch}
  120. maxQueryLength={MAX_QUERY_LENGTH}
  121. />
  122. )}
  123. </FilterActions>
  124. <ProfileEventsTable
  125. columns={fields}
  126. data={profiles.status === 'success' ? profiles.data : null}
  127. error={profiles.status === 'error' ? t('Unable to load profiles') : null}
  128. isLoading={profiles.status === 'pending'}
  129. sort={sort}
  130. sortableColumns={sortableFields}
  131. />
  132. </Layout.Main>
  133. );
  134. }}
  135. />
  136. );
  137. }
  138. function Profiles({organization, transaction}: ProfilesProps) {
  139. const location = useLocation();
  140. const {projects} = useProjects();
  141. const project = projects.find(p => p.id === location.query.project);
  142. const rawQuery = useMemo(
  143. () => decodeScalar(location.query.query, ''),
  144. [location.query.query]
  145. );
  146. const query = useMemo(() => {
  147. const conditions = new MutableSearch(rawQuery);
  148. conditions.setFilterValues('event.type', ['transaction']);
  149. conditions.setFilterValues('transaction', [transaction]);
  150. Object.keys(conditions.filters).forEach(field => {
  151. if (isAggregateField(field)) {
  152. conditions.removeFilter(field);
  153. }
  154. });
  155. return conditions.formatString();
  156. }, [transaction, rawQuery]);
  157. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  158. (searchQuery: string) => {
  159. browserHistory.push({
  160. ...location,
  161. query: {
  162. ...location.query,
  163. cursor: undefined,
  164. query: searchQuery || undefined,
  165. },
  166. });
  167. },
  168. [location]
  169. );
  170. const projectIds = useMemo(
  171. () => (project ? [parseInt(project?.id, 10)] : undefined),
  172. [project]
  173. );
  174. return (
  175. <PageLayout
  176. location={location}
  177. organization={organization}
  178. projects={projects}
  179. tab={Tab.PROFILING}
  180. generateEventView={() => EventView.fromLocation(location)}
  181. getDocumentTitle={() => t(`Profile: %s`, transaction)}
  182. fillSpace
  183. childComponent={() => {
  184. return (
  185. <StyledMain fullWidth>
  186. <FilterActions>
  187. <PageFilterBar condensed>
  188. <EnvironmentPageFilter />
  189. <DatePageFilter />
  190. </PageFilterBar>
  191. {organization.features.includes('search-query-builder-performance') ? (
  192. <TransactionSearchQueryBuilder
  193. projects={projectIds}
  194. initialQuery={rawQuery}
  195. onSearch={handleSearch}
  196. searchSource="transaction_profiles"
  197. />
  198. ) : (
  199. <SearchBar
  200. searchSource="transaction_profiles"
  201. organization={organization}
  202. projectIds={projectIds}
  203. query={rawQuery}
  204. onSearch={handleSearch}
  205. maxQueryLength={MAX_QUERY_LENGTH}
  206. />
  207. )}
  208. </FilterActions>
  209. <TransactionProfilesContent query={query} transaction={transaction} />
  210. </StyledMain>
  211. );
  212. }}
  213. />
  214. );
  215. }
  216. const FilterActions = styled('div')`
  217. margin-bottom: ${space(2)};
  218. gap: ${space(2)};
  219. display: grid;
  220. grid-template-columns: min-content 1fr;
  221. `;
  222. const StyledMain = styled(Layout.Main)`
  223. display: flex;
  224. flex-direction: column;
  225. flex: 1;
  226. `;
  227. function ProfilesIndex() {
  228. const organization = useOrganization();
  229. const location = useLocation();
  230. const transaction = decodeScalar(location.query.transaction);
  231. if (!transaction) {
  232. redirectToPerformanceHomepage(organization, location);
  233. return null;
  234. }
  235. if (organization.features.includes('continuous-profiling-compat')) {
  236. return <Profiles organization={organization} transaction={transaction} />;
  237. }
  238. return <ProfilesLegacy organization={organization} transaction={transaction} />;
  239. }
  240. export default ProfilesIndex;