index.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import React from 'react';
  2. import {browserHistory, WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import Feature from 'app/components/acl/feature';
  6. import Alert from 'app/components/alert';
  7. import LightWeightNoProjectMessage from 'app/components/lightWeightNoProjectMessage';
  8. import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
  9. import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
  10. import {t} from 'app/locale';
  11. import {PageContent} from 'app/styles/organization';
  12. import {GlobalSelection, Organization, Project} from 'app/types';
  13. import EventView from 'app/utils/discover/eventView';
  14. import {isAggregateField, WebVital} from 'app/utils/discover/fields';
  15. import {WEB_VITAL_DETAILS} from 'app/utils/performance/vitals/constants';
  16. import {decodeScalar} from 'app/utils/queryString';
  17. import {stringifyQueryObject, tokenizeSearch} from 'app/utils/tokenizeSearch';
  18. import withGlobalSelection from 'app/utils/withGlobalSelection';
  19. import withOrganization from 'app/utils/withOrganization';
  20. import withProjects from 'app/utils/withProjects';
  21. import {getTransactionName} from '../utils';
  22. import {PERCENTILE, VITAL_GROUPS} from './constants';
  23. import RumContent from './content';
  24. type Props = {
  25. location: Location;
  26. organization: Organization;
  27. projects: Project[];
  28. selection: GlobalSelection;
  29. } & Pick<WithRouterProps, 'router'>;
  30. type State = {
  31. eventView: EventView | undefined;
  32. };
  33. class TransactionVitals extends React.Component<Props> {
  34. state: State = {
  35. eventView: generateRumEventView(
  36. this.props.location,
  37. getTransactionName(this.props.location)
  38. ),
  39. };
  40. static getDerivedStateFromProps(nextProps: Readonly<Props>, prevState: State): State {
  41. return {
  42. ...prevState,
  43. eventView: generateRumEventView(
  44. nextProps.location,
  45. getTransactionName(nextProps.location)
  46. ),
  47. };
  48. }
  49. getDocumentTitle(): string {
  50. const name = getTransactionName(this.props.location);
  51. const hasTransactionName = typeof name === 'string' && String(name).trim().length > 0;
  52. if (hasTransactionName) {
  53. return [String(name).trim(), t('Vitals')].join(' \u2014 ');
  54. }
  55. return [t('Summary'), t('Vitals')].join(' \u2014 ');
  56. }
  57. renderNoAccess = () => {
  58. return <Alert type="warning">{t("You don't have access to this feature")}</Alert>;
  59. };
  60. render() {
  61. const {organization, projects, location} = this.props;
  62. const {eventView} = this.state;
  63. const transactionName = getTransactionName(location);
  64. if (!eventView || transactionName === undefined) {
  65. // If there is no transaction name, redirect to the Performance landing page
  66. browserHistory.replace({
  67. pathname: `/organizations/${organization.slug}/performance/`,
  68. query: {
  69. ...location.query,
  70. },
  71. });
  72. return null;
  73. }
  74. const shouldForceProject = eventView.project.length === 1;
  75. const forceProject = shouldForceProject
  76. ? projects.find(p => parseInt(p.id, 10) === eventView.project[0])
  77. : undefined;
  78. const projectSlugs = eventView.project
  79. .map(projectId => projects.find(p => parseInt(p.id, 10) === projectId))
  80. .filter((p: Project | undefined): p is Project => p !== undefined)
  81. .map(p => p.slug);
  82. return (
  83. <SentryDocumentTitle
  84. title={this.getDocumentTitle()}
  85. orgSlug={organization.slug}
  86. projectSlug={forceProject?.slug}
  87. >
  88. <Feature
  89. features={['performance-view']}
  90. organization={organization}
  91. renderDisabled={this.renderNoAccess}
  92. >
  93. <GlobalSelectionHeader
  94. lockedMessageSubject={t('transaction')}
  95. shouldForceProject={shouldForceProject}
  96. forceProject={forceProject}
  97. specificProjectSlugs={projectSlugs}
  98. disableMultipleProjectSelection
  99. showProjectSettingsLink
  100. >
  101. <StyledPageContent>
  102. <LightWeightNoProjectMessage organization={organization}>
  103. <RumContent
  104. location={location}
  105. eventView={eventView}
  106. transactionName={transactionName}
  107. organization={organization}
  108. projects={projects}
  109. />
  110. </LightWeightNoProjectMessage>
  111. </StyledPageContent>
  112. </GlobalSelectionHeader>
  113. </Feature>
  114. </SentryDocumentTitle>
  115. );
  116. }
  117. }
  118. const StyledPageContent = styled(PageContent)`
  119. padding: 0;
  120. `;
  121. function generateRumEventView(
  122. location: Location,
  123. transactionName: string | undefined
  124. ): EventView | undefined {
  125. if (transactionName === undefined) {
  126. return undefined;
  127. }
  128. const query = decodeScalar(location.query.query, '');
  129. const conditions = tokenizeSearch(query);
  130. conditions
  131. .setTagValues('event.type', ['transaction'])
  132. .setTagValues('transaction.op', ['pageload'])
  133. .setTagValues('transaction', [transactionName]);
  134. Object.keys(conditions.tagValues).forEach(field => {
  135. if (isAggregateField(field)) conditions.removeTag(field);
  136. });
  137. const vitals = VITAL_GROUPS.reduce((allVitals: WebVital[], group) => {
  138. return allVitals.concat(group.vitals);
  139. }, []);
  140. return EventView.fromNewQueryWithLocation(
  141. {
  142. id: undefined,
  143. version: 2,
  144. name: transactionName,
  145. fields: [
  146. ...vitals.map(vital => `percentile(${vital}, ${PERCENTILE})`),
  147. ...vitals.map(vital => `count_at_least(${vital}, 0)`),
  148. ...vitals.map(
  149. vital => `count_at_least(${vital}, ${WEB_VITAL_DETAILS[vital].poorThreshold})`
  150. ),
  151. ],
  152. query: stringifyQueryObject(conditions),
  153. projects: [],
  154. },
  155. location
  156. );
  157. }
  158. export default withGlobalSelection(withProjects(withOrganization(TransactionVitals)));