projectAnrScoreCard.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import {Fragment, useEffect, useState} from 'react';
  2. import {Location} from 'history';
  3. import pick from 'lodash/pick';
  4. import round from 'lodash/round';
  5. import {doSessionsRequest} from 'sentry/actionCreators/sessions';
  6. import {Button} from 'sentry/components/button';
  7. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  8. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  9. import {parseStatsPeriod} from 'sentry/components/organizations/timeRangeSelector/utils';
  10. import ScoreCard from 'sentry/components/scoreCard';
  11. import {URL_PARAM} from 'sentry/constants/pageFilters';
  12. import {IconArrow} from 'sentry/icons/iconArrow';
  13. import {t} from 'sentry/locale';
  14. import {PageFilters} from 'sentry/types';
  15. import {Organization, SessionApiResponse} from 'sentry/types/organization';
  16. import {trackAnalytics} from 'sentry/utils/analytics';
  17. import {formatAbbreviatedNumber, formatPercentage} from 'sentry/utils/formatters';
  18. import {getPeriod} from 'sentry/utils/getPeriod';
  19. import useApi from 'sentry/utils/useApi';
  20. import {
  21. getSessionTermDescription,
  22. SessionTerm,
  23. } from 'sentry/views/releases/utils/sessionTerm';
  24. type Props = {
  25. isProjectStabilized: boolean;
  26. location: Location;
  27. organization: Organization;
  28. selection: PageFilters;
  29. query?: string;
  30. };
  31. export function ProjectAnrScoreCard({
  32. isProjectStabilized,
  33. organization,
  34. selection,
  35. location,
  36. query,
  37. }: Props) {
  38. const {environments, projects, datetime} = selection;
  39. const {start, end, period} = datetime;
  40. const api = useApi();
  41. const [sessionsData, setSessionsData] = useState<SessionApiResponse | null>(null);
  42. const [previousSessionData, setPreviousSessionsData] =
  43. useState<SessionApiResponse | null>(null);
  44. useEffect(() => {
  45. let unmounted = false;
  46. const requestData = {
  47. orgSlug: organization.slug,
  48. field: ['anr_rate()'],
  49. environment: environments,
  50. project: projects,
  51. query,
  52. includeSeries: false,
  53. };
  54. doSessionsRequest(api, {...requestData, ...normalizeDateTimeParams(datetime)}).then(
  55. response => {
  56. if (unmounted) {
  57. return;
  58. }
  59. setSessionsData(response);
  60. }
  61. );
  62. return () => {
  63. unmounted = true;
  64. };
  65. }, [api, datetime, environments, organization.slug, projects, query]);
  66. useEffect(() => {
  67. let unmounted = false;
  68. if (
  69. !shouldFetchPreviousPeriod({
  70. start,
  71. end,
  72. period,
  73. })
  74. ) {
  75. setPreviousSessionsData(null);
  76. } else {
  77. const requestData = {
  78. orgSlug: organization.slug,
  79. field: ['anr_rate()'],
  80. environment: environments,
  81. project: projects,
  82. query,
  83. includeSeries: false,
  84. };
  85. const {start: previousStart} = parseStatsPeriod(
  86. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: true})
  87. .statsPeriod!
  88. );
  89. const {start: previousEnd} = parseStatsPeriod(
  90. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: false})
  91. .statsPeriod!
  92. );
  93. doSessionsRequest(api, {
  94. ...requestData,
  95. start: previousStart,
  96. end: previousEnd,
  97. }).then(response => {
  98. if (unmounted) {
  99. return;
  100. }
  101. setPreviousSessionsData(response);
  102. });
  103. }
  104. return () => {
  105. unmounted = true;
  106. };
  107. }, [start, end, period, api, organization.slug, environments, projects, query]);
  108. const value = sessionsData?.groups?.[0]?.totals['anr_rate()'] ?? null;
  109. const previousValue = previousSessionData?.groups?.[0]?.totals['anr_rate()'] ?? null;
  110. const hasCurrentAndPrevious = previousValue && value;
  111. const trend = hasCurrentAndPrevious ? round(value - previousValue, 4) : null;
  112. const trendStatus = !trend ? undefined : trend < 0 ? 'good' : 'bad';
  113. if (!isProjectStabilized) {
  114. return null;
  115. }
  116. function renderTrend() {
  117. return trend ? (
  118. <Fragment>
  119. {trend >= 0 ? (
  120. <IconArrow direction="up" size="xs" />
  121. ) : (
  122. <IconArrow direction="down" size="xs" />
  123. )}
  124. {`${formatAbbreviatedNumber(Math.abs(trend))}\u0025`}
  125. </Fragment>
  126. ) : null;
  127. }
  128. const endpointPath = `/organizations/${organization.slug}/issues/`;
  129. const issueQuery = ['mechanism:[ANR,AppExitInfo]', query].join(' ').trim();
  130. const queryParams = {
  131. ...normalizeDateTimeParams(pick(location.query, [...Object.values(URL_PARAM)])),
  132. query: issueQuery,
  133. sort: 'freq',
  134. };
  135. const issueSearch = {
  136. pathname: endpointPath,
  137. query: queryParams,
  138. };
  139. function renderButton() {
  140. return (
  141. <Button
  142. data-test-id="issues-open"
  143. size="xs"
  144. to={issueSearch}
  145. onClick={() => {
  146. trackAnalytics('project_detail.open_anr_issues', {
  147. organization,
  148. });
  149. }}
  150. >
  151. {t('View Issues')}
  152. </Button>
  153. );
  154. }
  155. return (
  156. <ScoreCard
  157. title={t('ANR Rate')}
  158. help={getSessionTermDescription(SessionTerm.ANR_RATE, null)}
  159. score={value ? formatPercentage(value, 3) : '\u2014'}
  160. trend={renderTrend()}
  161. trendStatus={trendStatus}
  162. renderOpenButton={renderButton}
  163. />
  164. );
  165. }