projectPerformance.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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 ProjectsStore from 'sentry/stores/projectsStore';
  15. import {Organization, Project, Scope} from 'sentry/types';
  16. import {DynamicSamplingBiasType} from 'sentry/types/sampling';
  17. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  18. import routeTitleGen from 'sentry/utils/routeTitle';
  19. import AsyncView from 'sentry/views/asyncView';
  20. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  21. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  22. // These labels need to be exported so that they can be used in audit logs
  23. export const retentionPrioritiesLabels = {
  24. boostLatestRelease: t('Prioritize new releases'),
  25. boostEnvironments: t('Prioritize dev environments'),
  26. boostKeyTransactions: t('Prioritize key transactions'),
  27. boostLowVolumeTransactions: t('Prioritize low-volume transactions'),
  28. ignoreHealthChecks: t('Deprioritize health checks'),
  29. };
  30. type RouteParams = {orgId: string; projectId: string};
  31. type Props = RouteComponentProps<{projectId: string}, {}> & {
  32. organization: Organization;
  33. project: Project;
  34. };
  35. type ProjectThreshold = {
  36. metric: string;
  37. threshold: string;
  38. editedBy?: string;
  39. id?: string;
  40. };
  41. type State = AsyncView['state'] & {
  42. threshold: ProjectThreshold;
  43. };
  44. class ProjectPerformance extends AsyncView<Props, State> {
  45. getTitle() {
  46. const {projectId} = this.props.params;
  47. return routeTitleGen(t('Performance'), projectId, false);
  48. }
  49. getProjectEndpoint({orgId, projectId}: RouteParams) {
  50. return `/projects/${orgId}/${projectId}/`;
  51. }
  52. getPerformanceIssuesEndpoint({orgId, projectId}: RouteParams) {
  53. return `/projects/${orgId}/${projectId}/performance-issues/configure/`;
  54. }
  55. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  56. const {params, organization} = this.props;
  57. const {projectId} = params;
  58. const endpoints: ReturnType<AsyncView['getEndpoints']> = [
  59. [
  60. 'threshold',
  61. `/projects/${organization.slug}/${projectId}/transaction-threshold/configure/`,
  62. ],
  63. ['project', `/projects/${organization.slug}/${projectId}/`],
  64. ];
  65. if (organization.features.includes('performance-issues-dev')) {
  66. const performanceIssuesEndpoint = [
  67. 'performance_issue_settings',
  68. `/projects/${organization.slug}/${projectId}/performance-issues/configure/`,
  69. ] as [string, string];
  70. endpoints.push(performanceIssuesEndpoint);
  71. }
  72. return endpoints;
  73. }
  74. getRetentionPrioritiesData(...data) {
  75. return {
  76. dynamicSamplingBiases: Object.entries(data[1].form).map(([key, value]) => ({
  77. id: key,
  78. active: value,
  79. })),
  80. };
  81. }
  82. handleDelete = () => {
  83. const {projectId} = this.props.params;
  84. const {organization} = this.props;
  85. this.setState({
  86. loading: true,
  87. });
  88. this.api.request(
  89. `/projects/${organization.slug}/${projectId}/transaction-threshold/configure/`,
  90. {
  91. method: 'DELETE',
  92. success: () => {
  93. trackAdvancedAnalyticsEvent(
  94. 'performance_views.project_transaction_threshold.clear',
  95. {organization}
  96. );
  97. },
  98. complete: () => this.fetchData(),
  99. }
  100. );
  101. };
  102. getEmptyMessage() {
  103. return t('There is no threshold set for this project.');
  104. }
  105. renderLoading() {
  106. return (
  107. <LoadingIndicatorContainer>
  108. <LoadingIndicator />
  109. </LoadingIndicatorContainer>
  110. );
  111. }
  112. get formFields(): Field[] {
  113. const fields: Field[] = [
  114. {
  115. name: 'metric',
  116. type: 'select',
  117. label: t('Calculation Method'),
  118. options: [
  119. {value: 'duration', label: t('Transaction Duration')},
  120. {value: 'lcp', label: t('Largest Contentful Paint')},
  121. ],
  122. help: tct(
  123. '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].',
  124. {
  125. link: (
  126. <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/" />
  127. ),
  128. }
  129. ),
  130. },
  131. {
  132. name: 'threshold',
  133. type: 'string',
  134. label: t('Response Time Threshold (ms)'),
  135. placeholder: t('300'),
  136. help: tct(
  137. '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.',
  138. {
  139. link1: (
  140. <ExternalLink href="https://docs.sentry.io/performance-monitoring/performance/metrics/#apdex" />
  141. ),
  142. link2: (
  143. <ExternalLink href="https://docs.sentry.io/product/performance/metrics/#user-misery" />
  144. ),
  145. }
  146. ),
  147. },
  148. ];
  149. return fields;
  150. }
  151. get performanceIssueFormFields(): Field[] {
  152. return [
  153. {
  154. name: 'performanceIssueCreationRate',
  155. type: 'range',
  156. label: t('Performance Issue Creation Rate'),
  157. min: 0.0,
  158. max: 1.0,
  159. step: 0.01,
  160. defaultValue: 0,
  161. help: t(
  162. 'This determines the rate at which performance issues are created. A rate of 0.0 will disable performance issue creation.'
  163. ),
  164. },
  165. {
  166. name: 'performanceIssueSendToPlatform',
  167. type: 'boolean',
  168. label: t('Send Occurrences To Platform'),
  169. defaultValue: false,
  170. help: t(
  171. 'This determines whether performance issue occurrences are sent to the issues platform.'
  172. ),
  173. },
  174. {
  175. name: 'performanceIssueCreationThroughPlatform',
  176. type: 'boolean',
  177. label: t('Create Issues Through Issues Platform'),
  178. defaultValue: false,
  179. help: t(
  180. 'This determines whether performance issues are created through the issues platform.'
  181. ),
  182. },
  183. ];
  184. }
  185. get performanceIssueDetectorsFormFields(): Field[] {
  186. return [
  187. {
  188. name: 'n_plus_one_db_detection_rate',
  189. type: 'range',
  190. label: t('N+1 (DB) Detection Rate'),
  191. min: 0.0,
  192. max: 1.0,
  193. step: 0.01,
  194. defaultValue: 0,
  195. },
  196. {
  197. name: 'n_plus_one_db_count',
  198. type: 'number',
  199. label: t('N+1 (DB) Minimum Count'),
  200. min: 0,
  201. max: 1000,
  202. defaultValue: 5,
  203. },
  204. {
  205. name: 'n_plus_one_db_duration_threshold',
  206. type: 'number',
  207. label: t('N+1 (DB) Duration Threshold'),
  208. min: 0,
  209. max: 1000000.0,
  210. defaultValue: 500,
  211. },
  212. {
  213. name: 'n_plus_one_api_calls_detection_rate',
  214. type: 'range',
  215. label: t('N+1 API Calls Detection Rate'),
  216. min: 0.0,
  217. max: 1.0,
  218. step: 0.01,
  219. defaultValue: 0,
  220. },
  221. {
  222. name: 'consecutive_db_queries_detection_rate',
  223. type: 'range',
  224. label: t('Consecutive DB Detection rate'),
  225. min: 0.0,
  226. max: 1.0,
  227. step: 0.01,
  228. defaultValue: 0,
  229. },
  230. ];
  231. }
  232. get retentionPrioritiesFormFields(): Field[] {
  233. return [
  234. {
  235. name: 'boostLatestRelease',
  236. type: 'boolean',
  237. label: retentionPrioritiesLabels.boostLatestRelease,
  238. help: t(
  239. 'Captures more transactions for your new releases as they are being adopted'
  240. ),
  241. getData: this.getRetentionPrioritiesData,
  242. },
  243. {
  244. name: 'boostEnvironments',
  245. type: 'boolean',
  246. label: retentionPrioritiesLabels.boostEnvironments,
  247. help: t(
  248. 'Captures more traces from environments that contain "dev", "test", "qa", and "local"'
  249. ),
  250. getData: this.getRetentionPrioritiesData,
  251. },
  252. {
  253. name: 'boostKeyTransactions',
  254. type: 'boolean',
  255. label: retentionPrioritiesLabels.boostKeyTransactions,
  256. help: t('Captures more of your most important (starred) transactions'),
  257. getData: this.getRetentionPrioritiesData,
  258. },
  259. {
  260. name: 'boostLowVolumeTransactions',
  261. type: 'boolean',
  262. label: retentionPrioritiesLabels.boostLowVolumeTransactions,
  263. help: t("Balance high-volume endpoints so they don't drown out low-volume ones"),
  264. visible: this.props.organization.features.includes(
  265. 'dynamic-sampling-transaction-name-priority'
  266. ),
  267. getData: this.getRetentionPrioritiesData,
  268. },
  269. {
  270. name: 'ignoreHealthChecks',
  271. type: 'boolean',
  272. label: retentionPrioritiesLabels.ignoreHealthChecks,
  273. help: t('Captures fewer of your health checks transactions'),
  274. getData: this.getRetentionPrioritiesData,
  275. },
  276. ];
  277. }
  278. get initialData() {
  279. const {threshold} = this.state;
  280. return {
  281. threshold: threshold.threshold,
  282. metric: threshold.metric,
  283. };
  284. }
  285. renderBody() {
  286. const {organization, project} = this.props;
  287. const endpoint = `/projects/${organization.slug}/${project.slug}/transaction-threshold/configure/`;
  288. const requiredScopes: Scope[] = ['project:write'];
  289. const params = {orgId: organization.slug, projectId: project.slug};
  290. const projectEndpoint = this.getProjectEndpoint(params);
  291. const performanceIssuesEndpoint = this.getPerformanceIssuesEndpoint(params);
  292. return (
  293. <Fragment>
  294. <SettingsPageHeader title={t('Performance')} />
  295. <PermissionAlert access={requiredScopes} />
  296. <Form
  297. saveOnBlur
  298. allowUndo
  299. initialData={this.initialData}
  300. apiMethod="POST"
  301. apiEndpoint={endpoint}
  302. onSubmitSuccess={resp => {
  303. const initial = this.initialData;
  304. const changedThreshold = initial.metric === resp.metric;
  305. trackAdvancedAnalyticsEvent(
  306. 'performance_views.project_transaction_threshold.change',
  307. {
  308. organization,
  309. from: changedThreshold ? initial.threshold : initial.metric,
  310. to: changedThreshold ? resp.threshold : resp.metric,
  311. key: changedThreshold ? 'threshold' : 'metric',
  312. }
  313. );
  314. this.setState({threshold: resp});
  315. }}
  316. >
  317. <Access access={requiredScopes}>
  318. {({hasAccess}) => (
  319. <JsonForm
  320. title={t('General')}
  321. fields={this.formFields}
  322. disabled={!hasAccess}
  323. renderFooter={() => (
  324. <Actions>
  325. <Button onClick={() => this.handleDelete()}>{t('Reset All')}</Button>
  326. </Actions>
  327. )}
  328. />
  329. )}
  330. </Access>
  331. </Form>
  332. <Feature features={['organizations:dynamic-sampling']}>
  333. <Form
  334. saveOnBlur
  335. allowUndo
  336. initialData={
  337. project.dynamicSamplingBiases?.reduce((acc, bias) => {
  338. acc[bias.id] = bias.active;
  339. return acc;
  340. }, {}) ?? {}
  341. }
  342. onSubmitSuccess={(response, _instance, id, change) => {
  343. ProjectsStore.onUpdateSuccess(response);
  344. trackAdvancedAnalyticsEvent(
  345. change?.new === true
  346. ? 'dynamic_sampling_settings.priority_enabled'
  347. : 'dynamic_sampling_settings.priority_disabled',
  348. {
  349. organization,
  350. project_id: project.id,
  351. id: id as DynamicSamplingBiasType,
  352. }
  353. );
  354. }}
  355. apiMethod="PUT"
  356. apiEndpoint={projectEndpoint}
  357. >
  358. <Access access={requiredScopes}>
  359. {({hasAccess}) => (
  360. <JsonForm
  361. title={t('Retention Priorities')}
  362. fields={this.retentionPrioritiesFormFields}
  363. disabled={!hasAccess}
  364. renderFooter={() => (
  365. <Actions>
  366. <Button
  367. external
  368. href="https://docs.sentry.io/product/performance/performance-at-scale/"
  369. >
  370. {t('Read docs')}
  371. </Button>
  372. </Actions>
  373. )}
  374. />
  375. )}
  376. </Access>
  377. </Form>
  378. </Feature>
  379. <Feature features={['organizations:performance-issues-dev']}>
  380. <Fragment>
  381. <Form
  382. saveOnBlur
  383. allowUndo
  384. initialData={{
  385. performanceIssueCreationRate:
  386. this.state.project.performanceIssueCreationRate,
  387. performanceIssueSendToPlatform:
  388. this.state.project.performanceIssueSendToPlatform,
  389. performanceIssueCreationThroughPlatform:
  390. this.state.project.performanceIssueCreationThroughPlatform,
  391. }}
  392. apiMethod="PUT"
  393. apiEndpoint={projectEndpoint}
  394. >
  395. <Access access={requiredScopes}>
  396. {({hasAccess}) => (
  397. <JsonForm
  398. title={t('Performance Issues - All')}
  399. fields={this.performanceIssueFormFields}
  400. disabled={!hasAccess}
  401. />
  402. )}
  403. </Access>
  404. </Form>
  405. <Form
  406. saveOnBlur
  407. allowUndo
  408. initialData={this.state.performance_issue_settings}
  409. apiMethod="PUT"
  410. apiEndpoint={performanceIssuesEndpoint}
  411. >
  412. <Access access={requiredScopes}>
  413. {({hasAccess}) => (
  414. <JsonForm
  415. title={t('Performance Issues - Detector Settings')}
  416. fields={this.performanceIssueDetectorsFormFields}
  417. disabled={!hasAccess}
  418. />
  419. )}
  420. </Access>
  421. </Form>
  422. </Fragment>
  423. </Feature>
  424. </Fragment>
  425. );
  426. }
  427. }
  428. const Actions = styled(PanelItem)`
  429. justify-content: flex-end;
  430. `;
  431. const LoadingIndicatorContainer = styled('div')`
  432. margin: 18px 18px 0;
  433. `;
  434. export default ProjectPerformance;