releaseSessionsChart.tsx 21 KB

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