projectStabilityScoreCard.tsx 4.9 KB

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