content.tsx 6.9 KB

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