projectStabilityScoreCard.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import round from 'lodash/round';
  2. import {
  3. getDiffInMinutes,
  4. shouldFetchPreviousPeriod,
  5. } from 'sentry/components/charts/utils';
  6. import LoadingError from 'sentry/components/loadingError';
  7. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  8. import ScoreCard from 'sentry/components/scoreCard';
  9. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  10. import {IconArrow} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import type {PageFilters, SessionApiResponse} from 'sentry/types';
  13. import {SessionFieldWithOperation} from 'sentry/types';
  14. import {defined} from 'sentry/utils';
  15. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  16. import {getPeriod} from 'sentry/utils/getPeriod';
  17. import {useApiQuery} from 'sentry/utils/queryClient';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import {displayCrashFreePercent} from 'sentry/views/releases/utils';
  20. import {
  21. getSessionTermDescription,
  22. SessionTerm,
  23. } from 'sentry/views/releases/utils/sessionTerm';
  24. import MissingReleasesButtons from '../missingFeatureButtons/missingReleasesButtons';
  25. type Props = {
  26. field:
  27. | SessionFieldWithOperation.CRASH_FREE_RATE_SESSIONS
  28. | SessionFieldWithOperation.CRASH_FREE_RATE_USERS;
  29. hasSessions: boolean | null;
  30. isProjectStabilized: boolean;
  31. selection: PageFilters;
  32. query?: string;
  33. };
  34. const useCrashFreeRate = (props: Props) => {
  35. const organization = useOrganization();
  36. const {selection, isProjectStabilized, hasSessions, query, field} = props;
  37. const isEnabled = !!(isProjectStabilized && hasSessions);
  38. const {projects, environments: environment, datetime} = selection;
  39. const {period} = datetime;
  40. const doubledPeriod = getPeriod(
  41. {period, start: undefined, end: undefined},
  42. {shouldDoublePeriod: true}
  43. ).statsPeriod;
  44. const commonQuery = {
  45. environment,
  46. project: projects[0],
  47. interval: getDiffInMinutes(datetime) > 24 * 60 ? '1d' : '1h',
  48. query,
  49. field,
  50. };
  51. // Unfortunately we can't do something like statsPeriod=28d&interval=14d to get scores for this and previous interval with the single request
  52. // https://github.com/getsentry/sentry/pull/22770#issuecomment-758595553
  53. const currentQuery = useApiQuery<SessionApiResponse>(
  54. [
  55. `/organizations/${organization.slug}/sessions/`,
  56. {
  57. query: {
  58. ...commonQuery,
  59. ...normalizeDateTimeParams(datetime),
  60. },
  61. },
  62. ],
  63. {staleTime: 0, enabled: isEnabled}
  64. );
  65. const isPreviousPeriodEnabled = shouldFetchPreviousPeriod({
  66. start: datetime.start,
  67. end: datetime.end,
  68. period: datetime.period,
  69. });
  70. const previousQuery = useApiQuery<SessionApiResponse>(
  71. [
  72. `/organizations/${organization.slug}/sessions/`,
  73. {
  74. query: {
  75. ...commonQuery,
  76. statsPeriodStart: doubledPeriod,
  77. statsPeriodEnd: period ?? DEFAULT_STATS_PERIOD,
  78. },
  79. },
  80. ],
  81. {
  82. staleTime: 0,
  83. enabled: isEnabled && isPreviousPeriodEnabled,
  84. }
  85. );
  86. return {
  87. crashFreeRate: currentQuery.data,
  88. previousCrashFreeRate: previousQuery.data,
  89. isLoading:
  90. currentQuery.isLoading || (previousQuery.isLoading && isPreviousPeriodEnabled),
  91. error: currentQuery.error || previousQuery.error,
  92. refetch: () => {
  93. currentQuery.refetch();
  94. previousQuery.refetch();
  95. },
  96. };
  97. };
  98. // shouldRenderBadRequests = true;
  99. function ProjectStabilityScoreCard(props: Props) {
  100. const {hasSessions} = props;
  101. const organization = useOrganization();
  102. const cardTitle =
  103. props.field === SessionFieldWithOperation.CRASH_FREE_RATE_SESSIONS
  104. ? t('Crash Free Sessions')
  105. : t('Crash Free Users');
  106. const cardHelp = getSessionTermDescription(
  107. props.field === SessionFieldWithOperation.CRASH_FREE_RATE_SESSIONS
  108. ? SessionTerm.CRASH_FREE_SESSIONS
  109. : SessionTerm.CRASH_FREE_USERS,
  110. null
  111. );
  112. const {crashFreeRate, previousCrashFreeRate, isLoading, error, refetch} =
  113. useCrashFreeRate(props);
  114. const score = !crashFreeRate
  115. ? undefined
  116. : crashFreeRate?.groups[0]?.totals[props.field] * 100;
  117. const previousScore = !previousCrashFreeRate
  118. ? undefined
  119. : previousCrashFreeRate?.groups[0]?.totals[props.field] * 100;
  120. const trend =
  121. defined(score) && defined(previousScore)
  122. ? round(score - previousScore, 3)
  123. : undefined;
  124. const shouldRenderTrend = !isLoading && defined(score) && defined(trend);
  125. if (hasSessions === false) {
  126. return (
  127. <ScoreCard
  128. title={cardTitle}
  129. help={cardHelp}
  130. score={<MissingReleasesButtons organization={organization} health />}
  131. />
  132. );
  133. }
  134. if (error) {
  135. return (
  136. <LoadingError
  137. message={
  138. (error.responseJSON?.detail as React.ReactNode) ||
  139. t('There was an error loading data.')
  140. }
  141. onRetry={refetch}
  142. />
  143. );
  144. }
  145. return (
  146. <ScoreCard
  147. title={cardTitle}
  148. help={cardHelp}
  149. score={isLoading || !defined(score) ? '\u2014' : displayCrashFreePercent(score)}
  150. trend={
  151. shouldRenderTrend ? (
  152. <div>
  153. {trend >= 0 ? (
  154. <IconArrow direction="up" size="xs" />
  155. ) : (
  156. <IconArrow direction="down" size="xs" />
  157. )}
  158. {`${formatAbbreviatedNumber(Math.abs(trend))}\u0025`}
  159. </div>
  160. ) : null
  161. }
  162. trendStatus={!trend ? undefined : trend > 0 ? 'good' : 'bad'}
  163. />
  164. );
  165. }
  166. export default ProjectStabilityScoreCard;