utils.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import type {Theme} from '@emotion/react';
  2. import type {Location} from 'history';
  3. import pick from 'lodash/pick';
  4. import type {Moment} from 'moment-timezone';
  5. import moment from 'moment-timezone';
  6. import MarkLine from 'sentry/components/charts/components/markLine';
  7. import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
  8. import {URL_PARAM} from 'sentry/constants/pageFilters';
  9. import {t} from 'sentry/locale';
  10. import type {
  11. Commit,
  12. CommitFile,
  13. FilesByRepository,
  14. ReleaseProject,
  15. ReleaseWithHealth,
  16. Repository,
  17. } from 'sentry/types';
  18. import {ReleaseComparisonChartType} from 'sentry/types';
  19. import type {Series} from 'sentry/types/echarts';
  20. import {decodeList} from 'sentry/utils/queryString';
  21. import {getReleaseBounds, getReleaseParams, isMobileRelease} from '../utils';
  22. import {commonTermsDescription, SessionTerm} from '../utils/sessionTerm';
  23. type CommitsByRepository = Record<string, Commit[]>;
  24. /**
  25. * Convert list of individual file changes into a per-file summary grouped by repository
  26. */
  27. export function getFilesByRepository(fileList: CommitFile[]) {
  28. return fileList.reduce<FilesByRepository>((filesByRepository, file) => {
  29. const {filename, repoName, author, type} = file;
  30. if (!filesByRepository.hasOwnProperty(repoName)) {
  31. filesByRepository[repoName] = {};
  32. }
  33. if (!filesByRepository[repoName].hasOwnProperty(filename)) {
  34. filesByRepository[repoName][filename] = {
  35. authors: {},
  36. types: new Set(),
  37. };
  38. }
  39. if (author.email) {
  40. filesByRepository[repoName][filename].authors[author.email] = author;
  41. }
  42. filesByRepository[repoName][filename].types.add(type);
  43. return filesByRepository;
  44. }, {});
  45. }
  46. /**
  47. * Convert list of individual commits into a summary grouped by repository
  48. */
  49. export function getCommitsByRepository(commitList: Commit[]): CommitsByRepository {
  50. return commitList.reduce<CommitsByRepository>((commitsByRepository, commit) => {
  51. const repositoryName = commit.repository?.name ?? t('unknown');
  52. if (!commitsByRepository.hasOwnProperty(repositoryName)) {
  53. commitsByRepository[repositoryName] = [];
  54. }
  55. commitsByRepository[repositoryName].push(commit);
  56. return commitsByRepository;
  57. }, {});
  58. }
  59. /**
  60. * Get request query according to the url params and active repository
  61. */
  62. type GetQueryProps = {
  63. location: Location;
  64. activeRepository?: Repository;
  65. perPage?: number;
  66. };
  67. export function getQuery({location, perPage = 40, activeRepository}: GetQueryProps) {
  68. const query = {
  69. ...pick(location.query, [...Object.values(URL_PARAM), 'cursor']),
  70. per_page: perPage,
  71. };
  72. if (!activeRepository) {
  73. return query;
  74. }
  75. return {
  76. ...query,
  77. repo_id: activeRepository.externalId,
  78. repo_name: activeRepository.name,
  79. };
  80. }
  81. /**
  82. * Get repositories to render according to the activeRepository
  83. */
  84. export function getReposToRender(repos: Array<string>, activeRepository?: Repository) {
  85. if (!activeRepository) {
  86. return repos;
  87. }
  88. return [activeRepository.name];
  89. }
  90. export const releaseComparisonChartLabels = {
  91. [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]: t('Crash Free Session Rate'),
  92. [ReleaseComparisonChartType.HEALTHY_SESSIONS]: t('Healthy'),
  93. [ReleaseComparisonChartType.ABNORMAL_SESSIONS]: t('Abnormal'),
  94. [ReleaseComparisonChartType.ERRORED_SESSIONS]: t('Errored'),
  95. [ReleaseComparisonChartType.CRASHED_SESSIONS]: t('Crashed Session Rate'),
  96. [ReleaseComparisonChartType.CRASH_FREE_USERS]: t('Crash Free User Rate'),
  97. [ReleaseComparisonChartType.HEALTHY_USERS]: t('Healthy'),
  98. [ReleaseComparisonChartType.ABNORMAL_USERS]: t('Abnormal'),
  99. [ReleaseComparisonChartType.ERRORED_USERS]: t('Errored'),
  100. [ReleaseComparisonChartType.CRASHED_USERS]: t('Crashed User Rate'),
  101. [ReleaseComparisonChartType.SESSION_COUNT]: t('Session Count'),
  102. [ReleaseComparisonChartType.USER_COUNT]: t('User Count'),
  103. [ReleaseComparisonChartType.ERROR_COUNT]: t('Error Count'),
  104. [ReleaseComparisonChartType.TRANSACTION_COUNT]: t('Transaction Count'),
  105. [ReleaseComparisonChartType.FAILURE_RATE]: t('Failure Rate'),
  106. };
  107. export const releaseComparisonChartTitles = {
  108. [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]: t('Crash Free Session Rate'),
  109. [ReleaseComparisonChartType.HEALTHY_SESSIONS]: t('Healthy Session Rate'),
  110. [ReleaseComparisonChartType.ABNORMAL_SESSIONS]: t('Abnormal Session Rate'),
  111. [ReleaseComparisonChartType.ERRORED_SESSIONS]: t('Errored Session Rate'),
  112. [ReleaseComparisonChartType.CRASHED_SESSIONS]: t('Crashed Session Rate'),
  113. [ReleaseComparisonChartType.CRASH_FREE_USERS]: t('Crash Free User Rate'),
  114. [ReleaseComparisonChartType.HEALTHY_USERS]: t('Healthy User Rate'),
  115. [ReleaseComparisonChartType.ABNORMAL_USERS]: t('Abnormal User Rate'),
  116. [ReleaseComparisonChartType.ERRORED_USERS]: t('Errored User Rate'),
  117. [ReleaseComparisonChartType.CRASHED_USERS]: t('Crashed User Rate'),
  118. [ReleaseComparisonChartType.SESSION_COUNT]: t('Session Count'),
  119. [ReleaseComparisonChartType.USER_COUNT]: t('User Count'),
  120. [ReleaseComparisonChartType.ERROR_COUNT]: t('Error Count'),
  121. [ReleaseComparisonChartType.TRANSACTION_COUNT]: t('Transaction Count'),
  122. [ReleaseComparisonChartType.FAILURE_RATE]: t('Failure Rate'),
  123. };
  124. export const releaseComparisonChartHelp = {
  125. [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]:
  126. commonTermsDescription[SessionTerm.CRASH_FREE_SESSIONS],
  127. [ReleaseComparisonChartType.CRASH_FREE_USERS]:
  128. commonTermsDescription[SessionTerm.CRASH_FREE_USERS],
  129. [ReleaseComparisonChartType.SESSION_COUNT]: t(
  130. 'The number of sessions in a given period.'
  131. ),
  132. [ReleaseComparisonChartType.USER_COUNT]: t('The number of users in a given period.'),
  133. };
  134. type GenerateReleaseMarklineOptions = {
  135. axisIndex?: number;
  136. hideLabel?: boolean;
  137. };
  138. function generateReleaseMarkLine(
  139. title: string,
  140. position: number,
  141. theme: Theme,
  142. options?: GenerateReleaseMarklineOptions
  143. ) {
  144. const {hideLabel, axisIndex} = options || {};
  145. return {
  146. seriesName: title,
  147. type: 'line',
  148. data: [{name: position, value: null as any}], // TODO(ts): echart types
  149. yAxisIndex: axisIndex ?? undefined,
  150. xAxisIndex: axisIndex ?? undefined,
  151. color: theme.gray300,
  152. markLine: MarkLine({
  153. silent: true,
  154. lineStyle: {color: theme.gray300, type: 'solid'},
  155. label: {
  156. position: 'insideEndBottom',
  157. formatter: hideLabel ? '' : title,
  158. // @ts-expect-error weird echart types
  159. font: 'Rubik',
  160. fontSize: 14,
  161. color: theme.chartLabel,
  162. backgroundColor: theme.chartOther,
  163. },
  164. data: [
  165. {
  166. xAxis: position,
  167. },
  168. ],
  169. }),
  170. };
  171. }
  172. export const releaseMarkLinesLabels = {
  173. created: t('Release Created'),
  174. adopted: t('Adopted'),
  175. unadopted: t('Replaced'),
  176. };
  177. export function generateReleaseMarkLines(
  178. release: ReleaseWithHealth,
  179. project: ReleaseProject,
  180. theme: Theme,
  181. location: Location,
  182. options?: GenerateReleaseMarklineOptions
  183. ) {
  184. const markLines: Series[] = [];
  185. const adoptionStages = release.adoptionStages?.[project.slug];
  186. const isSingleEnv = decodeList(location.query.environment).length === 1;
  187. const releaseBounds = getReleaseBounds(release);
  188. const {statsPeriod, ...releaseParamsRest} = getReleaseParams({
  189. location,
  190. releaseBounds,
  191. });
  192. let {start, end} = releaseParamsRest;
  193. const isDefaultPeriod = !(
  194. location.query.pageStart ||
  195. location.query.pageEnd ||
  196. location.query.pageStatsPeriod
  197. );
  198. if (statsPeriod) {
  199. const parsedStatsPeriod = parseStatsPeriod(statsPeriod, null);
  200. start = parsedStatsPeriod.start;
  201. end = parsedStatsPeriod.end;
  202. }
  203. const releaseCreated = moment(release.dateCreated).startOf('minute');
  204. if (
  205. releaseCreated.isBetween(start, end) ||
  206. (isDefaultPeriod && releaseBounds.type === 'normal')
  207. ) {
  208. markLines.push(
  209. generateReleaseMarkLine(
  210. releaseMarkLinesLabels.created,
  211. releaseCreated.valueOf(),
  212. theme,
  213. options
  214. )
  215. );
  216. }
  217. if (!isSingleEnv || !isMobileRelease(project.platform)) {
  218. // for now want to show marklines only on mobile platforms with single environment selected
  219. return markLines;
  220. }
  221. const releaseAdopted: Moment | undefined = adoptionStages?.adopted
  222. ? moment(adoptionStages.adopted)
  223. : undefined;
  224. if (releaseAdopted?.isBetween(start, end)) {
  225. markLines.push(
  226. generateReleaseMarkLine(
  227. releaseMarkLinesLabels.adopted,
  228. releaseAdopted.valueOf(),
  229. theme,
  230. options
  231. )
  232. );
  233. }
  234. const releaseReplaced: Moment | undefined = adoptionStages?.unadopted
  235. ? moment(adoptionStages.unadopted)
  236. : undefined;
  237. if (releaseReplaced?.isBetween(start, end)) {
  238. markLines.push(
  239. generateReleaseMarkLine(
  240. releaseMarkLinesLabels.unadopted,
  241. releaseReplaced.valueOf(),
  242. theme,
  243. options
  244. )
  245. );
  246. }
  247. return markLines;
  248. }