projectSessionsAnrRequest.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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/duration/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)']!.slice(
  91. 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)']!.slice(0, dataMiddleIndex).reduce(
  102. (value, groupAcc) => groupAcc + value,
  103. 0
  104. ),
  105. 0
  106. )
  107. : 0;
  108. const timeseriesData_ = [
  109. {
  110. seriesName: t('This Period'),
  111. data: filteredResponse.intervals
  112. .slice(shouldFetchWithPrevious ? dataMiddleIndex : 0)
  113. .map((interval, i) => {
  114. const anrRate = filteredResponse.groups.reduce(
  115. (acc, group) =>
  116. acc +
  117. group.series[yAxis]?.slice(
  118. shouldFetchWithPrevious ? dataMiddleIndex : 0
  119. )[i]!,
  120. 0
  121. );
  122. return {
  123. name: interval,
  124. value:
  125. totalUsers === 0 && previousPeriodTotalUsers === 0
  126. ? 0
  127. : anrRate === null
  128. ? null
  129. : anrRate * 100,
  130. };
  131. }),
  132. },
  133. ] as Series[];
  134. const badBehaviourSeries_ =
  135. yAxis === 'foreground_anr_rate()'
  136. ? [
  137. LineSeries({
  138. name: t('Overall Bad Behaviour Threshold'),
  139. data: filteredResponse.intervals
  140. .slice(shouldFetchWithPrevious ? dataMiddleIndex : 0)
  141. .map(interval => [interval, BAD_BEHAVIOUR_THRESHOLD]),
  142. lineStyle: {color: theme.red200, width: 2, type: 'dotted'},
  143. itemStyle: {color: theme.red200},
  144. animation: false,
  145. stack: 'bad_behaviour_threshold',
  146. }),
  147. ]
  148. : null;
  149. const previousTimeseriesData_ = shouldFetchWithPrevious
  150. ? ({
  151. seriesName: t('Previous Period'),
  152. data: filteredResponse.intervals
  153. .slice(0, dataMiddleIndex)
  154. .map((_interval, i) => {
  155. const previousAnrRate = filteredResponse.groups.reduce(
  156. (acc, group) =>
  157. acc + group.series[yAxis]?.slice(0, dataMiddleIndex)[i]!,
  158. 0
  159. );
  160. return {
  161. name: filteredResponse.intervals[i + dataMiddleIndex],
  162. value:
  163. totalUsers === 0 && previousPeriodTotalUsers === 0
  164. ? 0
  165. : previousAnrRate === null
  166. ? null
  167. : previousAnrRate * 100,
  168. };
  169. }),
  170. } as Series) // TODO(project-detail): Change SeriesDataUnit value to support null
  171. : null;
  172. setTimeseriesData(timeseriesData_);
  173. setPreviousTimeseriesData(previousTimeseriesData_);
  174. setBadBehaviourSeries(badBehaviourSeries_);
  175. }
  176. }, [
  177. data,
  178. onTotalValuesChange,
  179. queryParams.end,
  180. queryParams.start,
  181. shouldFetchWithPrevious,
  182. theme.red200,
  183. yAxis,
  184. ]);
  185. return (
  186. <Fragment>
  187. {children({
  188. loading: timeseriesData === null,
  189. reloading: isRefetching,
  190. errored: isError,
  191. totalSessions,
  192. previousTimeseriesData,
  193. timeseriesData: timeseriesData ?? [],
  194. additionalSeries: badBehaviourSeries ?? undefined,
  195. })}
  196. </Fragment>
  197. );
  198. }
  199. export default ProjectSessionsAnrRequest;