projectStabilityScoreCard.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {Button} from 'sentry/components/button';
  2. import {getInterval, shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  3. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  4. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  5. import {t} from 'sentry/locale';
  6. import type {PageFilters} from 'sentry/types/core';
  7. import type {SessionApiResponse} from 'sentry/types/organization';
  8. import {SessionFieldWithOperation} from 'sentry/types/organization';
  9. import type {Project} from 'sentry/types/project';
  10. import {defined} from 'sentry/utils';
  11. import {getPeriod} from 'sentry/utils/duration/getPeriod';
  12. import {useApiQuery} from 'sentry/utils/queryClient';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization';
  15. import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
  16. import {
  17. getSessionTermDescription,
  18. SessionTerm,
  19. } from 'sentry/views/releases/utils/sessionTerm';
  20. import MissingReleasesButtons from '../missingFeatureButtons/missingReleasesButtons';
  21. import {ActionWrapper} from './actionWrapper';
  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: getInterval(selection.datetime),
  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: Infinity, 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: Infinity,
  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 Title = <Widget.WidgetTitle title={cardTitle} />;
  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. <Widget
  121. Title={Title}
  122. Actions={
  123. <Widget.WidgetToolbar>
  124. <Widget.WidgetDescription description={cardHelp} />
  125. </Widget.WidgetToolbar>
  126. }
  127. Visualization={
  128. <ActionWrapper>
  129. <MissingReleasesButtons
  130. organization={organization}
  131. health
  132. platform={props.project?.platform}
  133. />
  134. </ActionWrapper>
  135. }
  136. />
  137. );
  138. }
  139. if (isLoading || !defined(score)) {
  140. return (
  141. <Widget
  142. Title={Title}
  143. Visualization={<BigNumberWidgetVisualization.LoadingPlaceholder />}
  144. />
  145. );
  146. }
  147. if (error) {
  148. return (
  149. <Widget
  150. Title={Title}
  151. Actions={
  152. <Widget.WidgetToolbar>
  153. <Button size="xs" onClick={refetch}>
  154. {t('Retry')}
  155. </Button>
  156. </Widget.WidgetToolbar>
  157. }
  158. Visualization={<Widget.WidgetError error={error} />}
  159. />
  160. );
  161. }
  162. return (
  163. <Widget
  164. Title={Title}
  165. Actions={
  166. <Widget.WidgetToolbar>
  167. <Widget.WidgetDescription description={cardHelp} />
  168. </Widget.WidgetToolbar>
  169. }
  170. Visualization={
  171. <BigNumberWidgetVisualization
  172. value={score / 100}
  173. previousPeriodValue={previousScore ? previousScore / 100 : undefined}
  174. field={`${props.field}()`}
  175. meta={{
  176. fields: {
  177. [`${props.field}()`]: 'percentage',
  178. },
  179. units: {},
  180. }}
  181. preferredPolarity="+"
  182. />
  183. }
  184. />
  185. );
  186. }
  187. export default ProjectStabilityScoreCard;