projectApdexScoreCard.tsx 5.6 KB

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