projectStabilityScoreCard.tsx 4.9 KB

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