utils.tsx 8.7 KB

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