content.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import {useEffect, useRef, useState} from 'react';
  2. import type {InjectedRouter} from 'react-router';
  3. import * as Sentry from '@sentry/react';
  4. import type {Location} from 'history';
  5. import isEqual from 'lodash/isEqual';
  6. import {loadOrganizationTags} from 'sentry/actionCreators/tags';
  7. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  8. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  9. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  10. import {t} from 'sentry/locale';
  11. import type {PageFilters, Project} from 'sentry/types';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import {browserHistory} from 'sentry/utils/browserHistory';
  14. import type {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  15. import {
  16. canUseMetricsData,
  17. METRIC_SEARCH_SETTING_PARAM,
  18. } from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  19. import {PerformanceEventViewProvider} from 'sentry/utils/performance/contexts/performanceEventViewContext';
  20. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  21. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  22. import useApi from 'sentry/utils/useApi';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import usePrevious from 'sentry/utils/usePrevious';
  25. import useProjects from 'sentry/utils/useProjects';
  26. import withPageFilters from 'sentry/utils/withPageFilters';
  27. import {getLandingDisplayFromParam} from './landing/utils';
  28. import {generatePerformanceEventView, getDefaultStatsPeriod} from './data';
  29. import {PerformanceLanding} from './landing';
  30. import {
  31. addRoutePerformanceContext,
  32. getSelectedProjectPlatforms,
  33. handleTrendsClick,
  34. } from './utils';
  35. type Props = {
  36. location: Location;
  37. router: InjectedRouter;
  38. selection: PageFilters;
  39. demoMode?: boolean;
  40. };
  41. type State = {
  42. error?: string;
  43. };
  44. function PerformanceContent({selection, location, demoMode, router}: Props) {
  45. const api = useApi();
  46. const organization = useOrganization();
  47. const {projects, reloadProjects} = useProjects();
  48. const mounted = useRef(false);
  49. const previousDateTime = usePrevious(selection.datetime);
  50. const [state, setState] = useState<State>({error: undefined});
  51. const withStaticFilters = canUseMetricsData(organization);
  52. const eventView = generatePerformanceEventView(
  53. location,
  54. projects,
  55. {
  56. withStaticFilters,
  57. },
  58. organization
  59. );
  60. function getOnboardingProject(): Project | undefined {
  61. // XXX used by getsentry to bypass onboarding for the upsell demo state.
  62. if (demoMode) {
  63. return undefined;
  64. }
  65. if (projects.length === 0) {
  66. return undefined;
  67. }
  68. // Current selection is 'my projects' or 'all projects'
  69. if (eventView.project.length === 0 || eventView.project[0] === ALL_ACCESS_PROJECTS) {
  70. const filtered = projects.filter(p => p.firstTransactionEvent === false);
  71. if (filtered.length === projects.length) {
  72. return filtered[0];
  73. }
  74. }
  75. // Any other subset of projects.
  76. const filtered = projects.filter(
  77. p =>
  78. eventView.project.includes(parseInt(p.id, 10)) &&
  79. p.firstTransactionEvent === false
  80. );
  81. if (filtered.length === eventView.project.length) {
  82. return filtered[0];
  83. }
  84. return undefined;
  85. }
  86. const onboardingProject = getOnboardingProject();
  87. useRouteAnalyticsEventNames(
  88. 'performance_views.overview.view',
  89. 'Performance Views: Transaction overview view'
  90. );
  91. useRouteAnalyticsParams({
  92. project_platforms: getSelectedProjectPlatforms(location, projects),
  93. show_onboarding: onboardingProject !== undefined,
  94. tab: getLandingDisplayFromParam(location)?.field,
  95. });
  96. // Refetch the project metadata if the selected project does not have performance data, because
  97. // we may have received performance data (and subsequently updated `Project.firstTransactionEvent`)
  98. // after the initial project fetch.
  99. useEffect(() => {
  100. if (onboardingProject) {
  101. reloadProjects();
  102. }
  103. // eslint-disable-next-line react-hooks/exhaustive-deps
  104. }, [onboardingProject]);
  105. useEffect(() => {
  106. if (!mounted.current) {
  107. loadOrganizationTags(api, organization.slug, selection);
  108. addRoutePerformanceContext(selection);
  109. mounted.current = true;
  110. return;
  111. }
  112. if (!isEqual(previousDateTime, selection.datetime)) {
  113. loadOrganizationTags(api, organization.slug, selection);
  114. addRoutePerformanceContext(selection);
  115. }
  116. }, [
  117. selection.datetime,
  118. previousDateTime,
  119. selection,
  120. api,
  121. organization,
  122. onboardingProject,
  123. location,
  124. projects,
  125. ]);
  126. function setError(newError?: string) {
  127. if (
  128. typeof newError === 'object' ||
  129. (Array.isArray(newError) && typeof newError[0] === 'object')
  130. ) {
  131. Sentry.withScope(scope => {
  132. scope.setExtra('error', newError);
  133. Sentry.captureException(new Error('setError failed with error type.'));
  134. });
  135. return;
  136. }
  137. setState({...state, error: newError});
  138. }
  139. function handleSearch(searchQuery: string, currentMEPState?: MEPState) {
  140. trackAnalytics('performance_views.overview.search', {organization});
  141. browserHistory.push({
  142. pathname: location.pathname,
  143. query: {
  144. ...location.query,
  145. cursor: undefined,
  146. query: String(searchQuery).trim() || undefined,
  147. [METRIC_SEARCH_SETTING_PARAM]: currentMEPState,
  148. isDefaultQuery: false,
  149. },
  150. });
  151. }
  152. return (
  153. <SentryDocumentTitle title={t('Performance')} orgSlug={organization.slug}>
  154. <PerformanceEventViewProvider value={{eventView}}>
  155. <PageFiltersContainer
  156. defaultSelection={{
  157. datetime: {
  158. start: null,
  159. end: null,
  160. utc: false,
  161. period: getDefaultStatsPeriod(organization),
  162. },
  163. }}
  164. >
  165. <PerformanceLanding
  166. router={router}
  167. eventView={eventView}
  168. setError={setError}
  169. handleSearch={handleSearch}
  170. handleTrendsClick={() =>
  171. handleTrendsClick({
  172. location,
  173. organization,
  174. projectPlatforms: getSelectedProjectPlatforms(location, projects),
  175. })
  176. }
  177. onboardingProject={onboardingProject}
  178. organization={organization}
  179. location={location}
  180. projects={projects}
  181. selection={selection}
  182. withStaticFilters={withStaticFilters}
  183. />
  184. </PageFiltersContainer>
  185. </PerformanceEventViewProvider>
  186. </SentryDocumentTitle>
  187. );
  188. }
  189. export default withPageFilters(PerformanceContent);