utils.tsx 8.9 KB

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