totalCrashFreeUsers.tsx 4.9 KB

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