releaseAdoption.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import {withRouter, WithRouterProps} from 'react-router';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import ChartZoom from 'sentry/components/charts/chartZoom';
  5. import ErrorPanel from 'sentry/components/charts/errorPanel';
  6. import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
  7. import TransitionChart from 'sentry/components/charts/transitionChart';
  8. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  9. import NotAvailable from 'sentry/components/notAvailable';
  10. import QuestionTooltip from 'sentry/components/questionTooltip';
  11. import SidebarSection from 'sentry/components/sidebarSection';
  12. import Tag from 'sentry/components/tag';
  13. import Tooltip from 'sentry/components/tooltip';
  14. import {IconWarning} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {
  18. ReleaseProject,
  19. ReleaseWithHealth,
  20. SessionApiResponse,
  21. SessionFieldWithOperation,
  22. } from 'sentry/types';
  23. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  24. import {getAdoptionSeries, getCount, getCountAtIndex} from 'sentry/utils/sessions';
  25. import {
  26. ADOPTION_STAGE_LABELS,
  27. getReleaseBounds,
  28. getReleaseParams,
  29. isMobileRelease,
  30. } from '../../../utils';
  31. import {generateReleaseMarkLines, releaseMarkLinesLabels} from '../../utils';
  32. const sessionsAxisIndex = 0;
  33. const usersAxisIndex = 1;
  34. const axisIndexToSessionsField = {
  35. [sessionsAxisIndex]: SessionFieldWithOperation.SESSIONS,
  36. [usersAxisIndex]: SessionFieldWithOperation.USERS,
  37. };
  38. type Props = {
  39. allSessions: SessionApiResponse | null;
  40. environment: string[];
  41. errored: boolean;
  42. loading: boolean;
  43. project: ReleaseProject;
  44. release: ReleaseWithHealth;
  45. releaseSessions: SessionApiResponse | null;
  46. reloading: boolean;
  47. } & WithRouterProps;
  48. function ReleaseAdoption({
  49. release,
  50. project,
  51. environment,
  52. releaseSessions,
  53. allSessions,
  54. loading,
  55. reloading,
  56. errored,
  57. router,
  58. location,
  59. }: Props) {
  60. const theme = useTheme();
  61. const hasUsers = !!getCount(releaseSessions?.groups, SessionFieldWithOperation.USERS);
  62. function getSeries() {
  63. if (!releaseSessions) {
  64. return [];
  65. }
  66. const sessionsMarkLines = generateReleaseMarkLines(
  67. release,
  68. project,
  69. theme,
  70. location,
  71. {
  72. hideLabel: true,
  73. axisIndex: sessionsAxisIndex,
  74. }
  75. );
  76. const series = [
  77. ...sessionsMarkLines,
  78. {
  79. seriesName: t('Sessions'),
  80. connectNulls: true,
  81. yAxisIndex: sessionsAxisIndex,
  82. xAxisIndex: sessionsAxisIndex,
  83. data: getAdoptionSeries(
  84. releaseSessions.groups,
  85. allSessions?.groups,
  86. releaseSessions.intervals,
  87. SessionFieldWithOperation.SESSIONS
  88. ),
  89. },
  90. ];
  91. if (hasUsers) {
  92. const usersMarkLines = generateReleaseMarkLines(release, project, theme, location, {
  93. hideLabel: true,
  94. axisIndex: usersAxisIndex,
  95. });
  96. series.push(...usersMarkLines);
  97. series.push({
  98. seriesName: t('Users'),
  99. connectNulls: true,
  100. yAxisIndex: usersAxisIndex,
  101. xAxisIndex: usersAxisIndex,
  102. data: getAdoptionSeries(
  103. releaseSessions.groups,
  104. allSessions?.groups,
  105. releaseSessions.intervals,
  106. SessionFieldWithOperation.USERS
  107. ),
  108. });
  109. }
  110. return series;
  111. }
  112. const colors = theme.charts.getColorPalette(2);
  113. const axisLineConfig = {
  114. scale: true,
  115. axisLine: {
  116. show: false,
  117. },
  118. axisTick: {
  119. show: false,
  120. },
  121. splitLine: {
  122. show: false,
  123. },
  124. max: 100,
  125. axisLabel: {
  126. formatter: (value: number) => `${value}%`,
  127. color: theme.chartLabel,
  128. },
  129. };
  130. const chartOptions: Omit<LineChartProps, 'series' | 'ref'> = {
  131. height: hasUsers ? 280 : 140,
  132. grid: [
  133. {
  134. top: '40px',
  135. left: '10px',
  136. right: '10px',
  137. height: '100px',
  138. },
  139. {
  140. top: '180px',
  141. left: '10px',
  142. right: '10px',
  143. height: '100px',
  144. },
  145. ],
  146. axisPointer: {
  147. // Link each x-axis together.
  148. link: [{xAxisIndex: [sessionsAxisIndex, usersAxisIndex]}],
  149. },
  150. xAxes: Array.from(new Array(2)).map((_i, index) => ({
  151. gridIndex: index,
  152. type: 'time' as const,
  153. show: false,
  154. })),
  155. yAxes: [
  156. {
  157. gridIndex: sessionsAxisIndex,
  158. ...axisLineConfig,
  159. },
  160. {
  161. gridIndex: usersAxisIndex,
  162. ...axisLineConfig,
  163. },
  164. ],
  165. // utc: utc === 'true', //TODO(release-comparison)
  166. isGroupedByDate: true,
  167. showTimeInTooltip: true,
  168. colors: [colors[0], colors[1]] as string[],
  169. tooltip: {
  170. trigger: 'axis' as const,
  171. truncate: 80,
  172. valueFormatter: (value, label, seriesParams: any) => {
  173. const {axisIndex, dataIndex} = seriesParams || {};
  174. const absoluteCount = getCountAtIndex(
  175. releaseSessions?.groups,
  176. axisIndexToSessionsField[axisIndex ?? 0],
  177. dataIndex ?? 0
  178. );
  179. return label && Object.values(releaseMarkLinesLabels).includes(label)
  180. ? ''
  181. : `<span>${formatAbbreviatedNumber(absoluteCount)} <span style="color: ${
  182. theme.textColor
  183. };margin-left: ${space(0.5)}">${value}%</span></span>`;
  184. },
  185. filter: (_, seriesParam: any) => {
  186. const {seriesName, axisIndex} = seriesParam;
  187. // do not display tooltips for "Users Adopted" marklines
  188. if (
  189. axisIndex === usersAxisIndex &&
  190. Object.values(releaseMarkLinesLabels).includes(seriesName)
  191. ) {
  192. return false;
  193. }
  194. return true;
  195. },
  196. },
  197. };
  198. const {
  199. statsPeriod: period,
  200. start,
  201. end,
  202. utc,
  203. } = getReleaseParams({
  204. location,
  205. releaseBounds: getReleaseBounds(release),
  206. });
  207. const adoptionStage = release.adoptionStages?.[project.slug]?.stage;
  208. const adoptionStageLabel = ADOPTION_STAGE_LABELS[adoptionStage];
  209. const multipleEnvironments = environment.length === 0 || environment.length > 1;
  210. return (
  211. <div>
  212. {isMobileRelease(project.platform) && (
  213. <SidebarSection
  214. title={t('Adoption Stage')}
  215. icon={
  216. multipleEnvironments && (
  217. <QuestionTooltip
  218. position="top"
  219. title={t(
  220. 'See if a release has low adoption, been adopted by users, or replaced by another release. Select an environment above to view the stage this release is in.'
  221. )}
  222. size="sm"
  223. />
  224. )
  225. }
  226. >
  227. {adoptionStageLabel && !multipleEnvironments ? (
  228. <div>
  229. <Tooltip title={adoptionStageLabel.tooltipTitle} isHoverable>
  230. <Tag type={adoptionStageLabel.type}>{adoptionStageLabel.name}</Tag>
  231. </Tooltip>
  232. <AdoptionEnvironment>
  233. {tct(`in [environment]`, {environment})}
  234. </AdoptionEnvironment>
  235. </div>
  236. ) : (
  237. <NotAvailableWrapper>
  238. <NotAvailable />
  239. </NotAvailableWrapper>
  240. )}
  241. </SidebarSection>
  242. )}
  243. <RelativeBox>
  244. {!loading && (
  245. <ChartLabel top="0px">
  246. <ChartTitle
  247. title={t('Sessions Adopted')}
  248. icon={
  249. <QuestionTooltip
  250. position="top"
  251. title={t(
  252. 'Adoption compares the sessions of a release with the total sessions for this project.'
  253. )}
  254. size="sm"
  255. />
  256. }
  257. />
  258. </ChartLabel>
  259. )}
  260. {!loading && hasUsers && (
  261. <ChartLabel top="140px">
  262. <ChartTitle
  263. title={t('Users Adopted')}
  264. icon={
  265. <QuestionTooltip
  266. position="top"
  267. title={t(
  268. 'Adoption compares the users of a release with the total users for this project.'
  269. )}
  270. size="sm"
  271. />
  272. }
  273. />
  274. </ChartLabel>
  275. )}
  276. {errored ? (
  277. <ErrorPanel height="280px">
  278. <IconWarning color="gray300" size="lg" />
  279. </ErrorPanel>
  280. ) : (
  281. <TransitionChart loading={loading} reloading={reloading} height="280px">
  282. <TransparentLoadingMask visible={reloading} />
  283. <ChartZoom
  284. router={router}
  285. period={period ?? undefined}
  286. utc={utc === 'true'}
  287. start={start}
  288. end={end}
  289. usePageDate
  290. xAxisIndex={[sessionsAxisIndex, usersAxisIndex]}
  291. >
  292. {zoomRenderProps => (
  293. <LineChart
  294. {...chartOptions}
  295. {...zoomRenderProps}
  296. series={getSeries()}
  297. transformSinglePointToLine
  298. />
  299. )}
  300. </ChartZoom>
  301. </TransitionChart>
  302. )}
  303. </RelativeBox>
  304. </div>
  305. );
  306. }
  307. const NotAvailableWrapper = styled('div')`
  308. display: flex;
  309. align-items: center;
  310. `;
  311. const AdoptionEnvironment = styled('span')`
  312. color: ${p => p.theme.textColor};
  313. margin-left: ${space(0.5)};
  314. font-size: ${p => p.theme.fontSizeSmall};
  315. `;
  316. const RelativeBox = styled('div')`
  317. position: relative;
  318. `;
  319. const ChartTitle = styled(SidebarSection)`
  320. margin: 0;
  321. `;
  322. const ChartLabel = styled('div')<{top: string}>`
  323. position: absolute;
  324. top: ${p => p.top};
  325. z-index: 1;
  326. left: 0;
  327. right: 0;
  328. `;
  329. export default withRouter(ReleaseAdoption);