releaseAdoption.tsx 10.0 KB

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