index.tsx 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import {Location} from 'history';
  6. import {Client} from 'sentry/api';
  7. import {Button} from 'sentry/components/button';
  8. import ErrorPanel from 'sentry/components/charts/errorPanel';
  9. import {ChartContainer} from 'sentry/components/charts/styles';
  10. import Count from 'sentry/components/count';
  11. import ErrorBoundary from 'sentry/components/errorBoundary';
  12. import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
  13. import NotAvailable from 'sentry/components/notAvailable';
  14. import {Panel, PanelTable} from 'sentry/components/panels';
  15. import {Tooltip} from 'sentry/components/tooltip';
  16. import {PlatformKey} from 'sentry/data/platformCategories';
  17. import {IconArrow, IconChevron, IconList, IconWarning} from 'sentry/icons';
  18. import {t, tct, tn} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import {
  21. Organization,
  22. ReleaseComparisonChartType,
  23. ReleaseProject,
  24. ReleaseWithHealth,
  25. SessionApiResponse,
  26. SessionFieldWithOperation,
  27. SessionStatus,
  28. } from 'sentry/types';
  29. import {defined} from 'sentry/utils';
  30. import {trackAnalytics} from 'sentry/utils/analytics';
  31. import {formatPercentage} from 'sentry/utils/formatters';
  32. import getDynamicText from 'sentry/utils/getDynamicText';
  33. import {decodeList, decodeScalar} from 'sentry/utils/queryString';
  34. import {getCount, getCrashFreeRate, getSessionStatusRate} from 'sentry/utils/sessions';
  35. import {Color} from 'sentry/utils/theme';
  36. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  37. import {
  38. displaySessionStatusPercent,
  39. getReleaseBounds,
  40. getReleaseHandledIssuesUrl,
  41. getReleaseParams,
  42. getReleaseUnhandledIssuesUrl,
  43. } from 'sentry/views/releases/utils';
  44. import ReleaseComparisonChartRow from './releaseComparisonChartRow';
  45. import ReleaseEventsChart from './releaseEventsChart';
  46. import ReleaseSessionsChart from './releaseSessionsChart';
  47. export type ReleaseComparisonRow = {
  48. allReleases: React.ReactNode;
  49. diff: React.ReactNode;
  50. diffColor: Color | null;
  51. diffDirection: 'up' | 'down' | null;
  52. drilldown: React.ReactNode;
  53. role: 'parent' | 'children' | 'default';
  54. thisRelease: React.ReactNode;
  55. type: ReleaseComparisonChartType;
  56. };
  57. type Props = {
  58. allSessions: SessionApiResponse | null;
  59. api: Client;
  60. errored: boolean;
  61. hasHealthData: boolean;
  62. loading: boolean;
  63. location: Location;
  64. organization: Organization;
  65. platform: PlatformKey;
  66. project: ReleaseProject;
  67. release: ReleaseWithHealth;
  68. releaseSessions: SessionApiResponse | null;
  69. reloading: boolean;
  70. };
  71. type EventsTotals = {
  72. allErrorCount: number;
  73. allFailureRate: number;
  74. allTransactionCount: number;
  75. releaseErrorCount: number;
  76. releaseFailureRate: number;
  77. releaseTransactionCount: number;
  78. } | null;
  79. type IssuesTotals = {
  80. handled: number;
  81. unhandled: number;
  82. } | null;
  83. function ReleaseComparisonChart({
  84. release,
  85. project,
  86. releaseSessions,
  87. allSessions,
  88. platform,
  89. location,
  90. loading,
  91. reloading,
  92. errored,
  93. api,
  94. organization,
  95. hasHealthData,
  96. }: Props) {
  97. const [issuesTotals, setIssuesTotals] = useState<IssuesTotals>(null);
  98. const [eventsTotals, setEventsTotals] = useState<EventsTotals>(null);
  99. const [eventsLoading, setEventsLoading] = useState(false);
  100. const [expanded, setExpanded] = useState(new Set());
  101. const [isOtherExpanded, setIsOtherExpanded] = useState(false);
  102. const charts: ReleaseComparisonRow[] = [];
  103. const additionalCharts: ReleaseComparisonRow[] = [];
  104. const hasDiscover =
  105. organization.features.includes('discover-basic') ||
  106. organization.features.includes('performance-view');
  107. const hasPerformance = organization.features.includes('performance-view');
  108. const {
  109. statsPeriod: period,
  110. start,
  111. end,
  112. utc,
  113. } = useMemo(
  114. () =>
  115. // Memoizing this so that it does not calculate different `end` for releases without events+sessions each rerender
  116. getReleaseParams({
  117. location,
  118. releaseBounds: getReleaseBounds(release),
  119. }),
  120. [release, location]
  121. );
  122. useEffect(() => {
  123. const chartInUrl = decodeScalar(location.query.chart) as ReleaseComparisonChartType;
  124. if (
  125. [
  126. ReleaseComparisonChartType.HEALTHY_SESSIONS,
  127. ReleaseComparisonChartType.ABNORMAL_SESSIONS,
  128. ReleaseComparisonChartType.ERRORED_SESSIONS,
  129. ReleaseComparisonChartType.CRASHED_SESSIONS,
  130. ].includes(chartInUrl)
  131. ) {
  132. setExpanded(e => new Set(e.add(ReleaseComparisonChartType.CRASH_FREE_SESSIONS)));
  133. }
  134. if (
  135. [
  136. ReleaseComparisonChartType.HEALTHY_USERS,
  137. ReleaseComparisonChartType.ABNORMAL_USERS,
  138. ReleaseComparisonChartType.ERRORED_USERS,
  139. ReleaseComparisonChartType.CRASHED_USERS,
  140. ].includes(chartInUrl)
  141. ) {
  142. setExpanded(e => new Set(e.add(ReleaseComparisonChartType.CRASH_FREE_USERS)));
  143. }
  144. if (
  145. [
  146. ReleaseComparisonChartType.SESSION_COUNT,
  147. ReleaseComparisonChartType.USER_COUNT,
  148. ReleaseComparisonChartType.ERROR_COUNT,
  149. ReleaseComparisonChartType.TRANSACTION_COUNT,
  150. ].includes(chartInUrl)
  151. ) {
  152. setIsOtherExpanded(true);
  153. }
  154. }, [location.query.chart]);
  155. const fetchEventsTotals = useCallback(async () => {
  156. const url = `/organizations/${organization.slug}/events/`;
  157. const commonQuery = {
  158. environment: decodeList(location.query.environment),
  159. project: decodeList(location.query.project),
  160. start,
  161. end,
  162. ...(period ? {statsPeriod: period} : {}),
  163. };
  164. setEventsLoading(true);
  165. try {
  166. const [
  167. releaseTransactionTotals,
  168. allTransactionTotals,
  169. releaseErrorTotals,
  170. allErrorTotals,
  171. ] = await Promise.all([
  172. api.requestPromise(url, {
  173. query: {
  174. field: ['failure_rate()', 'count()'],
  175. query: new MutableSearch([
  176. 'event.type:transaction',
  177. `release:${release.version}`,
  178. ]).formatString(),
  179. ...commonQuery,
  180. },
  181. }),
  182. api.requestPromise(url, {
  183. query: {
  184. field: ['failure_rate()', 'count()'],
  185. query: new MutableSearch(['event.type:transaction']).formatString(),
  186. ...commonQuery,
  187. },
  188. }),
  189. api.requestPromise(url, {
  190. query: {
  191. field: ['count()'],
  192. query: new MutableSearch([
  193. 'event.type:error',
  194. `release:${release.version}`,
  195. ]).formatString(),
  196. ...commonQuery,
  197. },
  198. }),
  199. api.requestPromise(url, {
  200. query: {
  201. field: ['count()'],
  202. query: new MutableSearch(['event.type:error']).formatString(),
  203. ...commonQuery,
  204. },
  205. }),
  206. ]);
  207. setEventsTotals({
  208. allErrorCount: allErrorTotals.data[0]['count()'],
  209. releaseErrorCount: releaseErrorTotals.data[0]['count()'],
  210. allTransactionCount: allTransactionTotals.data[0]['count()'],
  211. releaseTransactionCount: releaseTransactionTotals.data[0]['count()'],
  212. releaseFailureRate: releaseTransactionTotals.data[0]['failure_rate()'],
  213. allFailureRate: allTransactionTotals.data[0]['failure_rate()'],
  214. });
  215. setEventsLoading(false);
  216. } catch (err) {
  217. setEventsTotals(null);
  218. setEventsLoading(false);
  219. Sentry.captureException(err);
  220. }
  221. }, [
  222. api,
  223. end,
  224. location.query.environment,
  225. location.query.project,
  226. organization.slug,
  227. period,
  228. release.version,
  229. start,
  230. ]);
  231. const fetchIssuesTotals = useCallback(async () => {
  232. const UNHANDLED_QUERY = `release:"${release.version}" error.handled:0`;
  233. const HANDLED_QUERY = `release:"${release.version}" error.handled:1`;
  234. try {
  235. const response = await api.requestPromise(
  236. `/organizations/${organization.slug}/issues-count/`,
  237. {
  238. query: {
  239. project: project.id,
  240. environment: decodeList(location.query.environment),
  241. start,
  242. end,
  243. ...(period ? {statsPeriod: period} : {}),
  244. query: [UNHANDLED_QUERY, HANDLED_QUERY],
  245. },
  246. }
  247. );
  248. setIssuesTotals({
  249. handled: response[HANDLED_QUERY] ?? 0,
  250. unhandled: response[UNHANDLED_QUERY] ?? 0,
  251. });
  252. } catch (err) {
  253. setIssuesTotals(null);
  254. Sentry.captureException(err);
  255. }
  256. }, [
  257. api,
  258. end,
  259. location.query.environment,
  260. organization.slug,
  261. period,
  262. project.id,
  263. release.version,
  264. start,
  265. ]);
  266. useEffect(() => {
  267. if (hasDiscover || hasPerformance) {
  268. fetchEventsTotals();
  269. fetchIssuesTotals();
  270. }
  271. }, [fetchEventsTotals, fetchIssuesTotals, hasDiscover, hasPerformance]);
  272. const releaseCrashFreeSessions = getCrashFreeRate(
  273. releaseSessions?.groups,
  274. SessionFieldWithOperation.SESSIONS
  275. );
  276. const allCrashFreeSessions = getCrashFreeRate(
  277. allSessions?.groups,
  278. SessionFieldWithOperation.SESSIONS
  279. );
  280. const diffCrashFreeSessions =
  281. defined(releaseCrashFreeSessions) && defined(allCrashFreeSessions)
  282. ? releaseCrashFreeSessions - allCrashFreeSessions
  283. : null;
  284. const releaseHealthySessions = getSessionStatusRate(
  285. releaseSessions?.groups,
  286. SessionFieldWithOperation.SESSIONS,
  287. SessionStatus.HEALTHY
  288. );
  289. const allHealthySessions = getSessionStatusRate(
  290. allSessions?.groups,
  291. SessionFieldWithOperation.SESSIONS,
  292. SessionStatus.HEALTHY
  293. );
  294. const diffHealthySessions =
  295. defined(releaseHealthySessions) && defined(allHealthySessions)
  296. ? releaseHealthySessions - allHealthySessions
  297. : null;
  298. const releaseAbnormalSessions = getSessionStatusRate(
  299. releaseSessions?.groups,
  300. SessionFieldWithOperation.SESSIONS,
  301. SessionStatus.ABNORMAL
  302. );
  303. const allAbnormalSessions = getSessionStatusRate(
  304. allSessions?.groups,
  305. SessionFieldWithOperation.SESSIONS,
  306. SessionStatus.ABNORMAL
  307. );
  308. const diffAbnormalSessions =
  309. defined(releaseAbnormalSessions) && defined(allAbnormalSessions)
  310. ? releaseAbnormalSessions - allAbnormalSessions
  311. : null;
  312. const releaseErroredSessions = getSessionStatusRate(
  313. releaseSessions?.groups,
  314. SessionFieldWithOperation.SESSIONS,
  315. SessionStatus.ERRORED
  316. );
  317. const allErroredSessions = getSessionStatusRate(
  318. allSessions?.groups,
  319. SessionFieldWithOperation.SESSIONS,
  320. SessionStatus.ERRORED
  321. );
  322. const diffErroredSessions =
  323. defined(releaseErroredSessions) && defined(allErroredSessions)
  324. ? releaseErroredSessions - allErroredSessions
  325. : null;
  326. const releaseCrashedSessions = getSessionStatusRate(
  327. releaseSessions?.groups,
  328. SessionFieldWithOperation.SESSIONS,
  329. SessionStatus.CRASHED
  330. );
  331. const allCrashedSessions = getSessionStatusRate(
  332. allSessions?.groups,
  333. SessionFieldWithOperation.SESSIONS,
  334. SessionStatus.CRASHED
  335. );
  336. const diffCrashedSessions =
  337. defined(releaseCrashedSessions) && defined(allCrashedSessions)
  338. ? releaseCrashedSessions - allCrashedSessions
  339. : null;
  340. const releaseCrashFreeUsers = getCrashFreeRate(
  341. releaseSessions?.groups,
  342. SessionFieldWithOperation.USERS
  343. );
  344. const allCrashFreeUsers = getCrashFreeRate(
  345. allSessions?.groups,
  346. SessionFieldWithOperation.USERS
  347. );
  348. const diffCrashFreeUsers =
  349. defined(releaseCrashFreeUsers) && defined(allCrashFreeUsers)
  350. ? releaseCrashFreeUsers - allCrashFreeUsers
  351. : null;
  352. const releaseHealthyUsers = getSessionStatusRate(
  353. releaseSessions?.groups,
  354. SessionFieldWithOperation.USERS,
  355. SessionStatus.HEALTHY
  356. );
  357. const allHealthyUsers = getSessionStatusRate(
  358. allSessions?.groups,
  359. SessionFieldWithOperation.USERS,
  360. SessionStatus.HEALTHY
  361. );
  362. const diffHealthyUsers =
  363. defined(releaseHealthyUsers) && defined(allHealthyUsers)
  364. ? releaseHealthyUsers - allHealthyUsers
  365. : null;
  366. const releaseAbnormalUsers = getSessionStatusRate(
  367. releaseSessions?.groups,
  368. SessionFieldWithOperation.USERS,
  369. SessionStatus.ABNORMAL
  370. );
  371. const allAbnormalUsers = getSessionStatusRate(
  372. allSessions?.groups,
  373. SessionFieldWithOperation.USERS,
  374. SessionStatus.ABNORMAL
  375. );
  376. const diffAbnormalUsers =
  377. defined(releaseAbnormalUsers) && defined(allAbnormalUsers)
  378. ? releaseAbnormalUsers - allAbnormalUsers
  379. : null;
  380. const releaseErroredUsers = getSessionStatusRate(
  381. releaseSessions?.groups,
  382. SessionFieldWithOperation.USERS,
  383. SessionStatus.ERRORED
  384. );
  385. const allErroredUsers = getSessionStatusRate(
  386. allSessions?.groups,
  387. SessionFieldWithOperation.USERS,
  388. SessionStatus.ERRORED
  389. );
  390. const diffErroredUsers =
  391. defined(releaseErroredUsers) && defined(allErroredUsers)
  392. ? releaseErroredUsers - allErroredUsers
  393. : null;
  394. const releaseCrashedUsers = getSessionStatusRate(
  395. releaseSessions?.groups,
  396. SessionFieldWithOperation.USERS,
  397. SessionStatus.CRASHED
  398. );
  399. const allCrashedUsers = getSessionStatusRate(
  400. allSessions?.groups,
  401. SessionFieldWithOperation.USERS,
  402. SessionStatus.CRASHED
  403. );
  404. const diffCrashedUsers =
  405. defined(releaseCrashedUsers) && defined(allCrashedUsers)
  406. ? releaseCrashedUsers - allCrashedUsers
  407. : null;
  408. const releaseSessionsCount = getCount(
  409. releaseSessions?.groups,
  410. SessionFieldWithOperation.SESSIONS
  411. );
  412. const allSessionsCount = getCount(
  413. allSessions?.groups,
  414. SessionFieldWithOperation.SESSIONS
  415. );
  416. const releaseUsersCount = getCount(
  417. releaseSessions?.groups,
  418. SessionFieldWithOperation.USERS
  419. );
  420. const allUsersCount = getCount(allSessions?.groups, SessionFieldWithOperation.USERS);
  421. const diffFailure =
  422. eventsTotals?.releaseFailureRate && eventsTotals?.allFailureRate
  423. ? eventsTotals.releaseFailureRate - eventsTotals.allFailureRate
  424. : null;
  425. if (hasHealthData) {
  426. charts.push({
  427. type: ReleaseComparisonChartType.CRASH_FREE_SESSIONS,
  428. role: 'parent',
  429. drilldown: null,
  430. thisRelease: defined(releaseCrashFreeSessions)
  431. ? displaySessionStatusPercent(releaseCrashFreeSessions)
  432. : null,
  433. allReleases: defined(allCrashFreeSessions)
  434. ? displaySessionStatusPercent(allCrashFreeSessions)
  435. : null,
  436. diff: defined(diffCrashFreeSessions)
  437. ? displaySessionStatusPercent(diffCrashFreeSessions)
  438. : null,
  439. diffDirection: diffCrashFreeSessions
  440. ? diffCrashFreeSessions > 0
  441. ? 'up'
  442. : 'down'
  443. : null,
  444. diffColor: diffCrashFreeSessions
  445. ? diffCrashFreeSessions > 0
  446. ? 'green300'
  447. : 'red300'
  448. : null,
  449. });
  450. if (expanded.has(ReleaseComparisonChartType.CRASH_FREE_SESSIONS)) {
  451. charts.push(
  452. {
  453. type: ReleaseComparisonChartType.HEALTHY_SESSIONS,
  454. role: 'children',
  455. drilldown: null,
  456. thisRelease: defined(releaseHealthySessions)
  457. ? displaySessionStatusPercent(releaseHealthySessions)
  458. : null,
  459. allReleases: defined(allHealthySessions)
  460. ? displaySessionStatusPercent(allHealthySessions)
  461. : null,
  462. diff: defined(diffHealthySessions)
  463. ? displaySessionStatusPercent(diffHealthySessions)
  464. : null,
  465. diffDirection: diffHealthySessions
  466. ? diffHealthySessions > 0
  467. ? 'up'
  468. : 'down'
  469. : null,
  470. diffColor: diffHealthySessions
  471. ? diffHealthySessions > 0
  472. ? 'green300'
  473. : 'red300'
  474. : null,
  475. },
  476. {
  477. type: ReleaseComparisonChartType.ABNORMAL_SESSIONS,
  478. role: 'children',
  479. drilldown: null,
  480. thisRelease: defined(releaseAbnormalSessions)
  481. ? displaySessionStatusPercent(releaseAbnormalSessions)
  482. : null,
  483. allReleases: defined(allAbnormalSessions)
  484. ? displaySessionStatusPercent(allAbnormalSessions)
  485. : null,
  486. diff: defined(diffAbnormalSessions)
  487. ? displaySessionStatusPercent(diffAbnormalSessions)
  488. : null,
  489. diffDirection: diffAbnormalSessions
  490. ? diffAbnormalSessions > 0
  491. ? 'up'
  492. : 'down'
  493. : null,
  494. diffColor: diffAbnormalSessions
  495. ? diffAbnormalSessions > 0
  496. ? 'red300'
  497. : 'green300'
  498. : null,
  499. },
  500. {
  501. type: ReleaseComparisonChartType.ERRORED_SESSIONS,
  502. role: 'children',
  503. drilldown: defined(issuesTotals?.handled) ? (
  504. <Tooltip title={t('Open in Issues')}>
  505. <GlobalSelectionLink
  506. to={getReleaseHandledIssuesUrl(
  507. organization.slug,
  508. project.id,
  509. release.version,
  510. {start, end, period: period ?? undefined}
  511. )}
  512. >
  513. {tct('([count] handled [issues])', {
  514. count: issuesTotals?.handled
  515. ? issuesTotals.handled >= 100
  516. ? '99+'
  517. : issuesTotals.handled
  518. : 0,
  519. issues: tn('issue', 'issues', issuesTotals?.handled),
  520. })}
  521. </GlobalSelectionLink>
  522. </Tooltip>
  523. ) : null,
  524. thisRelease: defined(releaseErroredSessions)
  525. ? displaySessionStatusPercent(releaseErroredSessions)
  526. : null,
  527. allReleases: defined(allErroredSessions)
  528. ? displaySessionStatusPercent(allErroredSessions)
  529. : null,
  530. diff: defined(diffErroredSessions)
  531. ? displaySessionStatusPercent(diffErroredSessions)
  532. : null,
  533. diffDirection: diffErroredSessions
  534. ? diffErroredSessions > 0
  535. ? 'up'
  536. : 'down'
  537. : null,
  538. diffColor: diffErroredSessions
  539. ? diffErroredSessions > 0
  540. ? 'red300'
  541. : 'green300'
  542. : null,
  543. },
  544. {
  545. type: ReleaseComparisonChartType.CRASHED_SESSIONS,
  546. role: 'default',
  547. drilldown: defined(issuesTotals?.unhandled) ? (
  548. <Tooltip title={t('Open in Issues')}>
  549. <GlobalSelectionLink
  550. to={getReleaseUnhandledIssuesUrl(
  551. organization.slug,
  552. project.id,
  553. release.version,
  554. {start, end, period: period ?? undefined}
  555. )}
  556. >
  557. {tct('([count] unhandled [issues])', {
  558. count: issuesTotals?.unhandled
  559. ? issuesTotals.unhandled >= 100
  560. ? '99+'
  561. : issuesTotals.unhandled
  562. : 0,
  563. issues: tn('issue', 'issues', issuesTotals?.unhandled),
  564. })}
  565. </GlobalSelectionLink>
  566. </Tooltip>
  567. ) : null,
  568. thisRelease: defined(releaseCrashedSessions)
  569. ? displaySessionStatusPercent(releaseCrashedSessions)
  570. : null,
  571. allReleases: defined(allCrashedSessions)
  572. ? displaySessionStatusPercent(allCrashedSessions)
  573. : null,
  574. diff: defined(diffCrashedSessions)
  575. ? displaySessionStatusPercent(diffCrashedSessions)
  576. : null,
  577. diffDirection: diffCrashedSessions
  578. ? diffCrashedSessions > 0
  579. ? 'up'
  580. : 'down'
  581. : null,
  582. diffColor: diffCrashedSessions
  583. ? diffCrashedSessions > 0
  584. ? 'red300'
  585. : 'green300'
  586. : null,
  587. }
  588. );
  589. }
  590. }
  591. const hasUsers = !!getCount(releaseSessions?.groups, SessionFieldWithOperation.USERS);
  592. if (hasHealthData && (hasUsers || loading)) {
  593. charts.push({
  594. type: ReleaseComparisonChartType.CRASH_FREE_USERS,
  595. role: 'parent',
  596. drilldown: null,
  597. thisRelease: defined(releaseCrashFreeUsers)
  598. ? displaySessionStatusPercent(releaseCrashFreeUsers)
  599. : null,
  600. allReleases: defined(allCrashFreeUsers)
  601. ? displaySessionStatusPercent(allCrashFreeUsers)
  602. : null,
  603. diff: defined(diffCrashFreeUsers)
  604. ? displaySessionStatusPercent(diffCrashFreeUsers)
  605. : null,
  606. diffDirection: diffCrashFreeUsers ? (diffCrashFreeUsers > 0 ? 'up' : 'down') : null,
  607. diffColor: diffCrashFreeUsers
  608. ? diffCrashFreeUsers > 0
  609. ? 'green300'
  610. : 'red300'
  611. : null,
  612. });
  613. if (expanded.has(ReleaseComparisonChartType.CRASH_FREE_USERS)) {
  614. charts.push(
  615. {
  616. type: ReleaseComparisonChartType.HEALTHY_USERS,
  617. role: 'children',
  618. drilldown: null,
  619. thisRelease: defined(releaseHealthyUsers)
  620. ? displaySessionStatusPercent(releaseHealthyUsers)
  621. : null,
  622. allReleases: defined(allHealthyUsers)
  623. ? displaySessionStatusPercent(allHealthyUsers)
  624. : null,
  625. diff: defined(diffHealthyUsers)
  626. ? displaySessionStatusPercent(diffHealthyUsers)
  627. : null,
  628. diffDirection: diffHealthyUsers ? (diffHealthyUsers > 0 ? 'up' : 'down') : null,
  629. diffColor: diffHealthyUsers
  630. ? diffHealthyUsers > 0
  631. ? 'green300'
  632. : 'red300'
  633. : null,
  634. },
  635. {
  636. type: ReleaseComparisonChartType.ABNORMAL_USERS,
  637. role: 'children',
  638. drilldown: null,
  639. thisRelease: defined(releaseAbnormalUsers)
  640. ? displaySessionStatusPercent(releaseAbnormalUsers)
  641. : null,
  642. allReleases: defined(allAbnormalUsers)
  643. ? displaySessionStatusPercent(allAbnormalUsers)
  644. : null,
  645. diff: defined(diffAbnormalUsers)
  646. ? displaySessionStatusPercent(diffAbnormalUsers)
  647. : null,
  648. diffDirection: diffAbnormalUsers
  649. ? diffAbnormalUsers > 0
  650. ? 'up'
  651. : 'down'
  652. : null,
  653. diffColor: diffAbnormalUsers
  654. ? diffAbnormalUsers > 0
  655. ? 'red300'
  656. : 'green300'
  657. : null,
  658. },
  659. {
  660. type: ReleaseComparisonChartType.ERRORED_USERS,
  661. role: 'children',
  662. drilldown: null,
  663. thisRelease: defined(releaseErroredUsers)
  664. ? displaySessionStatusPercent(releaseErroredUsers)
  665. : null,
  666. allReleases: defined(allErroredUsers)
  667. ? displaySessionStatusPercent(allErroredUsers)
  668. : null,
  669. diff: defined(diffErroredUsers)
  670. ? displaySessionStatusPercent(diffErroredUsers)
  671. : null,
  672. diffDirection: diffErroredUsers ? (diffErroredUsers > 0 ? 'up' : 'down') : null,
  673. diffColor: diffErroredUsers
  674. ? diffErroredUsers > 0
  675. ? 'red300'
  676. : 'green300'
  677. : null,
  678. },
  679. {
  680. type: ReleaseComparisonChartType.CRASHED_USERS,
  681. role: 'default',
  682. drilldown: null,
  683. thisRelease: defined(releaseCrashedUsers)
  684. ? displaySessionStatusPercent(releaseCrashedUsers)
  685. : null,
  686. allReleases: defined(allCrashedUsers)
  687. ? displaySessionStatusPercent(allCrashedUsers)
  688. : null,
  689. diff: defined(diffCrashedUsers)
  690. ? displaySessionStatusPercent(diffCrashedUsers)
  691. : null,
  692. diffDirection: diffCrashedUsers ? (diffCrashedUsers > 0 ? 'up' : 'down') : null,
  693. diffColor: diffCrashedUsers
  694. ? diffCrashedUsers > 0
  695. ? 'red300'
  696. : 'green300'
  697. : null,
  698. }
  699. );
  700. }
  701. }
  702. if (hasPerformance) {
  703. charts.push({
  704. type: ReleaseComparisonChartType.FAILURE_RATE,
  705. role: 'default',
  706. drilldown: null,
  707. thisRelease: eventsTotals?.releaseFailureRate
  708. ? formatPercentage(eventsTotals?.releaseFailureRate)
  709. : null,
  710. allReleases: eventsTotals?.allFailureRate
  711. ? formatPercentage(eventsTotals?.allFailureRate)
  712. : null,
  713. diff: diffFailure ? formatPercentage(Math.abs(diffFailure)) : null,
  714. diffDirection: diffFailure ? (diffFailure > 0 ? 'up' : 'down') : null,
  715. diffColor: diffFailure ? (diffFailure > 0 ? 'red300' : 'green300') : null,
  716. });
  717. }
  718. if (hasHealthData) {
  719. additionalCharts.push({
  720. type: ReleaseComparisonChartType.SESSION_COUNT,
  721. role: 'default',
  722. drilldown: null,
  723. thisRelease: defined(releaseSessionsCount) ? (
  724. <Count value={releaseSessionsCount} />
  725. ) : null,
  726. allReleases: defined(allSessionsCount) ? <Count value={allSessionsCount} /> : null,
  727. diff: null,
  728. diffDirection: null,
  729. diffColor: null,
  730. });
  731. if (hasUsers || loading) {
  732. additionalCharts.push({
  733. type: ReleaseComparisonChartType.USER_COUNT,
  734. role: 'default',
  735. drilldown: null,
  736. thisRelease: defined(releaseUsersCount) ? (
  737. <Count value={releaseUsersCount} />
  738. ) : null,
  739. allReleases: defined(allUsersCount) ? <Count value={allUsersCount} /> : null,
  740. diff: null,
  741. diffDirection: null,
  742. diffColor: null,
  743. });
  744. }
  745. }
  746. if (hasDiscover) {
  747. additionalCharts.push({
  748. type: ReleaseComparisonChartType.ERROR_COUNT,
  749. role: 'default',
  750. drilldown: null,
  751. thisRelease: defined(eventsTotals?.releaseErrorCount) ? (
  752. <Count value={eventsTotals?.releaseErrorCount!} />
  753. ) : null,
  754. allReleases: defined(eventsTotals?.allErrorCount) ? (
  755. <Count value={eventsTotals?.allErrorCount!} />
  756. ) : null,
  757. diff: null,
  758. diffDirection: null,
  759. diffColor: null,
  760. });
  761. }
  762. if (hasPerformance) {
  763. additionalCharts.push({
  764. type: ReleaseComparisonChartType.TRANSACTION_COUNT,
  765. role: 'default',
  766. drilldown: null,
  767. thisRelease: defined(eventsTotals?.releaseTransactionCount) ? (
  768. <Count value={eventsTotals?.releaseTransactionCount!} />
  769. ) : null,
  770. allReleases: defined(eventsTotals?.allTransactionCount) ? (
  771. <Count value={eventsTotals?.allTransactionCount!} />
  772. ) : null,
  773. diff: null,
  774. diffDirection: null,
  775. diffColor: null,
  776. });
  777. }
  778. function handleChartChange(chartType: ReleaseComparisonChartType) {
  779. trackAnalytics('releases.change_chart_type', {
  780. organization,
  781. chartType,
  782. });
  783. browserHistory.push({
  784. ...location,
  785. query: {
  786. ...location.query,
  787. chart: chartType,
  788. },
  789. });
  790. }
  791. function handleExpanderToggle(chartType: ReleaseComparisonChartType) {
  792. if (expanded.has(chartType)) {
  793. expanded.delete(chartType);
  794. setExpanded(new Set(expanded));
  795. } else {
  796. setExpanded(new Set(expanded.add(chartType)));
  797. }
  798. }
  799. function getTableHeaders(withExpanders: boolean) {
  800. const headers = [
  801. <DescriptionCell key="description">{t('Description')}</DescriptionCell>,
  802. <Cell key="releases">{t('All Releases')}</Cell>,
  803. <Cell key="release">{t('This Release')}</Cell>,
  804. <Cell key="change">{t('Change')}</Cell>,
  805. ];
  806. if (withExpanders) {
  807. headers.push(<Cell key="expanders" />);
  808. }
  809. return headers;
  810. }
  811. function getChartDiff(
  812. diff: ReleaseComparisonRow['diff'],
  813. diffColor: ReleaseComparisonRow['diffColor'],
  814. diffDirection: ReleaseComparisonRow['diffDirection']
  815. ) {
  816. return diff ? (
  817. <Change color={defined(diffColor) ? diffColor : undefined}>
  818. {diff}{' '}
  819. {defined(diffDirection) ? (
  820. <IconArrow direction={diffDirection} size="xs" />
  821. ) : diff === '0%' ? null : (
  822. <StyledNotAvailable />
  823. )}
  824. </Change>
  825. ) : null;
  826. }
  827. // if there are no sessions, we do not need to do row toggling because there won't be as many rows
  828. if (!hasHealthData) {
  829. charts.push(...additionalCharts);
  830. additionalCharts.splice(0, additionalCharts.length);
  831. }
  832. let activeChart = decodeScalar(
  833. location.query.chart,
  834. hasHealthData
  835. ? ReleaseComparisonChartType.CRASH_FREE_SESSIONS
  836. : hasPerformance
  837. ? ReleaseComparisonChartType.FAILURE_RATE
  838. : ReleaseComparisonChartType.ERROR_COUNT
  839. ) as ReleaseComparisonChartType;
  840. let chart = [...charts, ...additionalCharts].find(ch => ch.type === activeChart);
  841. if (!chart) {
  842. chart = charts[0];
  843. activeChart = charts[0].type;
  844. }
  845. const showPlaceholders = loading || eventsLoading;
  846. const withExpanders = hasHealthData || additionalCharts.length > 0;
  847. if (errored || !chart) {
  848. return (
  849. <Panel>
  850. <ErrorPanel>
  851. <IconWarning color="gray300" size="lg" />
  852. </ErrorPanel>
  853. </Panel>
  854. );
  855. }
  856. const titleChartDiff =
  857. chart.diff !== '0%' && chart.thisRelease !== '0%'
  858. ? getChartDiff(chart.diff, chart.diffColor, chart.diffDirection)
  859. : null;
  860. function renderChartRow({
  861. diff,
  862. diffColor,
  863. diffDirection,
  864. ...rest
  865. }: ReleaseComparisonRow) {
  866. return (
  867. <ReleaseComparisonChartRow
  868. {...rest}
  869. key={rest.type}
  870. diff={diff}
  871. showPlaceholders={showPlaceholders}
  872. activeChart={activeChart}
  873. onChartChange={handleChartChange}
  874. chartDiff={getChartDiff(diff, diffColor, diffDirection)}
  875. onExpanderToggle={handleExpanderToggle}
  876. expanded={expanded.has(rest.type)}
  877. withExpanders={withExpanders}
  878. />
  879. );
  880. }
  881. return (
  882. <Fragment>
  883. <ChartPanel>
  884. <ErrorBoundary mini>
  885. <ChartContainer>
  886. {[
  887. ReleaseComparisonChartType.ERROR_COUNT,
  888. ReleaseComparisonChartType.TRANSACTION_COUNT,
  889. ReleaseComparisonChartType.FAILURE_RATE,
  890. ].includes(activeChart)
  891. ? getDynamicText({
  892. value: (
  893. <ReleaseEventsChart
  894. release={release}
  895. project={project}
  896. chartType={activeChart}
  897. period={period ?? undefined}
  898. start={start}
  899. end={end}
  900. utc={utc === 'true'}
  901. value={chart.thisRelease}
  902. diff={titleChartDiff}
  903. />
  904. ),
  905. fixed: 'Events Chart',
  906. })
  907. : getDynamicText({
  908. value: (
  909. <ReleaseSessionsChart
  910. releaseSessions={releaseSessions}
  911. allSessions={allSessions}
  912. release={release}
  913. project={project}
  914. chartType={activeChart}
  915. platform={platform}
  916. period={period ?? undefined}
  917. start={start}
  918. end={end}
  919. utc={utc === 'true'}
  920. value={chart.thisRelease}
  921. diff={titleChartDiff}
  922. loading={loading}
  923. reloading={reloading}
  924. />
  925. ),
  926. fixed: 'Sessions Chart',
  927. })}
  928. </ChartContainer>
  929. </ErrorBoundary>
  930. </ChartPanel>
  931. <ChartTable
  932. headers={getTableHeaders(withExpanders)}
  933. data-test-id="release-comparison-table"
  934. withExpanders={withExpanders}
  935. >
  936. {charts.map(chartRow => renderChartRow(chartRow))}
  937. {isOtherExpanded && additionalCharts.map(chartRow => renderChartRow(chartRow))}
  938. {additionalCharts.length > 0 && (
  939. <ShowMoreWrapper onClick={() => setIsOtherExpanded(!isOtherExpanded)}>
  940. <ShowMoreTitle>
  941. <IconList size="xs" />
  942. {isOtherExpanded
  943. ? tn('Hide %s Other', 'Hide %s Others', additionalCharts.length)
  944. : tn('Show %s Other', 'Show %s Others', additionalCharts.length)}
  945. </ShowMoreTitle>
  946. <ShowMoreButton>
  947. <Button
  948. borderless
  949. size="zero"
  950. icon={<IconChevron direction={isOtherExpanded ? 'up' : 'down'} />}
  951. aria-label={t('Toggle additional charts')}
  952. />
  953. </ShowMoreButton>
  954. </ShowMoreWrapper>
  955. )}
  956. </ChartTable>
  957. </Fragment>
  958. );
  959. }
  960. const ChartPanel = styled(Panel)`
  961. margin-bottom: 0;
  962. border-bottom-left-radius: 0;
  963. border-bottom: none;
  964. border-bottom-right-radius: 0;
  965. `;
  966. const Cell = styled('div')`
  967. text-align: right;
  968. ${p => p.theme.overflowEllipsis}
  969. `;
  970. const DescriptionCell = styled(Cell)`
  971. text-align: left;
  972. overflow: visible;
  973. `;
  974. const Change = styled('div')<{color?: Color}>`
  975. font-size: ${p => p.theme.fontSizeMedium};
  976. ${p => p.color && `color: ${p.theme[p.color]}`}
  977. `;
  978. const ChartTable = styled(PanelTable)<{withExpanders: boolean}>`
  979. border-top-left-radius: 0;
  980. border-top-right-radius: 0;
  981. grid-template-columns: minmax(400px, auto) repeat(3, minmax(min-content, 1fr)) ${p =>
  982. p.withExpanders ? '75px' : ''};
  983. > * {
  984. border-bottom: 1px solid ${p => p.theme.border};
  985. }
  986. @media (max-width: ${p => p.theme.breakpoints.large}) {
  987. grid-template-columns: repeat(4, minmax(min-content, 1fr)) ${p =>
  988. p.withExpanders ? '75px' : ''};
  989. }
  990. `;
  991. const StyledNotAvailable = styled(NotAvailable)`
  992. display: inline-block;
  993. `;
  994. const ShowMoreWrapper = styled('div')`
  995. display: contents;
  996. &:hover {
  997. cursor: pointer;
  998. }
  999. > * {
  1000. padding: ${space(1)} ${space(2)};
  1001. }
  1002. `;
  1003. const ShowMoreTitle = styled('div')`
  1004. color: ${p => p.theme.gray300};
  1005. font-size: ${p => p.theme.fontSizeMedium};
  1006. display: inline-grid;
  1007. grid-template-columns: auto auto;
  1008. gap: 10px;
  1009. align-items: center;
  1010. justify-content: flex-start;
  1011. svg {
  1012. margin-left: ${space(0.25)};
  1013. }
  1014. `;
  1015. const ShowMoreButton = styled('div')`
  1016. grid-column: 2 / -1;
  1017. display: flex;
  1018. align-items: center;
  1019. justify-content: flex-end;
  1020. `;
  1021. export default ReleaseComparisonChart;