projectAnrScoreCard.tsx 5.2 KB

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