projectPerformance.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Access from 'sentry/components/acl/access';
  5. import Feature from 'sentry/components/acl/feature';
  6. import Button from 'sentry/components/button';
  7. import Form from 'sentry/components/forms/form';
  8. import JsonForm from 'sentry/components/forms/jsonForm';
  9. import {Field} from 'sentry/components/forms/types';
  10. import ExternalLink from 'sentry/components/links/externalLink';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import {PanelItem} from 'sentry/components/panels';
  13. import {t, tct} from 'sentry/locale';
  14. import {Organization, Project, Scope} from 'sentry/types';
  15. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  16. import routeTitleGen from 'sentry/utils/routeTitle';
  17. import AsyncView from 'sentry/views/asyncView';
  18. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  19. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  20. type RouteParams = {orgId: string; projectId: string};
  21. type Props = RouteComponentProps<{orgId: string; projectId: string}, {}> & {
  22. organization: Organization;
  23. project: Project;
  24. };
  25. type ProjectThreshold = {
  26. metric: string;
  27. threshold: string;
  28. editedBy?: string;
  29. id?: string;
  30. };
  31. type State = AsyncView['state'] & {
  32. threshold: ProjectThreshold;
  33. };
  34. class ProjectPerformance extends AsyncView<Props, State> {
  35. getTitle() {
  36. const {projectId} = this.props.params;
  37. return routeTitleGen(t('Performance'), projectId, false);
  38. }
  39. getProjectEndpoint({orgId, projectId}: RouteParams) {
  40. return `/projects/${orgId}/${projectId}/`;
  41. }
  42. getPerformanceIssuesEndpoint({orgId, projectId}: RouteParams) {
  43. return `/projects/${orgId}/${projectId}/performance-issues/configure/`;
  44. }
  45. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  46. const {params, organization} = this.props;
  47. const {orgId, projectId} = params;
  48. const endpoints: ReturnType<AsyncView['getEndpoints']> = [
  49. ['threshold', `/projects/${orgId}/${projectId}/transaction-threshold/configure/`],
  50. ['project', `/projects/${orgId}/${projectId}/`],
  51. ];
  52. if (organization.features.includes('performance-issues-dev')) {
  53. const performanceIssuesEndpoint = [
  54. 'performance_issue_settings',
  55. `/projects/${orgId}/${projectId}/performance-issues/configure/`,
  56. ] as [string, string];
  57. endpoints.push(performanceIssuesEndpoint);
  58. }
  59. return endpoints;
  60. }
  61. handleDelete = () => {
  62. const {orgId, projectId} = this.props.params;
  63. const {organization} = this.props;
  64. this.setState({
  65. loading: true,
  66. });
  67. this.api.request(`/projects/${orgId}/${projectId}/transaction-threshold/configure/`, {
  68. method: 'DELETE',
  69. success: () => {
  70. trackAdvancedAnalyticsEvent(
  71. 'performance_views.project_transaction_threshold.clear',
  72. {organization}
  73. );
  74. },
  75. complete: () => this.fetchData(),
  76. });
  77. };
  78. getEmptyMessage() {
  79. return t('There is no threshold set for this project.');
  80. }
  81. renderLoading() {
  82. return (
  83. <LoadingIndicatorContainer>
  84. <LoadingIndicator />
  85. </LoadingIndicatorContainer>
  86. );
  87. }
  88. get formFields(): Field[] {
  89. const fields: Field[] = [
  90. {
  91. name: 'metric',
  92. type: 'select',
  93. label: t('Calculation Method'),
  94. options: [
  95. {value: 'duration', label: t('Transaction Duration')},
  96. {value: 'lcp', label: t('Largest Contentful Paint')},
  97. ],
  98. help: tct(
  99. '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].',
  100. {
  101. link: (
  102. <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/" />
  103. ),
  104. }
  105. ),
  106. },
  107. {
  108. name: 'threshold',
  109. type: 'string',
  110. label: t('Response Time Threshold (ms)'),
  111. placeholder: t('300'),
  112. help: tct(
  113. '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.',
  114. {
  115. link1: (
  116. <ExternalLink href="https://docs.sentry.io/performance-monitoring/performance/metrics/#apdex" />
  117. ),
  118. link2: (
  119. <ExternalLink href="https://docs.sentry.io/product/performance/metrics/#user-misery" />
  120. ),
  121. }
  122. ),
  123. },
  124. ];
  125. return fields;
  126. }
  127. get performanceIssueFormFields(): Field[] {
  128. return [
  129. {
  130. name: 'performanceIssueCreationRate',
  131. type: 'range',
  132. label: t('Performance Issue Creation Rate'),
  133. min: 0.0,
  134. max: 1.0,
  135. step: 0.01,
  136. defaultValue: 0,
  137. help: t(
  138. 'This determines the rate at which performance issues are created. A rate of 0.0 will disable performance issue creation.'
  139. ),
  140. },
  141. ];
  142. }
  143. get performanceIssueDetectorsFormFields(): Field[] {
  144. return [
  145. {
  146. name: 'n_plus_one_db_detection_rate',
  147. type: 'range',
  148. label: t('N+1 (DB) Detection Rate'),
  149. min: 0.0,
  150. max: 1.0,
  151. step: 0.01,
  152. defaultValue: 0,
  153. },
  154. {
  155. name: 'n_plus_one_db_issue_rate',
  156. type: 'range',
  157. label: t('N+1 (DB) Issue Rate'),
  158. min: 0.0,
  159. max: 1.0,
  160. step: 0.01,
  161. defaultValue: 0,
  162. },
  163. {
  164. name: 'n_plus_one_db_count',
  165. type: 'number',
  166. label: t('N+1 (DB) Minimum Count'),
  167. min: 0,
  168. max: 1000,
  169. defaultValue: 5,
  170. },
  171. {
  172. name: 'n_plus_one_db_duration_threshold',
  173. type: 'number',
  174. label: t('N+1 (DB) Duration Threshold'),
  175. min: 0,
  176. max: 1000000.0,
  177. defaultValue: 500,
  178. },
  179. ];
  180. }
  181. get initialData() {
  182. const {threshold} = this.state;
  183. return {
  184. threshold: threshold.threshold,
  185. metric: threshold.metric,
  186. };
  187. }
  188. renderBody() {
  189. const {organization, project, params} = this.props;
  190. const endpoint = `/projects/${organization.slug}/${project.slug}/transaction-threshold/configure/`;
  191. const requiredScopes: Scope[] = ['project:write'];
  192. const projectEndpoint = this.getProjectEndpoint(params);
  193. const performanceIssuesEndpoint = this.getPerformanceIssuesEndpoint(params);
  194. return (
  195. <Fragment>
  196. <SettingsPageHeader title={t('Performance')} />
  197. <PermissionAlert access={requiredScopes} />
  198. <Form
  199. saveOnBlur
  200. allowUndo
  201. initialData={this.initialData}
  202. apiMethod="POST"
  203. apiEndpoint={endpoint}
  204. onSubmitSuccess={resp => {
  205. const initial = this.initialData;
  206. const changedThreshold = initial.metric === resp.metric;
  207. trackAdvancedAnalyticsEvent(
  208. 'performance_views.project_transaction_threshold.change',
  209. {
  210. organization,
  211. from: changedThreshold ? initial.threshold : initial.metric,
  212. to: changedThreshold ? resp.threshold : resp.metric,
  213. key: changedThreshold ? 'threshold' : 'metric',
  214. }
  215. );
  216. this.setState({threshold: resp});
  217. }}
  218. >
  219. <Access access={requiredScopes}>
  220. {({hasAccess}) => (
  221. <JsonForm
  222. title={t('General')}
  223. fields={this.formFields}
  224. disabled={!hasAccess}
  225. renderFooter={() => (
  226. <Actions>
  227. <Button type="button" onClick={() => this.handleDelete()}>
  228. {t('Reset All')}
  229. </Button>
  230. </Actions>
  231. )}
  232. />
  233. )}
  234. </Access>
  235. </Form>
  236. <Feature features={['organizations:performance-issues-dev']}>
  237. <Fragment>
  238. <Form
  239. saveOnBlur
  240. allowUndo
  241. initialData={{
  242. performanceIssueCreationRate:
  243. this.state.project.performanceIssueCreationRate,
  244. }}
  245. apiMethod="PUT"
  246. apiEndpoint={projectEndpoint}
  247. >
  248. <Access access={requiredScopes}>
  249. {({hasAccess}) => (
  250. <JsonForm
  251. title={t('Performance Issues - All')}
  252. fields={this.performanceIssueFormFields}
  253. disabled={!hasAccess}
  254. />
  255. )}
  256. </Access>
  257. </Form>
  258. <Form
  259. saveOnBlur
  260. allowUndo
  261. initialData={this.state.performance_issue_settings}
  262. apiMethod="PUT"
  263. apiEndpoint={performanceIssuesEndpoint}
  264. >
  265. <Access access={requiredScopes}>
  266. {({hasAccess}) => (
  267. <JsonForm
  268. title={t('Performance Issues - Detector Settings')}
  269. fields={this.performanceIssueDetectorsFormFields}
  270. disabled={!hasAccess}
  271. />
  272. )}
  273. </Access>
  274. </Form>
  275. </Fragment>
  276. </Feature>
  277. </Fragment>
  278. );
  279. }
  280. }
  281. const Actions = styled(PanelItem)`
  282. justify-content: flex-end;
  283. `;
  284. const LoadingIndicatorContainer = styled('div')`
  285. margin: 18px 18px 0;
  286. `;
  287. export default ProjectPerformance;