projectStabilityScoreCard.tsx 5.6 KB

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