projectPerformance.tsx 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105
  1. import {Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import Access from 'sentry/components/acl/access';
  6. import Feature from 'sentry/components/acl/feature';
  7. import {Button} from 'sentry/components/button';
  8. import Confirm from 'sentry/components/confirm';
  9. import FieldWrapper from 'sentry/components/forms/fieldGroup/fieldWrapper';
  10. import Form from 'sentry/components/forms/form';
  11. import JsonForm from 'sentry/components/forms/jsonForm';
  12. import type {Field, JsonFormObject} from 'sentry/components/forms/types';
  13. import ExternalLink from 'sentry/components/links/externalLink';
  14. import LoadingIndicator from 'sentry/components/loadingIndicator';
  15. import Panel from 'sentry/components/panels/panel';
  16. import PanelFooter from 'sentry/components/panels/panelFooter';
  17. import PanelHeader from 'sentry/components/panels/panelHeader';
  18. import PanelItem from 'sentry/components/panels/panelItem';
  19. import {t, tct} from 'sentry/locale';
  20. import ConfigStore from 'sentry/stores/configStore';
  21. import ProjectsStore from 'sentry/stores/projectsStore';
  22. import {space} from 'sentry/styles/space';
  23. import type {Organization, Project, Scope} from 'sentry/types';
  24. import {IssueTitle, IssueType} from 'sentry/types';
  25. import type {DynamicSamplingBiasType} from 'sentry/types/sampling';
  26. import {trackAnalytics} from 'sentry/utils/analytics';
  27. import {safeGetQsParam} from 'sentry/utils/integrationUtil';
  28. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  29. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  30. import routeTitleGen from 'sentry/utils/routeTitle';
  31. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  32. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  33. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  34. // These labels need to be exported so that they can be used in audit logs
  35. export const retentionPrioritiesLabels = {
  36. boostLatestRelease: t('Prioritize new releases'),
  37. boostEnvironments: t('Prioritize dev environments'),
  38. boostLowVolumeTransactions: t('Prioritize low-volume transactions'),
  39. ignoreHealthChecks: t('Deprioritize health checks'),
  40. };
  41. export const allowedDurationValues: number[] = [
  42. 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1500, 2000, 2500,
  43. 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7500, 8000, 8500, 9000, 9500,
  44. 10000,
  45. ]; // In milliseconds
  46. export const allowedPercentageValues: number[] = [
  47. 0.2, 0.25, 0.3, 0.33, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95,
  48. ];
  49. export const allowedSizeValues: number[] = [
  50. 50000, 100000, 200000, 300000, 400000, 500000, 512000, 600000, 700000, 800000, 900000,
  51. 1000000, 2000000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000,
  52. 10000000,
  53. ]; // 50kb to 10MB in bytes
  54. export const projectDetectorSettingsId = 'detector-threshold-settings';
  55. type ProjectPerformanceSettings = {[key: string]: number | boolean};
  56. enum DetectorConfigAdmin {
  57. N_PLUS_DB_ENABLED = 'n_plus_one_db_queries_detection_enabled',
  58. SLOW_DB_ENABLED = 'slow_db_queries_detection_enabled',
  59. DB_MAIN_THREAD_ENABLED = 'db_on_main_thread_detection_enabled',
  60. FILE_IO_ENABLED = 'file_io_on_main_thread_detection_enabled',
  61. CONSECUTIVE_DB_ENABLED = 'consecutive_db_queries_detection_enabled',
  62. RENDER_BLOCK_ASSET_ENABLED = 'large_render_blocking_asset_detection_enabled',
  63. UNCOMPRESSED_ASSET_ENABLED = 'uncompressed_assets_detection_enabled',
  64. LARGE_HTTP_PAYLOAD_ENABLED = 'large_http_payload_detection_enabled',
  65. N_PLUS_ONE_API_CALLS_ENABLED = 'n_plus_one_api_calls_detection_enabled',
  66. CONSECUTIVE_HTTP_ENABLED = 'consecutive_http_spans_detection_enabled',
  67. HTTP_OVERHEAD_ENABLED = 'http_overhead_detection_enabled',
  68. TRANSACTION_DURATION_REGRESSION_ENABLED = 'transaction_duration_regression_detection_enabled',
  69. }
  70. export enum DetectorConfigCustomer {
  71. SLOW_DB_DURATION = 'slow_db_query_duration_threshold',
  72. N_PLUS_DB_DURATION = 'n_plus_one_db_duration_threshold',
  73. N_PLUS_API_CALLS_DURATION = 'n_plus_one_api_calls_total_duration_threshold',
  74. RENDER_BLOCKING_ASSET_RATIO = 'render_blocking_fcp_ratio',
  75. LARGE_HTT_PAYLOAD_SIZE = 'large_http_payload_size_threshold',
  76. DB_ON_MAIN_THREAD_DURATION = 'db_on_main_thread_duration_threshold',
  77. FILE_IO_MAIN_THREAD_DURATION = 'file_io_on_main_thread_duration_threshold',
  78. UNCOMPRESSED_ASSET_DURATION = 'uncompressed_asset_duration_threshold',
  79. UNCOMPRESSED_ASSET_SIZE = 'uncompressed_asset_size_threshold',
  80. CONSECUTIVE_DB_MIN_TIME_SAVED = 'consecutive_db_min_time_saved_threshold',
  81. CONSECUTIVE_HTTP_MIN_TIME_SAVED = 'consecutive_http_spans_min_time_saved_threshold',
  82. HTTP_OVERHEAD_REQUEST_DELAY = 'http_request_delay_threshold',
  83. }
  84. type RouteParams = {orgId: string; projectId: string};
  85. type Props = RouteComponentProps<{projectId: string}, {}> & {
  86. organization: Organization;
  87. project: Project;
  88. };
  89. type ProjectThreshold = {
  90. metric: string;
  91. threshold: string;
  92. editedBy?: string;
  93. id?: string;
  94. };
  95. type State = DeprecatedAsyncView['state'] & {
  96. threshold: ProjectThreshold;
  97. };
  98. class ProjectPerformance extends DeprecatedAsyncView<Props, State> {
  99. getTitle() {
  100. const {projectId} = this.props.params;
  101. return routeTitleGen(t('Performance'), projectId, false);
  102. }
  103. getProjectEndpoint({orgId, projectId}: RouteParams) {
  104. return `/projects/${orgId}/${projectId}/`;
  105. }
  106. getPerformanceIssuesEndpoint({orgId, projectId}: RouteParams) {
  107. return `/projects/${orgId}/${projectId}/performance-issues/configure/`;
  108. }
  109. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  110. const {params, organization} = this.props;
  111. const {projectId} = params;
  112. const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
  113. [
  114. 'threshold',
  115. `/projects/${organization.slug}/${projectId}/transaction-threshold/configure/`,
  116. ],
  117. ['project', `/projects/${organization.slug}/${projectId}/`],
  118. ];
  119. const performanceIssuesEndpoint: ReturnType<
  120. DeprecatedAsyncView['getEndpoints']
  121. >[number] = [
  122. 'performance_issue_settings',
  123. `/projects/${organization.slug}/${projectId}/performance-issues/configure/`,
  124. ];
  125. const generalSettingsEndpoint: ReturnType<
  126. DeprecatedAsyncView['getEndpoints']
  127. >[number] = [
  128. 'general',
  129. `/projects/${organization.slug}/${projectId}/performance/configure/`,
  130. ];
  131. endpoints.push(performanceIssuesEndpoint);
  132. endpoints.push(generalSettingsEndpoint);
  133. return endpoints;
  134. }
  135. getRetentionPrioritiesData(...data) {
  136. return {
  137. dynamicSamplingBiases: Object.entries(data[1].form).map(([key, value]) => ({
  138. id: key,
  139. active: value,
  140. })),
  141. };
  142. }
  143. handleDelete = () => {
  144. const {projectId} = this.props.params;
  145. const {organization} = this.props;
  146. this.setState({
  147. loading: true,
  148. });
  149. this.api.request(
  150. `/projects/${organization.slug}/${projectId}/transaction-threshold/configure/`,
  151. {
  152. method: 'DELETE',
  153. success: () => {
  154. trackAnalytics('performance_views.project_transaction_threshold.clear', {
  155. organization,
  156. });
  157. },
  158. complete: () => this.fetchData(),
  159. }
  160. );
  161. };
  162. handleThresholdsReset = () => {
  163. const {projectId} = this.props.params;
  164. const {organization, project} = this.props;
  165. this.setState({
  166. loading: true,
  167. });
  168. trackAnalytics('performance_views.project_issue_detection_thresholds_reset', {
  169. organization,
  170. project_slug: project.slug,
  171. });
  172. this.api.request(
  173. `/projects/${organization.slug}/${projectId}/performance-issues/configure/`,
  174. {
  175. method: 'DELETE',
  176. complete: () => this.fetchData(),
  177. }
  178. );
  179. };
  180. getEmptyMessage() {
  181. return t('There is no threshold set for this project.');
  182. }
  183. renderLoading() {
  184. return (
  185. <LoadingIndicatorContainer>
  186. <LoadingIndicator />
  187. </LoadingIndicatorContainer>
  188. );
  189. }
  190. get formFields(): Field[] {
  191. const fields: Field[] = [
  192. {
  193. name: 'metric',
  194. type: 'select',
  195. label: t('Calculation Method'),
  196. options: [
  197. {value: 'duration', label: t('Transaction Duration')},
  198. {value: 'lcp', label: t('Largest Contentful Paint')},
  199. ],
  200. help: tct(
  201. '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].',
  202. {
  203. link: (
  204. <ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/" />
  205. ),
  206. }
  207. ),
  208. },
  209. {
  210. name: 'threshold',
  211. type: 'string',
  212. label: t('Response Time Threshold (ms)'),
  213. placeholder: t('300'),
  214. help: tct(
  215. '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.',
  216. {
  217. link1: (
  218. <ExternalLink href="https://docs.sentry.io/performance-monitoring/performance/metrics/#apdex" />
  219. ),
  220. link2: (
  221. <ExternalLink href="https://docs.sentry.io/product/performance/metrics/#user-misery" />
  222. ),
  223. }
  224. ),
  225. },
  226. ];
  227. return fields;
  228. }
  229. get areAllConfigurationsDisabled(): boolean {
  230. let result = true;
  231. Object.values(DetectorConfigAdmin).forEach(threshold => {
  232. result = result && !this.state.performance_issue_settings[threshold];
  233. });
  234. return result;
  235. }
  236. get performanceIssueFormFields(): Field[] {
  237. return [
  238. {
  239. name: 'performanceIssueCreationRate',
  240. type: 'range',
  241. label: t('Performance Issue Creation Rate'),
  242. min: 0.0,
  243. max: 1.0,
  244. step: 0.01,
  245. defaultValue: 0,
  246. help: t(
  247. 'This determines the rate at which performance issues are created. A rate of 0.0 will disable performance issue creation.'
  248. ),
  249. },
  250. {
  251. name: 'performanceIssueSendToPlatform',
  252. type: 'boolean',
  253. label: t('Send Occurrences To Platform'),
  254. defaultValue: false,
  255. help: t(
  256. 'This determines whether performance issue occurrences are sent to the issues platform.'
  257. ),
  258. },
  259. {
  260. name: 'performanceIssueCreationThroughPlatform',
  261. type: 'boolean',
  262. label: t('Create Issues Through Issues Platform'),
  263. defaultValue: false,
  264. help: t(
  265. 'This determines whether performance issues are created through the issues platform.'
  266. ),
  267. },
  268. ];
  269. }
  270. get performanceIssueDetectorAdminFields(): Field[] {
  271. return [
  272. {
  273. name: DetectorConfigAdmin.N_PLUS_DB_ENABLED,
  274. type: 'boolean',
  275. label: t('N+1 DB Queries Detection Enabled'),
  276. defaultValue: true,
  277. onChange: value =>
  278. this.setState({
  279. performance_issue_settings: {
  280. ...this.state.performance_issue_settings,
  281. n_plus_one_db_queries_detection_enabled: value,
  282. },
  283. }),
  284. },
  285. {
  286. name: DetectorConfigAdmin.SLOW_DB_ENABLED,
  287. type: 'boolean',
  288. label: t('Slow DB Queries Detection Enabled'),
  289. defaultValue: true,
  290. onChange: value =>
  291. this.setState({
  292. performance_issue_settings: {
  293. ...this.state.performance_issue_settings,
  294. slow_db_queries_detection_enabled: value,
  295. },
  296. }),
  297. },
  298. {
  299. name: DetectorConfigAdmin.N_PLUS_ONE_API_CALLS_ENABLED,
  300. type: 'boolean',
  301. label: t('N+1 API Calls Detection Enabled'),
  302. defaultValue: true,
  303. onChange: value =>
  304. this.setState({
  305. performance_issue_settings: {
  306. ...this.state.performance_issue_settings,
  307. n_plus_one_api_calls_detection_enabled: value,
  308. },
  309. }),
  310. },
  311. {
  312. name: DetectorConfigAdmin.RENDER_BLOCK_ASSET_ENABLED,
  313. type: 'boolean',
  314. label: t('Large Render Blocking Asset Detection Enabled'),
  315. defaultValue: true,
  316. onChange: value =>
  317. this.setState({
  318. performance_issue_settings: {
  319. ...this.state.performance_issue_settings,
  320. large_render_blocking_asset_detection_enabled: value,
  321. },
  322. }),
  323. },
  324. {
  325. name: DetectorConfigAdmin.CONSECUTIVE_DB_ENABLED,
  326. type: 'boolean',
  327. label: t('Consecutive DB Queries Detection Enabled'),
  328. defaultValue: true,
  329. onChange: value =>
  330. this.setState({
  331. performance_issue_settings: {
  332. ...this.state.performance_issue_settings,
  333. consecutive_db_queries_detection_enabled: value,
  334. },
  335. }),
  336. },
  337. {
  338. name: DetectorConfigAdmin.LARGE_HTTP_PAYLOAD_ENABLED,
  339. type: 'boolean',
  340. label: t('Large HTTP Payload Detection Enabled'),
  341. defaultValue: true,
  342. onChange: value =>
  343. this.setState({
  344. performance_issue_settings: {
  345. ...this.state.performance_issue_settings,
  346. large_http_payload_detection_enabled: value,
  347. },
  348. }),
  349. },
  350. {
  351. name: DetectorConfigAdmin.DB_MAIN_THREAD_ENABLED,
  352. type: 'boolean',
  353. label: t('DB On Main Thread Detection Enabled'),
  354. defaultValue: true,
  355. onChange: value =>
  356. this.setState({
  357. performance_issue_settings: {
  358. ...this.state.performance_issue_settings,
  359. db_on_main_thread_detection_enabled: value,
  360. },
  361. }),
  362. },
  363. {
  364. name: DetectorConfigAdmin.FILE_IO_ENABLED,
  365. type: 'boolean',
  366. label: t('File I/O on Main Thread Detection Enabled'),
  367. defaultValue: true,
  368. onChange: value =>
  369. this.setState({
  370. performance_issue_settings: {
  371. ...this.state.performance_issue_settings,
  372. file_io_on_main_thread_detection_enabled: value,
  373. },
  374. }),
  375. },
  376. {
  377. name: DetectorConfigAdmin.UNCOMPRESSED_ASSET_ENABLED,
  378. type: 'boolean',
  379. label: t('Uncompressed Assets Detection Enabled'),
  380. defaultValue: true,
  381. onChange: value =>
  382. this.setState({
  383. performance_issue_settings: {
  384. ...this.state.performance_issue_settings,
  385. uncompressed_assets_detection_enabled: value,
  386. },
  387. }),
  388. },
  389. {
  390. name: DetectorConfigAdmin.CONSECUTIVE_HTTP_ENABLED,
  391. type: 'boolean',
  392. label: t('Consecutive HTTP Detection Enabled'),
  393. defaultValue: true,
  394. onChange: value =>
  395. this.setState({
  396. performance_issue_settings: {
  397. ...this.state.performance_issue_settings,
  398. consecutive_http_spans_detection_enabled: value,
  399. },
  400. }),
  401. },
  402. {
  403. name: DetectorConfigAdmin.HTTP_OVERHEAD_ENABLED,
  404. type: 'boolean',
  405. label: t('HTTP/1.1 Overhead Enabled'),
  406. defaultValue: true,
  407. onChange: value =>
  408. this.setState({
  409. performance_issue_settings: {
  410. ...this.state.performance_issue_settings,
  411. [DetectorConfigAdmin.HTTP_OVERHEAD_ENABLED]: value,
  412. },
  413. }),
  414. },
  415. {
  416. name: DetectorConfigAdmin.TRANSACTION_DURATION_REGRESSION_ENABLED,
  417. type: 'boolean',
  418. label: t('Transaction Duration Regression Enabled'),
  419. defaultValue: true,
  420. onChange: value =>
  421. this.setState({
  422. performance_issue_settings: {
  423. ...this.state.performance_issue__settings,
  424. [DetectorConfigAdmin.TRANSACTION_DURATION_REGRESSION_ENABLED]: value,
  425. },
  426. }),
  427. },
  428. ];
  429. }
  430. project_owner_detector_settings = (hasAccess: boolean): JsonFormObject[] => {
  431. const performanceSettings: ProjectPerformanceSettings =
  432. this.state.performance_issue_settings;
  433. const supportMail = ConfigStore.get('supportEmail');
  434. const disabledReason = hasAccess
  435. ? tct(
  436. 'Detection of this issue has been disabled. Contact our support team at [link:support@sentry.io].',
  437. {
  438. link: <ExternalLink href={'mailto:' + supportMail} />,
  439. }
  440. )
  441. : null;
  442. const formatDuration = (value: number | ''): string => {
  443. return value ? (value < 1000 ? `${value}ms` : `${value / 1000}s`) : '';
  444. };
  445. const formatSize = (value: number | ''): string => {
  446. return value
  447. ? value < 1000000
  448. ? `${value / 1000}KB`
  449. : `${value / 1000000}MB`
  450. : '';
  451. };
  452. const formatFrameRate = (value: number | ''): string => {
  453. const fps = value && 1000 / value;
  454. return fps ? `${Math.floor(fps / 5) * 5}fps` : '';
  455. };
  456. const issueType = safeGetQsParam('issueType');
  457. return [
  458. {
  459. title: IssueTitle.PERFORMANCE_N_PLUS_ONE_DB_QUERIES,
  460. fields: [
  461. {
  462. name: DetectorConfigCustomer.N_PLUS_DB_DURATION,
  463. type: 'range',
  464. label: t('Minimum Total Duration'),
  465. defaultValue: 100, // ms
  466. help: t(
  467. 'Setting the value to 100ms, means that an eligible event will be detected as a N+1 DB Query Issue only if the total duration of the involved spans exceeds 100ms'
  468. ),
  469. allowedValues: allowedDurationValues,
  470. disabled: !(
  471. hasAccess && performanceSettings[DetectorConfigAdmin.N_PLUS_DB_ENABLED]
  472. ),
  473. tickValues: [0, allowedDurationValues.length - 1],
  474. showTickLabels: true,
  475. formatLabel: formatDuration,
  476. flexibleControlStateSize: true,
  477. disabledReason,
  478. },
  479. ],
  480. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES,
  481. },
  482. {
  483. title: IssueTitle.PERFORMANCE_SLOW_DB_QUERY,
  484. fields: [
  485. {
  486. name: DetectorConfigCustomer.SLOW_DB_DURATION,
  487. type: 'range',
  488. label: t('Minimum Duration'),
  489. defaultValue: 1000, // ms
  490. help: t(
  491. 'Setting the value to 1s, means that an eligible event will be detected as a Slow DB Query Issue only if the duration of the involved db span exceeds 1s.'
  492. ),
  493. tickValues: [0, allowedDurationValues.slice(5).length - 1],
  494. showTickLabels: true,
  495. allowedValues: allowedDurationValues.slice(5),
  496. disabled: !(
  497. hasAccess && performanceSettings[DetectorConfigAdmin.SLOW_DB_ENABLED]
  498. ),
  499. formatLabel: formatDuration,
  500. disabledReason,
  501. },
  502. ],
  503. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_SLOW_DB_QUERY,
  504. },
  505. {
  506. title: IssueTitle.PERFORMANCE_N_PLUS_ONE_API_CALLS,
  507. fields: [
  508. {
  509. name: DetectorConfigCustomer.N_PLUS_API_CALLS_DURATION,
  510. type: 'range',
  511. label: t('Minimum Total Duration'),
  512. defaultValue: 300, // ms
  513. help: t(
  514. 'Setting the value to 300ms, means that an eligible event will be detected as a N+1 API Calls Issue only if the total duration of the involved spans exceeds 300ms'
  515. ),
  516. allowedValues: allowedDurationValues.slice(5),
  517. disabled: !(
  518. hasAccess &&
  519. performanceSettings[DetectorConfigAdmin.N_PLUS_ONE_API_CALLS_ENABLED]
  520. ),
  521. tickValues: [0, allowedDurationValues.slice(5).length - 1],
  522. showTickLabels: true,
  523. formatLabel: formatDuration,
  524. flexibleControlStateSize: true,
  525. disabledReason,
  526. },
  527. ],
  528. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_N_PLUS_ONE_API_CALLS,
  529. },
  530. {
  531. title: IssueTitle.PERFORMANCE_RENDER_BLOCKING_ASSET,
  532. fields: [
  533. {
  534. name: DetectorConfigCustomer.RENDER_BLOCKING_ASSET_RATIO,
  535. type: 'range',
  536. label: t('Minimum FCP Ratio'),
  537. defaultValue: 0.33,
  538. help: t(
  539. 'Setting the value to 33%, means that an eligible event will be detected as a Large Render Blocking Asset Issue only if the duration of the involved span is at least 33% of First Contentful Paint (FCP).'
  540. ),
  541. allowedValues: allowedPercentageValues,
  542. tickValues: [0, allowedPercentageValues.length - 1],
  543. showTickLabels: true,
  544. disabled: !(
  545. hasAccess &&
  546. performanceSettings[DetectorConfigAdmin.RENDER_BLOCK_ASSET_ENABLED]
  547. ),
  548. formatLabel: value => value && formatPercentage(value),
  549. disabledReason,
  550. },
  551. ],
  552. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_RENDER_BLOCKING_ASSET,
  553. },
  554. {
  555. title: IssueTitle.PERFORMANCE_LARGE_HTTP_PAYLOAD,
  556. fields: [
  557. {
  558. name: DetectorConfigCustomer.LARGE_HTT_PAYLOAD_SIZE,
  559. type: 'range',
  560. label: t('Minimum Size'),
  561. defaultValue: 1000000, // 1MB in bytes
  562. help: t(
  563. 'Setting the value to 1MB, means that an eligible event will be detected as a Large HTTP Payload Issue only if the involved HTTP span has a payload size that exceeds 1MB.'
  564. ),
  565. tickValues: [0, allowedSizeValues.slice(1).length - 1],
  566. showTickLabels: true,
  567. allowedValues: allowedSizeValues.slice(1),
  568. disabled: !(
  569. hasAccess &&
  570. performanceSettings[DetectorConfigAdmin.LARGE_HTTP_PAYLOAD_ENABLED]
  571. ),
  572. formatLabel: formatSize,
  573. disabledReason,
  574. },
  575. ],
  576. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_LARGE_HTTP_PAYLOAD,
  577. },
  578. {
  579. title: IssueTitle.PERFORMANCE_DB_MAIN_THREAD,
  580. fields: [
  581. {
  582. name: DetectorConfigCustomer.DB_ON_MAIN_THREAD_DURATION,
  583. type: 'range',
  584. label: t('Frame Rate Drop'),
  585. defaultValue: 16, // ms
  586. help: t(
  587. 'Setting the value to 60fps, means that an eligible event will be detected as a DB on Main Thread Issue only if database spans on the main thread cause frame rate to drop below 60fps.'
  588. ),
  589. tickValues: [0, 3],
  590. showTickLabels: true,
  591. allowedValues: [10, 16, 33, 50], // representation of 100 to 20 fps in milliseconds
  592. disabled: !(
  593. hasAccess && performanceSettings[DetectorConfigAdmin.DB_MAIN_THREAD_ENABLED]
  594. ),
  595. formatLabel: formatFrameRate,
  596. disabledReason,
  597. },
  598. ],
  599. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_DB_MAIN_THREAD,
  600. },
  601. {
  602. title: IssueTitle.PERFORMANCE_FILE_IO_MAIN_THREAD,
  603. fields: [
  604. {
  605. name: DetectorConfigCustomer.FILE_IO_MAIN_THREAD_DURATION,
  606. type: 'range',
  607. label: t('Frame Rate Drop'),
  608. defaultValue: 16, // ms
  609. help: t(
  610. 'Setting the value to 60fps, means that an eligible event will be detected as a File I/O on Main Thread Issue only if File I/O spans on the main thread cause frame rate to drop below 60fps.'
  611. ),
  612. tickValues: [0, 3],
  613. showTickLabels: true,
  614. allowedValues: [10, 16, 33, 50], // representation of 100, 60, 30, 20 fps in milliseconds
  615. disabled: !(
  616. hasAccess && performanceSettings[DetectorConfigAdmin.FILE_IO_ENABLED]
  617. ),
  618. formatLabel: formatFrameRate,
  619. disabledReason,
  620. },
  621. ],
  622. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_FILE_IO_MAIN_THREAD,
  623. },
  624. {
  625. title: IssueTitle.PERFORMANCE_CONSECUTIVE_DB_QUERIES,
  626. fields: [
  627. {
  628. name: DetectorConfigCustomer.CONSECUTIVE_DB_MIN_TIME_SAVED,
  629. type: 'range',
  630. label: t('Minimum Time Saved'),
  631. defaultValue: 100, // ms
  632. help: t(
  633. 'Setting the value to 100ms, means that an eligible event will be detected as a Consecutive DB Queries Issue only if the time saved by parallelizing the queries exceeds 100ms.'
  634. ),
  635. tickValues: [0, allowedDurationValues.slice(0, 23).length - 1],
  636. showTickLabels: true,
  637. allowedValues: allowedDurationValues.slice(0, 23),
  638. disabled: !(
  639. hasAccess && performanceSettings[DetectorConfigAdmin.CONSECUTIVE_DB_ENABLED]
  640. ),
  641. formatLabel: formatDuration,
  642. disabledReason,
  643. },
  644. ],
  645. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_CONSECUTIVE_DB_QUERIES,
  646. },
  647. {
  648. title: IssueTitle.PERFORMANCE_UNCOMPRESSED_ASSET,
  649. fields: [
  650. {
  651. name: DetectorConfigCustomer.UNCOMPRESSED_ASSET_SIZE,
  652. type: 'range',
  653. label: t('Minimum Size'),
  654. defaultValue: 512000, // in kilobytes
  655. help: t(
  656. 'Setting the value to 512KB, means that an eligible event will be detected as an Uncompressed Asset Issue only if the size of the uncompressed asset being transferred exceeds 512KB.'
  657. ),
  658. tickValues: [0, allowedSizeValues.slice(1).length - 1],
  659. showTickLabels: true,
  660. allowedValues: allowedSizeValues.slice(1),
  661. disabled: !(
  662. hasAccess &&
  663. performanceSettings[DetectorConfigAdmin.UNCOMPRESSED_ASSET_ENABLED]
  664. ),
  665. formatLabel: formatSize,
  666. disabledReason,
  667. },
  668. {
  669. name: DetectorConfigCustomer.UNCOMPRESSED_ASSET_DURATION,
  670. type: 'range',
  671. label: t('Minimum Duration'),
  672. defaultValue: 500, // in ms
  673. help: t(
  674. 'Setting the value to 500ms, means that an eligible event will be detected as an Uncompressed Asset Issue only if the duration of the span responsible for transferring the uncompressed asset exceeds 500ms.'
  675. ),
  676. tickValues: [0, allowedDurationValues.slice(5).length - 1],
  677. showTickLabels: true,
  678. allowedValues: allowedDurationValues.slice(5),
  679. disabled: !(
  680. hasAccess &&
  681. performanceSettings[DetectorConfigAdmin.UNCOMPRESSED_ASSET_ENABLED]
  682. ),
  683. formatLabel: formatDuration,
  684. disabledReason,
  685. },
  686. ],
  687. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_UNCOMPRESSED_ASSET,
  688. },
  689. {
  690. title: IssueTitle.PERFORMANCE_CONSECUTIVE_HTTP,
  691. fields: [
  692. {
  693. name: DetectorConfigCustomer.CONSECUTIVE_HTTP_MIN_TIME_SAVED,
  694. type: 'range',
  695. label: t('Minimum Time Saved'),
  696. defaultValue: 2000, // in ms
  697. help: t(
  698. 'Setting the value to 2s, means that an eligible event will be detected as a Consecutive HTTP Issue only if the time saved by parallelizing the http spans exceeds 2s.'
  699. ),
  700. tickValues: [0, allowedDurationValues.slice(14).length - 1],
  701. showTickLabels: true,
  702. allowedValues: allowedDurationValues.slice(14),
  703. disabled: !(
  704. hasAccess &&
  705. performanceSettings[DetectorConfigAdmin.CONSECUTIVE_HTTP_ENABLED]
  706. ),
  707. formatLabel: formatDuration,
  708. disabledReason,
  709. },
  710. ],
  711. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_CONSECUTIVE_HTTP,
  712. },
  713. {
  714. title: IssueTitle.PERFORMANCE_HTTP_OVERHEAD,
  715. fields: [
  716. {
  717. name: DetectorConfigCustomer.HTTP_OVERHEAD_REQUEST_DELAY,
  718. type: 'range',
  719. label: t('Request Delay'),
  720. defaultValue: 500, // in ms
  721. help: t(
  722. 'Setting the value to 500ms, means that the HTTP request delay (wait time) will have to exceed 500ms for an HTTP Overhead issue to be created.'
  723. ),
  724. tickValues: [0, allowedDurationValues.slice(6, 17).length - 1],
  725. showTickLabels: true,
  726. allowedValues: allowedDurationValues.slice(6, 17),
  727. disabled: !(
  728. hasAccess && performanceSettings[DetectorConfigAdmin.HTTP_OVERHEAD_ENABLED]
  729. ),
  730. formatLabel: formatDuration,
  731. disabledReason,
  732. },
  733. ],
  734. initiallyCollapsed: issueType !== IssueType.PERFORMANCE_HTTP_OVERHEAD,
  735. },
  736. ];
  737. };
  738. get retentionPrioritiesFormFields(): Field[] {
  739. return [
  740. {
  741. name: 'boostLatestRelease',
  742. type: 'boolean',
  743. label: retentionPrioritiesLabels.boostLatestRelease,
  744. help: t(
  745. 'Captures more transactions for your new releases as they are being adopted'
  746. ),
  747. getData: this.getRetentionPrioritiesData,
  748. },
  749. {
  750. name: 'boostEnvironments',
  751. type: 'boolean',
  752. label: retentionPrioritiesLabels.boostEnvironments,
  753. help: t(
  754. 'Captures more traces from environments that contain "debug", "dev", "local", "qa", and "test"'
  755. ),
  756. getData: this.getRetentionPrioritiesData,
  757. },
  758. {
  759. name: 'boostLowVolumeTransactions',
  760. type: 'boolean',
  761. label: retentionPrioritiesLabels.boostLowVolumeTransactions,
  762. help: t("Balance high-volume endpoints so they don't drown out low-volume ones"),
  763. getData: this.getRetentionPrioritiesData,
  764. },
  765. {
  766. name: 'ignoreHealthChecks',
  767. type: 'boolean',
  768. label: retentionPrioritiesLabels.ignoreHealthChecks,
  769. help: t('Captures fewer of your health checks transactions'),
  770. getData: this.getRetentionPrioritiesData,
  771. },
  772. ];
  773. }
  774. get initialData() {
  775. const {threshold} = this.state;
  776. return {
  777. threshold: threshold.threshold,
  778. metric: threshold.metric,
  779. };
  780. }
  781. renderBody() {
  782. const {organization, project} = this.props;
  783. const endpoint = `/projects/${organization.slug}/${project.slug}/transaction-threshold/configure/`;
  784. const requiredScopes: Scope[] = ['project:write'];
  785. const params = {orgId: organization.slug, projectId: project.slug};
  786. const projectEndpoint = this.getProjectEndpoint(params);
  787. const performanceIssuesEndpoint = this.getPerformanceIssuesEndpoint(params);
  788. const isSuperUser = isActiveSuperuser();
  789. return (
  790. <Fragment>
  791. <SettingsPageHeader title={t('Performance')} />
  792. <PermissionAlert project={project} />
  793. <Access access={requiredScopes} project={project}>
  794. {({hasAccess}) => (
  795. <Feature features="organizations:insights-initial-modules">
  796. <Form
  797. initialData={this.state.general}
  798. saveOnBlur
  799. apiEndpoint={`/projects/${organization.slug}/${project.slug}/performance/configure/`}
  800. >
  801. <JsonForm
  802. disabled={!hasAccess}
  803. fields={[
  804. {
  805. name: 'enable_images',
  806. type: 'boolean',
  807. label: t('Images'),
  808. help: t('Enables images from real data to be displayed'),
  809. },
  810. ]}
  811. title={t('General')}
  812. />
  813. </Form>
  814. </Feature>
  815. )}
  816. </Access>
  817. <Form
  818. saveOnBlur
  819. allowUndo
  820. initialData={this.initialData}
  821. apiMethod="POST"
  822. apiEndpoint={endpoint}
  823. onSubmitSuccess={resp => {
  824. const initial = this.initialData;
  825. const changedThreshold = initial.metric === resp.metric;
  826. trackAnalytics('performance_views.project_transaction_threshold.change', {
  827. organization,
  828. from: changedThreshold ? initial.threshold : initial.metric,
  829. to: changedThreshold ? resp.threshold : resp.metric,
  830. key: changedThreshold ? 'threshold' : 'metric',
  831. });
  832. this.setState({threshold: resp});
  833. }}
  834. >
  835. <Access access={requiredScopes} project={project}>
  836. {({hasAccess}) => (
  837. <JsonForm
  838. title={t('Threshold Settings')}
  839. fields={this.formFields}
  840. disabled={!hasAccess}
  841. renderFooter={() => (
  842. <Actions>
  843. <Button onClick={() => this.handleDelete()}>{t('Reset All')}</Button>
  844. </Actions>
  845. )}
  846. />
  847. )}
  848. </Access>
  849. </Form>
  850. <Feature features="organizations:dynamic-sampling">
  851. <Form
  852. saveOnBlur
  853. allowUndo
  854. initialData={
  855. project.dynamicSamplingBiases?.reduce((acc, bias) => {
  856. acc[bias.id] = bias.active;
  857. return acc;
  858. }, {}) ?? {}
  859. }
  860. onSubmitSuccess={(response, _instance, id, change) => {
  861. ProjectsStore.onUpdateSuccess(response);
  862. trackAnalytics(
  863. change?.new === true
  864. ? 'dynamic_sampling_settings.priority_enabled'
  865. : 'dynamic_sampling_settings.priority_disabled',
  866. {
  867. organization,
  868. project_id: project.id,
  869. id: id as DynamicSamplingBiasType,
  870. }
  871. );
  872. }}
  873. apiMethod="PUT"
  874. apiEndpoint={projectEndpoint}
  875. >
  876. <Access access={requiredScopes} project={project}>
  877. {({hasAccess}) => (
  878. <JsonForm
  879. title={t('Retention Priorities')}
  880. fields={this.retentionPrioritiesFormFields}
  881. disabled={!hasAccess}
  882. renderFooter={() => (
  883. <Actions>
  884. <Button
  885. external
  886. href="https://docs.sentry.io/product/performance/performance-at-scale/"
  887. >
  888. {t('Read docs')}
  889. </Button>
  890. </Actions>
  891. )}
  892. />
  893. )}
  894. </Access>
  895. </Form>
  896. </Feature>
  897. <Fragment>
  898. <Feature features="organizations:performance-issues-dev">
  899. <Form
  900. saveOnBlur
  901. allowUndo
  902. initialData={{
  903. performanceIssueCreationRate:
  904. this.state.project.performanceIssueCreationRate,
  905. performanceIssueSendToPlatform:
  906. this.state.project.performanceIssueSendToPlatform,
  907. performanceIssueCreationThroughPlatform:
  908. this.state.project.performanceIssueCreationThroughPlatform,
  909. }}
  910. apiMethod="PUT"
  911. apiEndpoint={projectEndpoint}
  912. >
  913. <Access access={requiredScopes} project={project}>
  914. {({hasAccess}) => (
  915. <JsonForm
  916. title={t('Performance Issues - All')}
  917. fields={this.performanceIssueFormFields}
  918. disabled={!hasAccess}
  919. />
  920. )}
  921. </Access>
  922. </Form>
  923. </Feature>
  924. {isSuperUser && (
  925. <Form
  926. saveOnBlur
  927. allowUndo
  928. initialData={this.state.performance_issue_settings}
  929. apiMethod="PUT"
  930. onSubmitError={error => {
  931. if (error.status === 403) {
  932. addErrorMessage(
  933. t(
  934. 'This action requires active super user access. Please re-authenticate to make changes.'
  935. )
  936. );
  937. }
  938. }}
  939. apiEndpoint={performanceIssuesEndpoint}
  940. >
  941. <JsonForm
  942. title={t(
  943. '### INTERNAL ONLY ### - Performance Issues Admin Detector Settings'
  944. )}
  945. fields={this.performanceIssueDetectorAdminFields}
  946. disabled={!isSuperUser}
  947. />
  948. </Form>
  949. )}
  950. <Form
  951. allowUndo
  952. initialData={this.state.performance_issue_settings}
  953. apiMethod="PUT"
  954. apiEndpoint={performanceIssuesEndpoint}
  955. saveOnBlur
  956. onSubmitSuccess={(option: {[key: string]: number}) => {
  957. const [threshold_key, threshold_value] = Object.entries(option)[0];
  958. trackAnalytics(
  959. 'performance_views.project_issue_detection_threshold_changed',
  960. {
  961. organization,
  962. project_slug: project.slug,
  963. threshold_key,
  964. threshold_value,
  965. }
  966. );
  967. }}
  968. >
  969. <Access access={requiredScopes} project={project}>
  970. {({hasAccess}) => (
  971. <div id={projectDetectorSettingsId}>
  972. <StyledPanelHeader>
  973. {t('Performance Issues - Detector Threshold Settings')}
  974. </StyledPanelHeader>
  975. <StyledJsonForm
  976. forms={this.project_owner_detector_settings(hasAccess)}
  977. collapsible
  978. />
  979. <StyledPanelFooter>
  980. <Actions>
  981. <Confirm
  982. message={t(
  983. 'Are you sure you wish to reset all detector thresholds?'
  984. )}
  985. onConfirm={() => this.handleThresholdsReset()}
  986. disabled={!hasAccess || this.areAllConfigurationsDisabled}
  987. >
  988. <Button>{t('Reset All Thresholds')}</Button>
  989. </Confirm>
  990. </Actions>
  991. </StyledPanelFooter>
  992. </div>
  993. )}
  994. </Access>
  995. </Form>
  996. </Fragment>
  997. </Fragment>
  998. );
  999. }
  1000. }
  1001. const Actions = styled(PanelItem)`
  1002. justify-content: flex-end;
  1003. `;
  1004. const StyledPanelHeader = styled(PanelHeader)`
  1005. border: 1px solid ${p => p.theme.border};
  1006. border-bottom: none;
  1007. `;
  1008. const StyledJsonForm = styled(JsonForm)`
  1009. ${Panel} {
  1010. margin-bottom: 0;
  1011. border-radius: 0;
  1012. border-bottom: 0;
  1013. }
  1014. ${FieldWrapper} {
  1015. border-top: 1px solid ${p => p.theme.border};
  1016. }
  1017. ${FieldWrapper} + ${FieldWrapper} {
  1018. border-top: 0;
  1019. }
  1020. ${Panel} + ${Panel} {
  1021. border-top: 1px solid ${p => p.theme.border};
  1022. }
  1023. ${PanelHeader} {
  1024. border-bottom: 0;
  1025. text-transform: none;
  1026. margin-bottom: 0;
  1027. background: none;
  1028. padding: ${space(3)} ${space(2)};
  1029. }
  1030. `;
  1031. const StyledPanelFooter = styled(PanelFooter)`
  1032. background: ${p => p.theme.background};
  1033. border: 1px solid ${p => p.theme.border};
  1034. border-radius: 0 0 calc(${p => p.theme.panelBorderRadius} - 1px)
  1035. calc(${p => p.theme.panelBorderRadius} - 1px);
  1036. ${Actions} {
  1037. padding: ${space(1.5)};
  1038. }
  1039. `;
  1040. const LoadingIndicatorContainer = styled('div')`
  1041. margin: 18px 18px 0;
  1042. `;
  1043. export default ProjectPerformance;