utils.tsx 9.7 KB

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