releaseAdoption.tsx 11 KB

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