releaseSessionsChart.tsx 20 KB

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