index.tsx 33 KB

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