index.tsx 34 KB

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