releaseSessionsChart.tsx 20 KB

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