index.tsx 33 KB

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