sessions.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import {Theme} from '@emotion/react';
  2. import compact from 'lodash/compact';
  3. import mean from 'lodash/mean';
  4. import moment from 'moment';
  5. import {
  6. DateTimeObject,
  7. getDiffInMinutes,
  8. SIX_HOURS,
  9. SIXTY_DAYS,
  10. THIRTY_DAYS,
  11. TWENTY_FOUR_HOURS,
  12. } from 'sentry/components/charts/utils';
  13. import {SessionApiResponse, SessionFieldWithOperation, SessionStatus} from 'sentry/types';
  14. import {SeriesDataUnit} from 'sentry/types/echarts';
  15. import {defined, percent} from 'sentry/utils';
  16. import {getCrashFreePercent, getSessionStatusPercent} from 'sentry/views/releases/utils';
  17. import {sessionTerm} from 'sentry/views/releases/utils/sessionTerm';
  18. /**
  19. * If the time window is less than or equal 10, seconds will be displayed on the graphs
  20. */
  21. export const MINUTES_THRESHOLD_TO_DISPLAY_SECONDS = 10;
  22. export function getCount(
  23. groups: SessionApiResponse['groups'] = [],
  24. field: SessionFieldWithOperation
  25. ) {
  26. return groups.reduce((acc, group) => acc + group.totals[field], 0);
  27. }
  28. export function getCountAtIndex(
  29. groups: SessionApiResponse['groups'] = [],
  30. field: SessionFieldWithOperation,
  31. index: number
  32. ) {
  33. return groups.reduce((acc, group) => acc + group.series[field][index], 0);
  34. }
  35. export function getCrashFreeRate(
  36. groups: SessionApiResponse['groups'] = [],
  37. field: SessionFieldWithOperation
  38. ) {
  39. const crashedRate = getSessionStatusRate(groups, field, SessionStatus.CRASHED);
  40. return defined(crashedRate) ? getCrashFreePercent(100 - crashedRate) : null;
  41. }
  42. export function getSeriesAverage(
  43. groups: SessionApiResponse['groups'] = [],
  44. field: SessionFieldWithOperation
  45. ) {
  46. const totalCount = getCount(groups, field);
  47. const dataPoints = groups.filter(group => !!group.totals[field]).length;
  48. return !defined(totalCount) || dataPoints === null || totalCount === 0
  49. ? null
  50. : totalCount / dataPoints;
  51. }
  52. export function getSeriesSum(
  53. groups: SessionApiResponse['groups'] = [],
  54. field: SessionFieldWithOperation,
  55. intervals: SessionApiResponse['intervals'] = []
  56. ) {
  57. const dataPointsSums: number[] = Array(intervals.length).fill(0);
  58. const groupSeries = groups.map(group => group.series[field]);
  59. groupSeries.forEach(series => {
  60. series.forEach((dataPoint, idx) => (dataPointsSums[idx] += dataPoint));
  61. });
  62. return dataPointsSums;
  63. }
  64. export function getSessionStatusRate(
  65. groups: SessionApiResponse['groups'] = [],
  66. field: SessionFieldWithOperation,
  67. status: SessionStatus
  68. ) {
  69. const totalCount = getCount(groups, field);
  70. const crashedCount = getCount(
  71. groups.filter(({by}) => by['session.status'] === status),
  72. field
  73. );
  74. return !defined(totalCount) || totalCount === 0
  75. ? null
  76. : percent(crashedCount ?? 0, totalCount ?? 0);
  77. }
  78. export function getCrashFreeRateSeries(
  79. groups: SessionApiResponse['groups'] = [],
  80. intervals: SessionApiResponse['intervals'] = [],
  81. field: SessionFieldWithOperation
  82. ): SeriesDataUnit[] {
  83. return compact(
  84. intervals.map((interval, i) => {
  85. const intervalTotalSessions = groups.reduce(
  86. (acc, group) => acc + (group.series[field]?.[i] ?? 0),
  87. 0
  88. );
  89. const intervalCrashedSessions =
  90. groups.find(group => group.by['session.status'] === SessionStatus.CRASHED)
  91. ?.series[field]?.[i] ?? 0;
  92. const crashedSessionsPercent = percent(
  93. intervalCrashedSessions,
  94. intervalTotalSessions
  95. );
  96. if (intervalTotalSessions === 0) {
  97. return null;
  98. }
  99. return {
  100. name: interval,
  101. value: getCrashFreePercent(100 - crashedSessionsPercent),
  102. };
  103. })
  104. );
  105. }
  106. export function getSessionStatusRateSeries(
  107. groups: SessionApiResponse['groups'] = [],
  108. intervals: SessionApiResponse['intervals'] = [],
  109. field: SessionFieldWithOperation,
  110. status: SessionStatus
  111. ): SeriesDataUnit[] {
  112. return compact(
  113. intervals.map((interval, i) => {
  114. const intervalTotalSessions = groups.reduce(
  115. (acc, group) => acc + group.series[field][i],
  116. 0
  117. );
  118. const intervalStatusSessions =
  119. groups.find(group => group.by['session.status'] === status)?.series[field][i] ??
  120. 0;
  121. const statusSessionsPercent = percent(
  122. intervalStatusSessions,
  123. intervalTotalSessions
  124. );
  125. if (intervalTotalSessions === 0) {
  126. return null;
  127. }
  128. return {
  129. name: interval,
  130. value: getSessionStatusPercent(statusSessionsPercent),
  131. };
  132. })
  133. );
  134. }
  135. export function getAdoptionSeries(
  136. releaseGroups: SessionApiResponse['groups'] = [],
  137. allGroups: SessionApiResponse['groups'] = [],
  138. intervals: SessionApiResponse['intervals'] = [],
  139. field: SessionFieldWithOperation
  140. ): SeriesDataUnit[] {
  141. return intervals.map((interval, i) => {
  142. const intervalReleaseSessions = releaseGroups.reduce(
  143. (acc, group) => acc + (group.series[field]?.[i] ?? 0),
  144. 0
  145. );
  146. const intervalTotalSessions = allGroups.reduce(
  147. (acc, group) => acc + (group.series[field]?.[i] ?? 0),
  148. 0
  149. );
  150. const intervalAdoption = percent(intervalReleaseSessions, intervalTotalSessions);
  151. return {
  152. name: interval,
  153. value: Math.round(intervalAdoption),
  154. };
  155. });
  156. }
  157. export function getCountSeries(
  158. field: SessionFieldWithOperation,
  159. group?: SessionApiResponse['groups'][0],
  160. intervals: SessionApiResponse['intervals'] = []
  161. ): SeriesDataUnit[] {
  162. return intervals.map((interval, index) => ({
  163. name: interval,
  164. value: group?.series[field][index] ?? 0,
  165. }));
  166. }
  167. export function initSessionsChart(theme: Theme) {
  168. const colors = theme.charts.getColorPalette(14);
  169. return {
  170. [SessionStatus.HEALTHY]: {
  171. seriesName: sessionTerm.healthy,
  172. data: [],
  173. color: theme.green300,
  174. areaStyle: {
  175. color: theme.green300,
  176. opacity: 1,
  177. },
  178. lineStyle: {
  179. opacity: 0,
  180. width: 0.4,
  181. },
  182. },
  183. [SessionStatus.ERRORED]: {
  184. seriesName: sessionTerm.errored,
  185. data: [],
  186. color: colors[12],
  187. areaStyle: {
  188. color: colors[12],
  189. opacity: 1,
  190. },
  191. lineStyle: {
  192. opacity: 0,
  193. width: 0.4,
  194. },
  195. },
  196. [SessionStatus.ABNORMAL]: {
  197. seriesName: sessionTerm.abnormal,
  198. data: [],
  199. color: colors[15],
  200. areaStyle: {
  201. color: colors[15],
  202. opacity: 1,
  203. },
  204. lineStyle: {
  205. opacity: 0,
  206. width: 0.4,
  207. },
  208. },
  209. [SessionStatus.CRASHED]: {
  210. seriesName: sessionTerm.crashed,
  211. data: [],
  212. color: theme.red300,
  213. areaStyle: {
  214. color: theme.red300,
  215. opacity: 1,
  216. },
  217. lineStyle: {
  218. opacity: 0,
  219. width: 0.4,
  220. },
  221. },
  222. };
  223. }
  224. type GetSessionsIntervalOptions = {
  225. dailyInterval?: boolean;
  226. highFidelity?: boolean;
  227. };
  228. export function getSessionsInterval(
  229. datetimeObj: DateTimeObject,
  230. {highFidelity, dailyInterval}: GetSessionsIntervalOptions = {}
  231. ) {
  232. const diffInMinutes = getDiffInMinutes(datetimeObj);
  233. if (moment(datetimeObj.start).isSameOrBefore(moment().subtract(30, 'days'))) {
  234. // we cannot use sub-hour session resolution on buckets older than 30 days
  235. highFidelity = false;
  236. }
  237. if (dailyInterval === true && diffInMinutes > TWENTY_FOUR_HOURS) {
  238. return '1d';
  239. }
  240. if (diffInMinutes >= SIXTY_DAYS) {
  241. return '1d';
  242. }
  243. if (diffInMinutes >= THIRTY_DAYS) {
  244. return '4h';
  245. }
  246. if (diffInMinutes >= SIX_HOURS) {
  247. return '1h';
  248. }
  249. // limit on backend for sub-hour session resolution is set to six hours
  250. if (highFidelity) {
  251. if (diffInMinutes <= MINUTES_THRESHOLD_TO_DISPLAY_SECONDS) {
  252. // This only works for metrics-based session stats.
  253. // Backend will silently replace with '1m' for session-based stats.
  254. return '10s';
  255. }
  256. if (diffInMinutes <= 30) {
  257. return '1m';
  258. }
  259. return '5m';
  260. }
  261. return '1h';
  262. }
  263. // Sessions API can only round intervals to the closest hour - this is especially problematic when using sub-hour resolution.
  264. // We filter out results that are out of bounds on frontend and recalculate totals.
  265. export function filterSessionsInTimeWindow(
  266. sessions: SessionApiResponse,
  267. start?: string,
  268. end?: string
  269. ) {
  270. if (!start || !end) {
  271. return sessions;
  272. }
  273. const filteredIndexes: number[] = [];
  274. const intervals = sessions.intervals.filter((interval, index) => {
  275. const isBetween = moment
  276. .utc(interval)
  277. .isBetween(moment.utc(start), moment.utc(end), undefined, '[]');
  278. if (isBetween) {
  279. filteredIndexes.push(index);
  280. }
  281. return isBetween;
  282. });
  283. const groups = sessions.groups.map(group => {
  284. const series = {};
  285. const totals = {};
  286. Object.keys(group.series).forEach(field => {
  287. totals[field] = 0;
  288. series[field] = group.series[field].filter((value, index) => {
  289. const isBetween = filteredIndexes.includes(index);
  290. if (isBetween) {
  291. totals[field] = (totals[field] ?? 0) + value;
  292. }
  293. return isBetween;
  294. });
  295. if (field.startsWith('p50')) {
  296. totals[field] = mean(series[field]);
  297. }
  298. if (field.startsWith('count_unique')) {
  299. /* E.g. users
  300. We cannot sum here because users would not be unique anymore.
  301. User can be repeated and part of multiple buckets in series but it's still that one user so totals would be wrong.
  302. This operation is not 100% correct, because we are filtering series in time window but the total is for unfiltered series (it's the closest thing we can do right now) */
  303. totals[field] = group.totals[field];
  304. }
  305. });
  306. return {...group, series, totals};
  307. });
  308. return {
  309. start: intervals[0],
  310. end: intervals[intervals.length - 1],
  311. query: sessions.query,
  312. intervals,
  313. groups,
  314. };
  315. }