totalCrashFreeUsers.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import styled from '@emotion/styled';
  2. import {Location} from 'history';
  3. import pick from 'lodash/pick';
  4. import moment from 'moment';
  5. import AsyncComponent from 'app/components/asyncComponent';
  6. import Count from 'app/components/count';
  7. import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
  8. import {URL_PARAM} from 'app/constants/globalSelectionHeader';
  9. import {t, tn} from 'app/locale';
  10. import overflowEllipsis from 'app/styles/overflowEllipsis';
  11. import space from 'app/styles/space';
  12. import {CrashFreeTimeBreakdown, Organization} from 'app/types';
  13. import {defined} from 'app/utils';
  14. import {displayCrashFreePercent} from '../../utils';
  15. import {SectionHeading, Wrapper} from './styles';
  16. type Props = AsyncComponent['props'] & {
  17. location: Location;
  18. organization: Organization;
  19. version: string;
  20. projectSlug: string;
  21. };
  22. type State = AsyncComponent['state'] & {
  23. releaseStats?: {usersBreakdown: CrashFreeTimeBreakdown} | null;
  24. };
  25. class TotalCrashFreeUsers extends AsyncComponent<Props, State> {
  26. shouldReload = true;
  27. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  28. const {location, organization, projectSlug, version} = this.props;
  29. return [
  30. [
  31. 'releaseStats',
  32. `/projects/${organization.slug}/${projectSlug}/releases/${version}/stats/`,
  33. {
  34. query: {
  35. ...getParams(
  36. pick(location.query, [URL_PARAM.PROJECT, URL_PARAM.ENVIRONMENT])
  37. ),
  38. type: 'sessions',
  39. },
  40. },
  41. ],
  42. ];
  43. }
  44. componentDidUpdate(prevProps: Props) {
  45. if (prevProps.version !== this.props.version) {
  46. this.remountComponent();
  47. }
  48. }
  49. renderLoading() {
  50. return this.renderBody();
  51. }
  52. renderBody() {
  53. const crashFreeTimeBreakdown = this.state.releaseStats?.usersBreakdown;
  54. if (!crashFreeTimeBreakdown?.length) {
  55. return null;
  56. }
  57. const timeline = crashFreeTimeBreakdown
  58. .map(({date, crashFreeUsers, totalUsers}, index, data) => {
  59. // count number of crash free users from knowing percent and total
  60. const crashFreeUserCount = Math.round(((crashFreeUsers ?? 0) * totalUsers) / 100);
  61. // first item of timeline is release creation date, then we want to have relative date label
  62. const dateLabel =
  63. index === 0
  64. ? t('Release created')
  65. : `${moment(data[0].date).from(date, true)} ${t('later')}`;
  66. return {date: moment(date), dateLabel, crashFreeUsers, crashFreeUserCount};
  67. })
  68. // remove those timeframes that are in the future
  69. .filter(item => item.date.isBefore())
  70. // we want timeline to go from bottom to up
  71. .reverse();
  72. if (!timeline.length) {
  73. return null;
  74. }
  75. return (
  76. <Wrapper>
  77. <SectionHeading>{t('Total Crash Free Users')}</SectionHeading>
  78. <Timeline>
  79. {timeline.map(row => (
  80. <Row key={row.date.toString()}>
  81. <InnerRow>
  82. <Text bold>{row.date.format('MMMM D')}</Text>
  83. <Text bold right>
  84. <Count value={row.crashFreeUserCount} />{' '}
  85. {tn('user', 'users', row.crashFreeUserCount)}
  86. </Text>
  87. </InnerRow>
  88. <InnerRow>
  89. <Text>{row.dateLabel}</Text>
  90. <Text right>
  91. {defined(row.crashFreeUsers)
  92. ? displayCrashFreePercent(row.crashFreeUsers)
  93. : '-'}
  94. </Text>
  95. </InnerRow>
  96. </Row>
  97. ))}
  98. </Timeline>
  99. </Wrapper>
  100. );
  101. }
  102. }
  103. const Timeline = styled('div')`
  104. font-size: ${p => p.theme.fontSizeMedium};
  105. line-height: 1.2;
  106. `;
  107. const DOT_SIZE = 10;
  108. const Row = styled('div')`
  109. border-left: 1px solid ${p => p.theme.border};
  110. padding-left: ${space(2)};
  111. padding-bottom: ${space(1)};
  112. margin-left: ${space(1)};
  113. position: relative;
  114. &:before {
  115. content: '';
  116. width: ${DOT_SIZE}px;
  117. height: ${DOT_SIZE}px;
  118. border-radius: 100%;
  119. background-color: ${p => p.theme.purple300};
  120. position: absolute;
  121. top: 0;
  122. left: -${Math.floor(DOT_SIZE / 2)}px;
  123. }
  124. &:last-child {
  125. border-left: 0;
  126. }
  127. `;
  128. const InnerRow = styled('div')`
  129. display: grid;
  130. grid-column-gap: ${space(2)};
  131. grid-auto-flow: column;
  132. grid-auto-columns: 1fr;
  133. padding-bottom: ${space(0.5)};
  134. `;
  135. const Text = styled('div')<{bold?: boolean; right?: boolean}>`
  136. text-align: ${p => (p.right ? 'right' : 'left')};
  137. color: ${p => (p.bold ? p.theme.textColor : p.theme.gray300)};
  138. padding-bottom: ${space(0.25)};
  139. ${overflowEllipsis};
  140. `;
  141. export default TotalCrashFreeUsers;