utils.tsx 9.3 KB

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