projectStabilityScoreCard.tsx 5.6 KB

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