releaseAdoption.tsx 11 KB

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