projectApdexScoreCard.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import {Fragment} from 'react';
  2. import round from 'lodash/round';
  3. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  4. import Count from 'sentry/components/count';
  5. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  6. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  7. import ScoreCard from 'sentry/components/scoreCard';
  8. import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
  9. import {IconArrow} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import type {PageFilters} from 'sentry/types/core';
  12. import type {Organization} from 'sentry/types/organization';
  13. import {defined} from 'sentry/utils';
  14. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  15. import {getPeriod} from 'sentry/utils/duration/getPeriod';
  16. import {getTermHelp, PerformanceTerm} from 'sentry/views/performance/data';
  17. import MissingPerformanceButtons from '../missingFeatureButtons/missingPerformanceButtons';
  18. type Props = DeprecatedAsyncComponent['props'] & {
  19. isProjectStabilized: boolean;
  20. organization: Organization;
  21. selection: PageFilters;
  22. hasTransactions?: boolean;
  23. query?: string;
  24. };
  25. type State = DeprecatedAsyncComponent['state'] & {
  26. currentApdex: TableData | null;
  27. previousApdex: TableData | null;
  28. };
  29. class ProjectApdexScoreCard extends DeprecatedAsyncComponent<Props, State> {
  30. shouldRenderBadRequests = true;
  31. getDefaultState() {
  32. return {
  33. ...super.getDefaultState(),
  34. currentApdex: null,
  35. previousApdex: null,
  36. };
  37. }
  38. getEndpoints() {
  39. const {organization, selection, isProjectStabilized, hasTransactions, query} =
  40. this.props;
  41. if (!this.hasFeature() || !isProjectStabilized || !hasTransactions) {
  42. return [];
  43. }
  44. const {projects, environments, datetime} = selection;
  45. const {period} = datetime;
  46. const commonQuery = {
  47. environment: environments,
  48. project: projects.map(proj => String(proj)),
  49. field: ['apdex()'],
  50. query: ['event.type:transaction count():>0', query].join(' ').trim(),
  51. };
  52. const endpoints: ReturnType<DeprecatedAsyncComponent['getEndpoints']> = [
  53. [
  54. 'currentApdex',
  55. `/organizations/${organization.slug}/events/`,
  56. {query: {...commonQuery, ...normalizeDateTimeParams(datetime)}},
  57. ],
  58. ];
  59. if (
  60. shouldFetchPreviousPeriod({
  61. start: datetime.start,
  62. end: datetime.end,
  63. period: datetime.period,
  64. })
  65. ) {
  66. const {start: previousStart} = parseStatsPeriod(
  67. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: true})
  68. .statsPeriod!
  69. );
  70. const {start: previousEnd} = parseStatsPeriod(
  71. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: false})
  72. .statsPeriod!
  73. );
  74. endpoints.push([
  75. 'previousApdex',
  76. `/organizations/${organization.slug}/events/`,
  77. {query: {...commonQuery, start: previousStart, end: previousEnd}},
  78. ]);
  79. }
  80. return endpoints;
  81. }
  82. componentDidUpdate(prevProps: Props) {
  83. const {selection, isProjectStabilized, hasTransactions, query} = this.props;
  84. if (
  85. prevProps.selection !== selection ||
  86. prevProps.hasTransactions !== hasTransactions ||
  87. prevProps.isProjectStabilized !== isProjectStabilized ||
  88. prevProps.query !== query
  89. ) {
  90. this.remountComponent();
  91. }
  92. }
  93. hasFeature() {
  94. return this.props.organization.features.includes('performance-view');
  95. }
  96. get cardTitle() {
  97. return t('Apdex');
  98. }
  99. get cardHelp() {
  100. const {organization} = this.props;
  101. const baseHelp = getTermHelp(organization, PerformanceTerm.APDEX);
  102. if (this.trend) {
  103. return baseHelp + t(' This shows how it has changed since the last period.');
  104. }
  105. return baseHelp;
  106. }
  107. get currentApdex() {
  108. const {currentApdex} = this.state;
  109. const apdex = currentApdex?.data[0]?.['apdex()'];
  110. return typeof apdex === 'undefined' ? undefined : Number(apdex);
  111. }
  112. get previousApdex() {
  113. const {previousApdex} = this.state;
  114. const apdex = previousApdex?.data[0]?.['apdex()'];
  115. return typeof apdex === 'undefined' ? undefined : Number(apdex);
  116. }
  117. get trend() {
  118. if (this.currentApdex && this.previousApdex) {
  119. return round(this.currentApdex - this.previousApdex, 3);
  120. }
  121. return null;
  122. }
  123. get trendStatus(): React.ComponentProps<typeof ScoreCard>['trendStatus'] {
  124. if (!this.trend) {
  125. return undefined;
  126. }
  127. return this.trend > 0 ? 'good' : 'bad';
  128. }
  129. renderLoading() {
  130. return this.renderBody();
  131. }
  132. renderMissingFeatureCard() {
  133. const {organization} = this.props;
  134. return (
  135. <ScoreCard
  136. title={this.cardTitle}
  137. help={this.cardHelp}
  138. score={<MissingPerformanceButtons organization={organization} />}
  139. />
  140. );
  141. }
  142. renderScore() {
  143. return defined(this.currentApdex) ? <Count value={this.currentApdex} /> : '\u2014';
  144. }
  145. renderTrend() {
  146. // we want to show trend only after currentApdex has loaded to prevent jumping
  147. return defined(this.currentApdex) && defined(this.trend) ? (
  148. <Fragment>
  149. {this.trend >= 0 ? (
  150. <IconArrow direction="up" size="xs" />
  151. ) : (
  152. <IconArrow direction="down" size="xs" />
  153. )}
  154. <Count value={Math.abs(this.trend)} />
  155. </Fragment>
  156. ) : null;
  157. }
  158. renderBody() {
  159. const {hasTransactions} = this.props;
  160. if (!this.hasFeature() || hasTransactions === false) {
  161. return this.renderMissingFeatureCard();
  162. }
  163. return (
  164. <ScoreCard
  165. title={this.cardTitle}
  166. help={this.cardHelp}
  167. score={this.renderScore()}
  168. trend={this.renderTrend()}
  169. trendStatus={this.trendStatus}
  170. />
  171. );
  172. }
  173. }
  174. export default ProjectApdexScoreCard;