sessionsRequest.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import * as React from 'react';
  2. import {withTheme} from '@emotion/react';
  3. import isEqual from 'lodash/isEqual';
  4. import omit from 'lodash/omit';
  5. import {addErrorMessage} from 'app/actionCreators/indicator';
  6. import {Client} from 'app/api';
  7. import {getSeriesApiInterval} from 'app/components/charts/utils';
  8. import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
  9. import {t} from 'app/locale';
  10. import {
  11. GlobalSelection,
  12. Organization,
  13. SessionApiResponse,
  14. SessionField,
  15. SessionStatus,
  16. } from 'app/types';
  17. import {Series} from 'app/types/echarts';
  18. import {percent} from 'app/utils';
  19. import {getPeriod} from 'app/utils/getPeriod';
  20. import {getCount, getCountSeries, initSessionsChart} from 'app/utils/sessions';
  21. import {Theme} from 'app/utils/theme';
  22. import {getCrashFreePercent} from 'app/views/releases/utils';
  23. import {DisplayModes} from '../projectCharts';
  24. import {shouldFetchPreviousPeriod} from '../utils';
  25. const omitIgnoredProps = (props: Props) =>
  26. omit(props, ['api', 'organization', 'children', 'selection.datetime.utc']);
  27. type ReleaseStatsRequestRenderProps = {
  28. loading: boolean;
  29. reloading: boolean;
  30. errored: boolean;
  31. timeseriesData: Series[];
  32. previousTimeseriesData: Series | null;
  33. totalSessions: number | null;
  34. };
  35. type Props = {
  36. api: Client;
  37. organization: Organization;
  38. selection: GlobalSelection;
  39. children: (renderProps: ReleaseStatsRequestRenderProps) => React.ReactNode;
  40. onTotalValuesChange: (value: number | null) => void;
  41. displayMode: DisplayModes.SESSIONS | DisplayModes.STABILITY;
  42. theme: Theme;
  43. disablePrevious?: boolean;
  44. query?: string;
  45. };
  46. type State = {
  47. reloading: boolean;
  48. errored: boolean;
  49. timeseriesData: Series[] | null;
  50. previousTimeseriesData: Series | null;
  51. totalSessions: number | null;
  52. };
  53. class SessionsRequest extends React.Component<Props, State> {
  54. state: State = {
  55. reloading: false,
  56. errored: false,
  57. timeseriesData: null,
  58. previousTimeseriesData: null,
  59. totalSessions: null,
  60. };
  61. componentDidMount() {
  62. this.fetchData();
  63. }
  64. componentDidUpdate(prevProps: Props) {
  65. if (!isEqual(omitIgnoredProps(this.props), omitIgnoredProps(prevProps))) {
  66. this.fetchData();
  67. }
  68. }
  69. componentWillUnmount() {
  70. this.unmounting = true;
  71. }
  72. private unmounting: boolean = false;
  73. fetchData = async () => {
  74. const {api, selection, onTotalValuesChange, displayMode, disablePrevious} =
  75. this.props;
  76. const shouldFetchWithPrevious =
  77. !disablePrevious && shouldFetchPreviousPeriod(selection.datetime);
  78. this.setState(state => ({
  79. reloading: state.timeseriesData !== null,
  80. errored: false,
  81. }));
  82. try {
  83. const response: SessionApiResponse = await api.requestPromise(this.path, {
  84. query: this.queryParams({shouldFetchWithPrevious}),
  85. });
  86. const {timeseriesData, previousTimeseriesData, totalSessions} =
  87. displayMode === DisplayModes.SESSIONS
  88. ? this.transformSessionCountData(response)
  89. : this.transformData(response, {fetchedWithPrevious: shouldFetchWithPrevious});
  90. if (this.unmounting) {
  91. return;
  92. }
  93. this.setState({
  94. reloading: false,
  95. timeseriesData,
  96. previousTimeseriesData,
  97. totalSessions,
  98. });
  99. onTotalValuesChange(totalSessions);
  100. } catch {
  101. addErrorMessage(t('Error loading chart data'));
  102. this.setState({
  103. errored: true,
  104. reloading: false,
  105. timeseriesData: null,
  106. previousTimeseriesData: null,
  107. totalSessions: null,
  108. });
  109. }
  110. };
  111. get path() {
  112. const {organization} = this.props;
  113. return `/organizations/${organization.slug}/sessions/`;
  114. }
  115. queryParams({shouldFetchWithPrevious = false}) {
  116. const {selection, query} = this.props;
  117. const {datetime, projects, environments: environment} = selection;
  118. const baseParams = {
  119. field: 'sum(session)',
  120. groupBy: 'session.status',
  121. interval: getSeriesApiInterval(datetime),
  122. project: projects[0],
  123. environment,
  124. query,
  125. };
  126. if (!shouldFetchWithPrevious) {
  127. return {
  128. ...baseParams,
  129. ...getParams(datetime),
  130. };
  131. }
  132. const {period} = selection.datetime;
  133. const doubledPeriod = getPeriod(
  134. {period, start: undefined, end: undefined},
  135. {shouldDoublePeriod: true}
  136. ).statsPeriod;
  137. return {
  138. ...baseParams,
  139. statsPeriod: doubledPeriod,
  140. };
  141. }
  142. transformData(responseData: SessionApiResponse, {fetchedWithPrevious = false}) {
  143. const {theme} = this.props;
  144. // Take the floor just in case, but data should always be divisible by 2
  145. const dataMiddleIndex = Math.floor(responseData.intervals.length / 2);
  146. // calculate the total number of sessions for this period (exclude previous if there)
  147. const totalSessions = responseData.groups.reduce(
  148. (acc, group) =>
  149. acc +
  150. group.series['sum(session)']
  151. .slice(fetchedWithPrevious ? dataMiddleIndex : 0)
  152. .reduce((value, groupAcc) => groupAcc + value, 0),
  153. 0
  154. );
  155. const previousPeriodTotalSessions = fetchedWithPrevious
  156. ? responseData.groups.reduce(
  157. (acc, group) =>
  158. acc +
  159. group.series['sum(session)']
  160. .slice(0, dataMiddleIndex)
  161. .reduce((value, groupAcc) => groupAcc + value, 0),
  162. 0
  163. )
  164. : 0;
  165. // TODO(project-details): refactor this to avoid duplication as we add more session charts
  166. const timeseriesData = [
  167. {
  168. seriesName: t('This Period'),
  169. color: theme.green300,
  170. data: responseData.intervals
  171. .slice(fetchedWithPrevious ? dataMiddleIndex : 0)
  172. .map((interval, i) => {
  173. const totalIntervalSessions = responseData.groups.reduce(
  174. (acc, group) =>
  175. acc +
  176. group.series['sum(session)'].slice(
  177. fetchedWithPrevious ? dataMiddleIndex : 0
  178. )[i],
  179. 0
  180. );
  181. const intervalCrashedSessions =
  182. responseData.groups
  183. .find(group => group.by['session.status'] === 'crashed')
  184. ?.series['sum(session)'].slice(fetchedWithPrevious ? dataMiddleIndex : 0)[
  185. i
  186. ] ?? 0;
  187. const crashedSessionsPercent = percent(
  188. intervalCrashedSessions,
  189. totalIntervalSessions
  190. );
  191. return {
  192. name: interval,
  193. value:
  194. totalSessions === 0 && previousPeriodTotalSessions === 0
  195. ? 0
  196. : totalIntervalSessions === 0
  197. ? null
  198. : getCrashFreePercent(100 - crashedSessionsPercent),
  199. };
  200. }),
  201. },
  202. ] as Series[]; // TODO(project-detail): Change SeriesDataUnit value to support null
  203. const previousTimeseriesData = fetchedWithPrevious
  204. ? ({
  205. seriesName: t('Previous Period'),
  206. data: responseData.intervals.slice(0, dataMiddleIndex).map((_interval, i) => {
  207. const totalIntervalSessions = responseData.groups.reduce(
  208. (acc, group) =>
  209. acc + group.series['sum(session)'].slice(0, dataMiddleIndex)[i],
  210. 0
  211. );
  212. const intervalCrashedSessions =
  213. responseData.groups
  214. .find(group => group.by['session.status'] === 'crashed')
  215. ?.series['sum(session)'].slice(0, dataMiddleIndex)[i] ?? 0;
  216. const crashedSessionsPercent = percent(
  217. intervalCrashedSessions,
  218. totalIntervalSessions
  219. );
  220. return {
  221. name: responseData.intervals[i + dataMiddleIndex],
  222. value:
  223. totalSessions === 0 && previousPeriodTotalSessions === 0
  224. ? 0
  225. : totalIntervalSessions === 0
  226. ? null
  227. : getCrashFreePercent(100 - crashedSessionsPercent),
  228. };
  229. }),
  230. } as Series) // TODO(project-detail): Change SeriesDataUnit value to support null
  231. : null;
  232. return {
  233. totalSessions,
  234. timeseriesData,
  235. previousTimeseriesData,
  236. };
  237. }
  238. transformSessionCountData(responseData: SessionApiResponse) {
  239. const {theme} = this.props;
  240. const sessionsChart = initSessionsChart(theme);
  241. const {intervals, groups} = responseData;
  242. const totalSessions = getCount(responseData.groups, SessionField.SESSIONS);
  243. const chartData = [
  244. {
  245. ...sessionsChart[SessionStatus.HEALTHY],
  246. data: getCountSeries(
  247. SessionField.SESSIONS,
  248. groups.find(g => g.by['session.status'] === SessionStatus.HEALTHY),
  249. intervals
  250. ),
  251. },
  252. {
  253. ...sessionsChart[SessionStatus.ERRORED],
  254. data: getCountSeries(
  255. SessionField.SESSIONS,
  256. groups.find(g => g.by['session.status'] === SessionStatus.ERRORED),
  257. intervals
  258. ),
  259. },
  260. {
  261. ...sessionsChart[SessionStatus.ABNORMAL],
  262. data: getCountSeries(
  263. SessionField.SESSIONS,
  264. groups.find(g => g.by['session.status'] === SessionStatus.ABNORMAL),
  265. intervals
  266. ),
  267. },
  268. {
  269. ...sessionsChart[SessionStatus.CRASHED],
  270. data: getCountSeries(
  271. SessionField.SESSIONS,
  272. groups.find(g => g.by['session.status'] === SessionStatus.CRASHED),
  273. intervals
  274. ),
  275. },
  276. ];
  277. return {
  278. timeseriesData: chartData,
  279. previousTimeseriesData: null,
  280. totalSessions,
  281. };
  282. }
  283. render() {
  284. const {children} = this.props;
  285. const {timeseriesData, reloading, errored, totalSessions, previousTimeseriesData} =
  286. this.state;
  287. const loading = timeseriesData === null;
  288. return children({
  289. loading,
  290. reloading,
  291. errored,
  292. totalSessions,
  293. previousTimeseriesData,
  294. timeseriesData: timeseriesData ?? [],
  295. });
  296. }
  297. }
  298. export default withTheme(SessionsRequest);