projectSessionsAnrRequest.tsx 6.8 KB

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