projectPerformance.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Button from 'sentry/components/button';
  5. import Form from 'sentry/components/forms/form';
  6. import JsonForm from 'sentry/components/forms/jsonForm';
  7. import {Field} from 'sentry/components/forms/type';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import {PanelItem} from 'sentry/components/panels';
  11. import {t, tct} from 'sentry/locale';
  12. import {Organization, Project} from 'sentry/types';
  13. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  14. import routeTitleGen from 'sentry/utils/routeTitle';
  15. import AsyncView from 'sentry/views/asyncView';
  16. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  17. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  18. type Props = RouteComponentProps<{orgId: string; projectId: string}, {}> & {
  19. organization: Organization;
  20. project: Project;
  21. };
  22. type ProjectThreshold = {
  23. metric: string;
  24. threshold: string;
  25. editedBy?: string;
  26. id?: string;
  27. };
  28. type State = AsyncView['state'] & {
  29. threshold: ProjectThreshold;
  30. };
  31. class ProjectPerformance extends AsyncView<Props, State> {
  32. getTitle() {
  33. const {projectId} = this.props.params;
  34. return routeTitleGen(t('Performance'), projectId, false);
  35. }
  36. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  37. const {params} = this.props;
  38. const {orgId, projectId} = params;
  39. const endpoints: ReturnType<AsyncView['getEndpoints']> = [
  40. ['threshold', `/projects/${orgId}/${projectId}/transaction-threshold/configure/`],
  41. ];
  42. return endpoints;
  43. }
  44. handleDelete = () => {
  45. const {orgId, projectId} = this.props.params;
  46. const {organization} = this.props;
  47. this.setState({
  48. loading: true,
  49. });
  50. this.api.request(`/projects/${orgId}/${projectId}/transaction-threshold/configure/`, {
  51. method: 'DELETE',
  52. success: () => {
  53. trackAnalyticsEvent({
  54. eventKey: 'performance_views.project_transaction_threshold.clear',
  55. eventName: 'Project Transaction Threshold: Cleared',
  56. organization_id: organization.id,
  57. });
  58. },
  59. complete: () => this.fetchData(),
  60. });
  61. };
  62. getEmptyMessage() {
  63. return t('There is no threshold set for this project.');
  64. }
  65. renderLoading() {
  66. return (
  67. <LoadingIndicatorContainer>
  68. <LoadingIndicator />
  69. </LoadingIndicatorContainer>
  70. );
  71. }
  72. get formFields(): Field[] {
  73. const fields: Field[] = [
  74. {
  75. name: 'metric',
  76. type: 'select',
  77. label: t('Calculation Method'),
  78. options: [
  79. {value: 'duration', label: t('Transaction Duration')},
  80. {value: 'lcp', label: t('Largest Contentful Paint')},
  81. ],
  82. help: tct(
  83. 'This determines which duration is used to set your thresholds. By default, we use transaction duration which measures the entire length of the transaction. You can also set this to use a [link:Web Vital].',
  84. {
  85. link: (
  86. <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/" />
  87. ),
  88. }
  89. ),
  90. },
  91. {
  92. name: 'threshold',
  93. type: 'string',
  94. label: t('Response Time Threshold (ms)'),
  95. placeholder: t('300'),
  96. help: tct(
  97. 'Define what a satisfactory response time is based on the calculation method above. This will affect how your [link1:Apdex] and [link2:User Misery] thresholds are calculated. For example, misery will be 4x your satisfactory response time.',
  98. {
  99. link1: (
  100. <ExternalLink href="https://docs.sentry.io/performance-monitoring/performance/metrics/#apdex" />
  101. ),
  102. link2: (
  103. <ExternalLink href="https://docs.sentry.io/product/performance/metrics/#user-misery" />
  104. ),
  105. }
  106. ),
  107. },
  108. ];
  109. return fields;
  110. }
  111. get initialData() {
  112. const {threshold} = this.state;
  113. return {
  114. threshold: threshold.threshold,
  115. metric: threshold.metric,
  116. };
  117. }
  118. renderBody() {
  119. const {organization, project} = this.props;
  120. const endpoint = `/projects/${organization.slug}/${project.slug}/transaction-threshold/configure/`;
  121. return (
  122. <Fragment>
  123. <SettingsPageHeader title={t('Performance')} />
  124. <PermissionAlert />
  125. <Form
  126. saveOnBlur
  127. allowUndo
  128. initialData={this.initialData}
  129. apiMethod="POST"
  130. apiEndpoint={endpoint}
  131. onSubmitSuccess={resp => {
  132. const initial = this.initialData;
  133. const changedThreshold = initial.metric === resp.metric;
  134. trackAnalyticsEvent({
  135. eventKey: 'performance_views.project_transaction_threshold.change',
  136. eventName: 'Project Transaction Threshold: Changed',
  137. organization_id: organization.id,
  138. from: changedThreshold ? initial.threshold : initial.metric,
  139. to: changedThreshold ? resp.threshold : resp.metric,
  140. key: changedThreshold ? 'threshold' : 'metric',
  141. });
  142. this.setState({threshold: resp});
  143. }}
  144. >
  145. <JsonForm
  146. title={t('General')}
  147. fields={this.formFields}
  148. renderFooter={() => (
  149. <Actions>
  150. <Button type="button" onClick={() => this.handleDelete()}>
  151. {t('Reset All')}
  152. </Button>
  153. </Actions>
  154. )}
  155. />
  156. </Form>
  157. </Fragment>
  158. );
  159. }
  160. }
  161. const Actions = styled(PanelItem)`
  162. justify-content: flex-end;
  163. `;
  164. const LoadingIndicatorContainer = styled('div')`
  165. margin: 18px 18px 0;
  166. `;
  167. export default ProjectPerformance;