utils.tsx 8.5 KB

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