projectStabilityScoreCard.tsx 4.9 KB

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