index.tsx 34 KB

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