releaseSessionsChart.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. import * as React from 'react';
  2. import {withRouter, WithRouterProps} from 'react-router';
  3. import {withTheme} from '@emotion/react';
  4. import round from 'lodash/round';
  5. import AreaChart from 'app/components/charts/areaChart';
  6. import ChartZoom from 'app/components/charts/chartZoom';
  7. import StackedAreaChart from 'app/components/charts/stackedAreaChart';
  8. import {HeaderTitleLegend, HeaderValue} from 'app/components/charts/styles';
  9. import TransitionChart from 'app/components/charts/transitionChart';
  10. import TransparentLoadingMask from 'app/components/charts/transparentLoadingMask';
  11. import QuestionTooltip from 'app/components/questionTooltip';
  12. import {PlatformKey} from 'app/data/platformCategories';
  13. import {t} from 'app/locale';
  14. import {
  15. ReleaseComparisonChartType,
  16. ReleaseProject,
  17. ReleaseWithHealth,
  18. SessionApiResponse,
  19. SessionField,
  20. SessionStatus,
  21. } from 'app/types';
  22. import {defined} from 'app/utils';
  23. import {getCrashFreeRateSeries, getSessionStatusRateSeries} from 'app/utils/sessions';
  24. import {Theme} from 'app/utils/theme';
  25. import {displayCrashFreePercent} from 'app/views/releases/utils';
  26. import {
  27. generateReleaseMarkLines,
  28. releaseComparisonChartHelp,
  29. releaseComparisonChartTitles,
  30. releaseMarkLinesLabels,
  31. } from '../../utils';
  32. import {
  33. fillChartDataFromSessionsResponse,
  34. initSessionsBreakdownChartData,
  35. } from '../chart/utils';
  36. type Props = {
  37. theme: Theme;
  38. release: ReleaseWithHealth;
  39. project: ReleaseProject;
  40. releaseSessions: SessionApiResponse | null;
  41. allSessions: SessionApiResponse | null;
  42. chartType: ReleaseComparisonChartType;
  43. platform: PlatformKey;
  44. value: React.ReactNode;
  45. diff: React.ReactNode;
  46. loading: boolean;
  47. reloading: boolean;
  48. period?: string;
  49. start?: string;
  50. end?: string;
  51. utc?: boolean;
  52. } & WithRouterProps;
  53. class ReleaseSessionsChart extends React.Component<Props> {
  54. formatTooltipValue = (value: string | number | null, label?: string) => {
  55. if (label && Object.values(releaseMarkLinesLabels).includes(label)) {
  56. return '';
  57. }
  58. const {chartType} = this.props;
  59. if (value === null) {
  60. return '\u2015';
  61. }
  62. switch (chartType) {
  63. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  64. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  65. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  66. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  67. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  68. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  69. case ReleaseComparisonChartType.HEALTHY_USERS:
  70. case ReleaseComparisonChartType.ABNORMAL_USERS:
  71. case ReleaseComparisonChartType.ERRORED_USERS:
  72. case ReleaseComparisonChartType.CRASHED_USERS:
  73. return defined(value) ? `${value}%` : '\u2015';
  74. case ReleaseComparisonChartType.SESSION_COUNT:
  75. case ReleaseComparisonChartType.SESSION_DURATION:
  76. case ReleaseComparisonChartType.USER_COUNT:
  77. default:
  78. return typeof value === 'number' ? value.toLocaleString() : value;
  79. }
  80. };
  81. getYAxis() {
  82. const {theme, chartType} = this.props;
  83. switch (chartType) {
  84. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  85. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  86. return {
  87. max: 100,
  88. scale: true,
  89. axisLabel: {
  90. formatter: (value: number) => displayCrashFreePercent(value),
  91. color: theme.chartLabel,
  92. },
  93. };
  94. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  95. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  96. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  97. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  98. case ReleaseComparisonChartType.HEALTHY_USERS:
  99. case ReleaseComparisonChartType.ABNORMAL_USERS:
  100. case ReleaseComparisonChartType.ERRORED_USERS:
  101. case ReleaseComparisonChartType.CRASHED_USERS:
  102. return {
  103. scale: true,
  104. axisLabel: {
  105. formatter: (value: number) => `${round(value, 2)}%`,
  106. color: theme.chartLabel,
  107. },
  108. };
  109. case ReleaseComparisonChartType.SESSION_COUNT:
  110. case ReleaseComparisonChartType.SESSION_DURATION:
  111. case ReleaseComparisonChartType.USER_COUNT:
  112. default:
  113. return undefined;
  114. }
  115. }
  116. getChart():
  117. | React.ComponentType<StackedAreaChart['props']>
  118. | React.ComponentType<AreaChart['props']> {
  119. const {chartType} = this.props;
  120. switch (chartType) {
  121. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  122. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  123. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  124. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  125. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  126. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  127. case ReleaseComparisonChartType.HEALTHY_USERS:
  128. case ReleaseComparisonChartType.ABNORMAL_USERS:
  129. case ReleaseComparisonChartType.ERRORED_USERS:
  130. case ReleaseComparisonChartType.CRASHED_USERS:
  131. default:
  132. return AreaChart;
  133. case ReleaseComparisonChartType.SESSION_COUNT:
  134. case ReleaseComparisonChartType.SESSION_DURATION:
  135. case ReleaseComparisonChartType.USER_COUNT:
  136. return StackedAreaChart;
  137. }
  138. }
  139. getColors() {
  140. const {theme, chartType} = this.props;
  141. const colors = theme.charts.getColorPalette(14);
  142. switch (chartType) {
  143. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  144. return [colors[0]];
  145. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  146. return [theme.green300];
  147. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  148. return [colors[15]];
  149. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  150. return [colors[12]];
  151. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  152. return [theme.red300];
  153. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  154. return [colors[6]];
  155. case ReleaseComparisonChartType.HEALTHY_USERS:
  156. return [theme.green300];
  157. case ReleaseComparisonChartType.ABNORMAL_USERS:
  158. return [colors[15]];
  159. case ReleaseComparisonChartType.ERRORED_USERS:
  160. return [colors[12]];
  161. case ReleaseComparisonChartType.CRASHED_USERS:
  162. return [theme.red300];
  163. case ReleaseComparisonChartType.SESSION_COUNT:
  164. case ReleaseComparisonChartType.SESSION_DURATION:
  165. case ReleaseComparisonChartType.USER_COUNT:
  166. default:
  167. return undefined;
  168. }
  169. }
  170. getSeries(chartType: ReleaseComparisonChartType) {
  171. const {releaseSessions, allSessions, release, location, project, theme} = this.props;
  172. if (!releaseSessions) {
  173. return {};
  174. }
  175. const markLines = generateReleaseMarkLines(release, project, theme, location);
  176. switch (chartType) {
  177. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  178. return {
  179. series: [
  180. {
  181. seriesName: t('This Release'),
  182. connectNulls: true,
  183. data: getCrashFreeRateSeries(
  184. releaseSessions?.groups,
  185. releaseSessions?.intervals,
  186. SessionField.SESSIONS
  187. ),
  188. },
  189. ],
  190. previousSeries: [
  191. {
  192. seriesName: t('All Releases'),
  193. data: getCrashFreeRateSeries(
  194. allSessions?.groups,
  195. allSessions?.intervals,
  196. SessionField.SESSIONS
  197. ),
  198. },
  199. ],
  200. markLines,
  201. };
  202. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  203. return {
  204. series: [
  205. {
  206. seriesName: t('This Release'),
  207. connectNulls: true,
  208. data: getSessionStatusRateSeries(
  209. releaseSessions?.groups,
  210. releaseSessions?.intervals,
  211. SessionField.SESSIONS,
  212. SessionStatus.HEALTHY
  213. ),
  214. },
  215. ],
  216. previousSeries: [
  217. {
  218. seriesName: t('All Releases'),
  219. data: getSessionStatusRateSeries(
  220. allSessions?.groups,
  221. allSessions?.intervals,
  222. SessionField.SESSIONS,
  223. SessionStatus.HEALTHY
  224. ),
  225. },
  226. ],
  227. markLines,
  228. };
  229. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  230. return {
  231. series: [
  232. {
  233. seriesName: t('This Release'),
  234. connectNulls: true,
  235. data: getSessionStatusRateSeries(
  236. releaseSessions?.groups,
  237. releaseSessions?.intervals,
  238. SessionField.SESSIONS,
  239. SessionStatus.ABNORMAL
  240. ),
  241. },
  242. ],
  243. previousSeries: [
  244. {
  245. seriesName: t('All Releases'),
  246. data: getSessionStatusRateSeries(
  247. allSessions?.groups,
  248. allSessions?.intervals,
  249. SessionField.SESSIONS,
  250. SessionStatus.ABNORMAL
  251. ),
  252. },
  253. ],
  254. markLines,
  255. };
  256. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  257. return {
  258. series: [
  259. {
  260. seriesName: t('This Release'),
  261. connectNulls: true,
  262. data: getSessionStatusRateSeries(
  263. releaseSessions?.groups,
  264. releaseSessions?.intervals,
  265. SessionField.SESSIONS,
  266. SessionStatus.ERRORED
  267. ),
  268. },
  269. ],
  270. previousSeries: [
  271. {
  272. seriesName: t('All Releases'),
  273. data: getSessionStatusRateSeries(
  274. allSessions?.groups,
  275. allSessions?.intervals,
  276. SessionField.SESSIONS,
  277. SessionStatus.ERRORED
  278. ),
  279. },
  280. ],
  281. markLines,
  282. };
  283. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  284. return {
  285. series: [
  286. {
  287. seriesName: t('This Release'),
  288. connectNulls: true,
  289. data: getSessionStatusRateSeries(
  290. releaseSessions?.groups,
  291. releaseSessions?.intervals,
  292. SessionField.SESSIONS,
  293. SessionStatus.CRASHED
  294. ),
  295. },
  296. ],
  297. previousSeries: [
  298. {
  299. seriesName: t('All Releases'),
  300. data: getSessionStatusRateSeries(
  301. allSessions?.groups,
  302. allSessions?.intervals,
  303. SessionField.SESSIONS,
  304. SessionStatus.CRASHED
  305. ),
  306. },
  307. ],
  308. markLines,
  309. };
  310. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  311. return {
  312. series: [
  313. {
  314. seriesName: t('This Release'),
  315. connectNulls: true,
  316. data: getCrashFreeRateSeries(
  317. releaseSessions?.groups,
  318. releaseSessions?.intervals,
  319. SessionField.USERS
  320. ),
  321. },
  322. ],
  323. previousSeries: [
  324. {
  325. seriesName: t('All Releases'),
  326. data: getCrashFreeRateSeries(
  327. allSessions?.groups,
  328. allSessions?.intervals,
  329. SessionField.USERS
  330. ),
  331. },
  332. ],
  333. markLines,
  334. };
  335. case ReleaseComparisonChartType.HEALTHY_USERS:
  336. return {
  337. series: [
  338. {
  339. seriesName: t('This Release'),
  340. connectNulls: true,
  341. data: getSessionStatusRateSeries(
  342. releaseSessions?.groups,
  343. releaseSessions?.intervals,
  344. SessionField.USERS,
  345. SessionStatus.HEALTHY
  346. ),
  347. },
  348. ],
  349. previousSeries: [
  350. {
  351. seriesName: t('All Releases'),
  352. data: getSessionStatusRateSeries(
  353. allSessions?.groups,
  354. allSessions?.intervals,
  355. SessionField.USERS,
  356. SessionStatus.HEALTHY
  357. ),
  358. },
  359. ],
  360. markLines,
  361. };
  362. case ReleaseComparisonChartType.ABNORMAL_USERS:
  363. return {
  364. series: [
  365. {
  366. seriesName: t('This Release'),
  367. connectNulls: true,
  368. data: getSessionStatusRateSeries(
  369. releaseSessions?.groups,
  370. releaseSessions?.intervals,
  371. SessionField.USERS,
  372. SessionStatus.ABNORMAL
  373. ),
  374. },
  375. ],
  376. previousSeries: [
  377. {
  378. seriesName: t('All Releases'),
  379. data: getSessionStatusRateSeries(
  380. allSessions?.groups,
  381. allSessions?.intervals,
  382. SessionField.USERS,
  383. SessionStatus.ABNORMAL
  384. ),
  385. },
  386. ],
  387. markLines,
  388. };
  389. case ReleaseComparisonChartType.ERRORED_USERS:
  390. return {
  391. series: [
  392. {
  393. seriesName: t('This Release'),
  394. connectNulls: true,
  395. data: getSessionStatusRateSeries(
  396. releaseSessions?.groups,
  397. releaseSessions?.intervals,
  398. SessionField.USERS,
  399. SessionStatus.ERRORED
  400. ),
  401. },
  402. ],
  403. previousSeries: [
  404. {
  405. seriesName: t('All Releases'),
  406. data: getSessionStatusRateSeries(
  407. allSessions?.groups,
  408. allSessions?.intervals,
  409. SessionField.USERS,
  410. SessionStatus.ERRORED
  411. ),
  412. },
  413. ],
  414. markLines,
  415. };
  416. case ReleaseComparisonChartType.CRASHED_USERS:
  417. return {
  418. series: [
  419. {
  420. seriesName: t('This Release'),
  421. connectNulls: true,
  422. data: getSessionStatusRateSeries(
  423. releaseSessions?.groups,
  424. releaseSessions?.intervals,
  425. SessionField.USERS,
  426. SessionStatus.CRASHED
  427. ),
  428. },
  429. ],
  430. previousSeries: [
  431. {
  432. seriesName: t('All Releases'),
  433. data: getSessionStatusRateSeries(
  434. allSessions?.groups,
  435. allSessions?.intervals,
  436. SessionField.USERS,
  437. SessionStatus.CRASHED
  438. ),
  439. },
  440. ],
  441. markLines,
  442. };
  443. case ReleaseComparisonChartType.SESSION_COUNT:
  444. return {
  445. series: Object.values(
  446. fillChartDataFromSessionsResponse({
  447. response: releaseSessions,
  448. field: SessionField.SESSIONS,
  449. groupBy: 'session.status',
  450. chartData: initSessionsBreakdownChartData(theme),
  451. })
  452. ),
  453. markLines,
  454. };
  455. case ReleaseComparisonChartType.SESSION_DURATION:
  456. return {
  457. series: Object.values(
  458. fillChartDataFromSessionsResponse({
  459. response: releaseSessions,
  460. field: SessionField.DURATION,
  461. groupBy: 'session.status',
  462. chartData: initSessionsBreakdownChartData(theme),
  463. })
  464. ),
  465. markLines,
  466. };
  467. case ReleaseComparisonChartType.USER_COUNT:
  468. return {
  469. series: Object.values(
  470. fillChartDataFromSessionsResponse({
  471. response: releaseSessions,
  472. field: SessionField.USERS,
  473. groupBy: 'session.status',
  474. chartData: initSessionsBreakdownChartData(theme),
  475. })
  476. ),
  477. markLines,
  478. };
  479. default:
  480. return {};
  481. }
  482. }
  483. render() {
  484. const {chartType, router, period, start, end, utc, value, diff, loading, reloading} =
  485. this.props;
  486. const Chart = this.getChart();
  487. const {series, previousSeries, markLines} = this.getSeries(chartType);
  488. const legend = {
  489. right: 10,
  490. top: 0,
  491. data: [...(series ?? []), ...(previousSeries ?? [])].map(s => s.seriesName),
  492. };
  493. return (
  494. <TransitionChart loading={loading} reloading={reloading} height="240px">
  495. <TransparentLoadingMask visible={reloading} />
  496. <HeaderTitleLegend aria-label={t('Chart Title')}>
  497. {releaseComparisonChartTitles[chartType]}
  498. {releaseComparisonChartHelp[chartType] && (
  499. <QuestionTooltip
  500. size="sm"
  501. position="top"
  502. title={releaseComparisonChartHelp[chartType]}
  503. />
  504. )}
  505. </HeaderTitleLegend>
  506. <HeaderValue aria-label={t('Chart Value')}>
  507. {value} {diff}
  508. </HeaderValue>
  509. <ChartZoom
  510. router={router}
  511. period={period}
  512. utc={utc}
  513. start={start}
  514. end={end}
  515. usePageDate
  516. >
  517. {zoomRenderProps => (
  518. <Chart
  519. legend={legend}
  520. series={[...(series ?? []), ...(markLines ?? [])]}
  521. previousPeriod={previousSeries ?? []}
  522. {...zoomRenderProps}
  523. grid={{
  524. left: '10px',
  525. right: '10px',
  526. top: '70px',
  527. bottom: '0px',
  528. }}
  529. yAxis={this.getYAxis()}
  530. tooltip={{valueFormatter: this.formatTooltipValue}}
  531. colors={this.getColors()}
  532. transformSinglePointToBar
  533. height={240}
  534. />
  535. )}
  536. </ChartZoom>
  537. </TransitionChart>
  538. );
  539. }
  540. }
  541. export default withTheme(withRouter(ReleaseSessionsChart));