projectSessionsAnrRequest.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import {Fragment, useEffect, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import type {LineSeriesOption} from 'echarts';
  4. import LineSeries from 'sentry/components/charts/series/lineSeries';
  5. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  6. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  7. import {t} from 'sentry/locale';
  8. import type {Series} from 'sentry/types/echarts';
  9. import type {SessionApiResponse} from 'sentry/types/organization';
  10. import {defined} from 'sentry/utils';
  11. import {getPeriod} from 'sentry/utils/getPeriod';
  12. import {filterSessionsInTimeWindow, getSessionsInterval} from 'sentry/utils/sessions';
  13. import useApiRequests from 'sentry/utils/useApiRequests';
  14. import {DisplayModes} from '../projectCharts';
  15. import type {ProjectSessionsChartRequestProps} from './projectSessionsChartRequest';
  16. const BAD_BEHAVIOUR_THRESHOLD = 0.47;
  17. type State = {
  18. sessionsData: SessionApiResponse;
  19. };
  20. function ProjectSessionsAnrRequest({
  21. children,
  22. organization,
  23. disablePrevious,
  24. selection,
  25. displayMode,
  26. query,
  27. onTotalValuesChange,
  28. }: Omit<ProjectSessionsChartRequestProps, 'theme'>) {
  29. const {datetime, projects, environments: environment} = selection;
  30. const theme = useTheme();
  31. const yAxis =
  32. displayMode === DisplayModes.ANR_RATE ? 'anr_rate()' : 'foreground_anr_rate()';
  33. const [timeseriesData, setTimeseriesData] = useState<Series[] | null>(null);
  34. const [badBehaviourSeries, setBadBehaviourSeries] = useState<LineSeriesOption[] | null>(
  35. null
  36. );
  37. const [previousTimeseriesData, setPreviousTimeseriesData] = useState<Series | null>(
  38. null
  39. );
  40. const [totalSessions, setTotalSessions] = useState<number | null>(null);
  41. const shouldFetchWithPrevious =
  42. !disablePrevious &&
  43. shouldFetchPreviousPeriod({
  44. start: datetime.start,
  45. end: datetime.end,
  46. period: datetime.period,
  47. });
  48. function getParams(): Record<string, any> {
  49. const baseParams = {
  50. field: [yAxis, 'count_unique(user)'],
  51. interval: getSessionsInterval(datetime, {
  52. highFidelity: organization.features.includes('minute-resolution-sessions'),
  53. dailyInterval: true,
  54. }),
  55. project: projects[0],
  56. environment,
  57. query,
  58. };
  59. if (!shouldFetchWithPrevious) {
  60. return {
  61. ...baseParams,
  62. ...normalizeDateTimeParams(datetime),
  63. };
  64. }
  65. const {period} = selection.datetime;
  66. const doubledPeriod = getPeriod(
  67. {period, start: undefined, end: undefined},
  68. {shouldDoublePeriod: true}
  69. ).statsPeriod;
  70. return {
  71. ...baseParams,
  72. statsPeriod: doubledPeriod,
  73. };
  74. }
  75. const queryParams = getParams();
  76. const {data, isReloading, hasError} = useApiRequests<State>({
  77. endpoints: [
  78. [
  79. 'sessionsData',
  80. `/organizations/${organization.slug}/sessions/`,
  81. {query: queryParams},
  82. ],
  83. ],
  84. });
  85. useEffect(() => {
  86. if (defined(data.sessionsData)) {
  87. const filteredResponse = filterSessionsInTimeWindow(
  88. data.sessionsData,
  89. queryParams.start,
  90. queryParams.end
  91. );
  92. const dataMiddleIndex = Math.floor(filteredResponse.intervals.length / 2);
  93. const totalUsers = filteredResponse.groups.reduce(
  94. (acc, group) =>
  95. acc +
  96. group.series['count_unique(user)']
  97. .slice(shouldFetchWithPrevious ? dataMiddleIndex : 0)
  98. .reduce((value, groupAcc) => groupAcc + value, 0),
  99. 0
  100. );
  101. setTotalSessions(totalUsers);
  102. onTotalValuesChange(totalUsers);
  103. const previousPeriodTotalUsers = filteredResponse
  104. ? filteredResponse.groups.reduce(
  105. (acc, group) =>
  106. acc +
  107. group.series['count_unique(user)']
  108. .slice(0, dataMiddleIndex)
  109. .reduce((value, groupAcc) => groupAcc + value, 0),
  110. 0
  111. )
  112. : 0;
  113. const timeseriesData_ = [
  114. {
  115. seriesName: t('This Period'),
  116. data: filteredResponse.intervals
  117. .slice(shouldFetchWithPrevious ? dataMiddleIndex : 0)
  118. .map((interval, i) => {
  119. const anrRate = filteredResponse.groups.reduce(
  120. (acc, group) =>
  121. acc +
  122. group.series[yAxis]?.slice(
  123. shouldFetchWithPrevious ? dataMiddleIndex : 0
  124. )[i],
  125. 0
  126. );
  127. return {
  128. name: interval,
  129. value:
  130. totalUsers === 0 && previousPeriodTotalUsers === 0
  131. ? 0
  132. : anrRate === null
  133. ? null
  134. : anrRate * 100,
  135. };
  136. }),
  137. },
  138. ] as Series[];
  139. const badBehaviourSeries_ =
  140. yAxis === 'foreground_anr_rate()'
  141. ? [
  142. LineSeries({
  143. name: t('Overall Bad Behaviour Threshold'),
  144. data: filteredResponse.intervals
  145. .slice(shouldFetchWithPrevious ? dataMiddleIndex : 0)
  146. .map(interval => [interval, BAD_BEHAVIOUR_THRESHOLD]),
  147. lineStyle: {color: theme.red200, width: 2, type: 'dotted'},
  148. itemStyle: {color: theme.red200},
  149. animation: false,
  150. stack: 'bad_behaviour_threshold',
  151. }),
  152. ]
  153. : null;
  154. const previousTimeseriesData_ = shouldFetchWithPrevious
  155. ? ({
  156. seriesName: t('Previous Period'),
  157. data: filteredResponse.intervals
  158. .slice(0, dataMiddleIndex)
  159. .map((_interval, i) => {
  160. const previousAnrRate = filteredResponse.groups.reduce(
  161. (acc, group) => acc + group.series[yAxis]?.slice(0, dataMiddleIndex)[i],
  162. 0
  163. );
  164. return {
  165. name: filteredResponse.intervals[i + dataMiddleIndex],
  166. value:
  167. totalUsers === 0 && previousPeriodTotalUsers === 0
  168. ? 0
  169. : previousAnrRate === null
  170. ? null
  171. : previousAnrRate * 100,
  172. };
  173. }),
  174. } as Series) // TODO(project-detail): Change SeriesDataUnit value to support null
  175. : null;
  176. setTimeseriesData(timeseriesData_);
  177. setPreviousTimeseriesData(previousTimeseriesData_);
  178. setBadBehaviourSeries(badBehaviourSeries_);
  179. }
  180. }, [
  181. data.sessionsData,
  182. onTotalValuesChange,
  183. queryParams.end,
  184. queryParams.start,
  185. shouldFetchWithPrevious,
  186. theme.red200,
  187. yAxis,
  188. ]);
  189. return (
  190. <Fragment>
  191. {children({
  192. loading: timeseriesData === null,
  193. reloading: isReloading,
  194. errored: hasError,
  195. totalSessions,
  196. previousTimeseriesData,
  197. timeseriesData: timeseriesData ?? [],
  198. additionalSeries: badBehaviourSeries ?? undefined,
  199. })}
  200. </Fragment>
  201. );
  202. }
  203. export default ProjectSessionsAnrRequest;