releaseSessionsChart.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. import {Component} from 'react';
  2. import {WithRouterProps} from 'react-router';
  3. import {Theme, withTheme} from '@emotion/react';
  4. import round from 'lodash/round';
  5. import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart';
  6. import ChartZoom from 'sentry/components/charts/chartZoom';
  7. import StackedAreaChart from 'sentry/components/charts/stackedAreaChart';
  8. import {HeaderTitleLegend, HeaderValue} from 'sentry/components/charts/styles';
  9. import TransitionChart from 'sentry/components/charts/transitionChart';
  10. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  11. import QuestionTooltip from 'sentry/components/questionTooltip';
  12. import {PlatformKey} from 'sentry/data/platformCategories';
  13. import {t} from 'sentry/locale';
  14. import {
  15. ReleaseComparisonChartType,
  16. ReleaseProject,
  17. ReleaseWithHealth,
  18. SessionApiResponse,
  19. SessionFieldWithOperation,
  20. SessionStatus,
  21. } from 'sentry/types';
  22. import {defined} from 'sentry/utils';
  23. import {
  24. getCountSeries,
  25. getCrashFreeRateSeries,
  26. getSessionStatusRateSeries,
  27. initSessionsChart,
  28. MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
  29. } from 'sentry/utils/sessions';
  30. // eslint-disable-next-line no-restricted-imports
  31. import withSentryRouter from 'sentry/utils/withSentryRouter';
  32. import {displayCrashFreePercent} from 'sentry/views/releases/utils';
  33. import {
  34. generateReleaseMarkLines,
  35. releaseComparisonChartHelp,
  36. releaseComparisonChartTitles,
  37. releaseMarkLinesLabels,
  38. } from '../../utils';
  39. type Props = {
  40. allSessions: SessionApiResponse | null;
  41. chartType: ReleaseComparisonChartType;
  42. diff: React.ReactNode;
  43. loading: boolean;
  44. platform: PlatformKey;
  45. project: ReleaseProject;
  46. release: ReleaseWithHealth;
  47. releaseSessions: SessionApiResponse | null;
  48. reloading: boolean;
  49. theme: Theme;
  50. value: React.ReactNode;
  51. end?: string;
  52. period?: string | null;
  53. start?: string;
  54. utc?: boolean;
  55. } & WithRouterProps;
  56. class ReleaseSessionsChart extends Component<Props> {
  57. formatTooltipValue = (value: string | number | null, label?: string) => {
  58. if (label && Object.values(releaseMarkLinesLabels).includes(label)) {
  59. return '';
  60. }
  61. const {chartType} = this.props;
  62. if (value === null) {
  63. return '\u2015';
  64. }
  65. switch (chartType) {
  66. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  67. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  68. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  69. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  70. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  71. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  72. case ReleaseComparisonChartType.HEALTHY_USERS:
  73. case ReleaseComparisonChartType.ABNORMAL_USERS:
  74. case ReleaseComparisonChartType.ERRORED_USERS:
  75. case ReleaseComparisonChartType.CRASHED_USERS:
  76. return defined(value) ? `${value}%` : '\u2015';
  77. case ReleaseComparisonChartType.SESSION_COUNT:
  78. case ReleaseComparisonChartType.USER_COUNT:
  79. default:
  80. return typeof value === 'number' ? value.toLocaleString() : value;
  81. }
  82. };
  83. getYAxis() {
  84. const {theme, chartType} = this.props;
  85. switch (chartType) {
  86. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  87. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  88. return {
  89. max: 100,
  90. scale: true,
  91. axisLabel: {
  92. formatter: (value: number) => displayCrashFreePercent(value),
  93. color: theme.chartLabel,
  94. },
  95. };
  96. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  97. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  98. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  99. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  100. case ReleaseComparisonChartType.HEALTHY_USERS:
  101. case ReleaseComparisonChartType.ABNORMAL_USERS:
  102. case ReleaseComparisonChartType.ERRORED_USERS:
  103. case ReleaseComparisonChartType.CRASHED_USERS:
  104. return {
  105. scale: true,
  106. axisLabel: {
  107. formatter: (value: number) => `${round(value, 2)}%`,
  108. color: theme.chartLabel,
  109. },
  110. };
  111. case ReleaseComparisonChartType.SESSION_COUNT:
  112. case ReleaseComparisonChartType.USER_COUNT:
  113. default:
  114. return undefined;
  115. }
  116. }
  117. getChart():
  118. | React.ComponentType<StackedAreaChart['props']>
  119. | React.ComponentType<AreaChartProps> {
  120. const {chartType} = this.props;
  121. switch (chartType) {
  122. case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
  123. case ReleaseComparisonChartType.HEALTHY_SESSIONS:
  124. case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
  125. case ReleaseComparisonChartType.ERRORED_SESSIONS:
  126. case ReleaseComparisonChartType.CRASHED_SESSIONS:
  127. case ReleaseComparisonChartType.CRASH_FREE_USERS:
  128. case ReleaseComparisonChartType.HEALTHY_USERS:
  129. case ReleaseComparisonChartType.ABNORMAL_USERS:
  130. case ReleaseComparisonChartType.ERRORED_USERS:
  131. case ReleaseComparisonChartType.CRASHED_USERS:
  132. default:
  133. return AreaChart;
  134. case ReleaseComparisonChartType.SESSION_COUNT:
  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.USER_COUNT:
  165. default:
  166. return undefined;
  167. }
  168. }
  169. getSeries(chartType: ReleaseComparisonChartType) {
  170. const {releaseSessions, allSessions, release, location, project, theme} = this.props;
  171. const countCharts = initSessionsChart(theme);
  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. SessionFieldWithOperation.SESSIONS
  187. ),
  188. },
  189. ],
  190. previousSeries: [
  191. {
  192. seriesName: t('All Releases'),
  193. data: getCrashFreeRateSeries(
  194. allSessions?.groups,
  195. allSessions?.intervals,
  196. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.USERS
  320. ),
  321. },
  322. ],
  323. previousSeries: [
  324. {
  325. seriesName: t('All Releases'),
  326. data: getCrashFreeRateSeries(
  327. allSessions?.groups,
  328. allSessions?.intervals,
  329. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.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. SessionFieldWithOperation.USERS,
  437. SessionStatus.CRASHED
  438. ),
  439. },
  440. ],
  441. markLines,
  442. };
  443. case ReleaseComparisonChartType.SESSION_COUNT:
  444. return {
  445. series: [
  446. {
  447. ...countCharts[SessionStatus.HEALTHY],
  448. data: getCountSeries(
  449. SessionFieldWithOperation.SESSIONS,
  450. releaseSessions.groups.find(
  451. g => g.by['session.status'] === SessionStatus.HEALTHY
  452. ),
  453. releaseSessions.intervals
  454. ),
  455. },
  456. {
  457. ...countCharts[SessionStatus.ERRORED],
  458. data: getCountSeries(
  459. SessionFieldWithOperation.SESSIONS,
  460. releaseSessions.groups.find(
  461. g => g.by['session.status'] === SessionStatus.ERRORED
  462. ),
  463. releaseSessions.intervals
  464. ),
  465. },
  466. {
  467. ...countCharts[SessionStatus.ABNORMAL],
  468. data: getCountSeries(
  469. SessionFieldWithOperation.SESSIONS,
  470. releaseSessions.groups.find(
  471. g => g.by['session.status'] === SessionStatus.ABNORMAL
  472. ),
  473. releaseSessions.intervals
  474. ),
  475. },
  476. {
  477. ...countCharts[SessionStatus.CRASHED],
  478. data: getCountSeries(
  479. SessionFieldWithOperation.SESSIONS,
  480. releaseSessions.groups.find(
  481. g => g.by['session.status'] === SessionStatus.CRASHED
  482. ),
  483. releaseSessions.intervals
  484. ),
  485. },
  486. ],
  487. markLines,
  488. };
  489. case ReleaseComparisonChartType.USER_COUNT:
  490. return {
  491. series: [
  492. {
  493. ...countCharts[SessionStatus.HEALTHY],
  494. data: getCountSeries(
  495. SessionFieldWithOperation.USERS,
  496. releaseSessions.groups.find(
  497. g => g.by['session.status'] === SessionStatus.HEALTHY
  498. ),
  499. releaseSessions.intervals
  500. ),
  501. },
  502. {
  503. ...countCharts[SessionStatus.ERRORED],
  504. data: getCountSeries(
  505. SessionFieldWithOperation.USERS,
  506. releaseSessions.groups.find(
  507. g => g.by['session.status'] === SessionStatus.ERRORED
  508. ),
  509. releaseSessions.intervals
  510. ),
  511. },
  512. {
  513. ...countCharts[SessionStatus.ABNORMAL],
  514. data: getCountSeries(
  515. SessionFieldWithOperation.USERS,
  516. releaseSessions.groups.find(
  517. g => g.by['session.status'] === SessionStatus.ABNORMAL
  518. ),
  519. releaseSessions.intervals
  520. ),
  521. },
  522. {
  523. ...countCharts[SessionStatus.CRASHED],
  524. data: getCountSeries(
  525. SessionFieldWithOperation.USERS,
  526. releaseSessions.groups.find(
  527. g => g.by['session.status'] === SessionStatus.CRASHED
  528. ),
  529. releaseSessions.intervals
  530. ),
  531. },
  532. ],
  533. markLines,
  534. };
  535. default:
  536. return {};
  537. }
  538. }
  539. render() {
  540. const {chartType, router, period, start, end, utc, value, diff, loading, reloading} =
  541. this.props;
  542. const Chart = this.getChart();
  543. const {series, previousSeries, markLines} = this.getSeries(chartType);
  544. const legend = {
  545. right: 10,
  546. top: 0,
  547. textStyle: {
  548. padding: [2, 0, 0, 0],
  549. },
  550. data: [...(series ?? []), ...(previousSeries ?? [])].map(s => s.seriesName),
  551. };
  552. return (
  553. <TransitionChart loading={loading} reloading={reloading} height="240px">
  554. <TransparentLoadingMask visible={reloading} />
  555. <HeaderTitleLegend aria-label={t('Chart Title')}>
  556. {releaseComparisonChartTitles[chartType]}
  557. {releaseComparisonChartHelp[chartType] && (
  558. <QuestionTooltip
  559. size="sm"
  560. position="top"
  561. title={releaseComparisonChartHelp[chartType]}
  562. />
  563. )}
  564. </HeaderTitleLegend>
  565. <HeaderValue aria-label={t('Chart Value')}>
  566. {value} {diff}
  567. </HeaderValue>
  568. <ChartZoom
  569. router={router}
  570. period={period}
  571. utc={utc}
  572. start={start}
  573. end={end}
  574. usePageDate
  575. >
  576. {zoomRenderProps => (
  577. <Chart
  578. legend={legend}
  579. series={[...(series ?? []), ...(markLines ?? [])]}
  580. previousPeriod={previousSeries ?? []}
  581. {...zoomRenderProps}
  582. grid={{
  583. left: '10px',
  584. right: '10px',
  585. top: '70px',
  586. bottom: '0px',
  587. }}
  588. minutesThresholdToDisplaySeconds={MINUTES_THRESHOLD_TO_DISPLAY_SECONDS}
  589. yAxis={this.getYAxis()}
  590. tooltip={{valueFormatter: this.formatTooltipValue}}
  591. colors={this.getColors()}
  592. transformSinglePointToBar
  593. height={240}
  594. />
  595. )}
  596. </ChartZoom>
  597. </TransitionChart>
  598. );
  599. }
  600. }
  601. export default withTheme(withSentryRouter(ReleaseSessionsChart));