index.tsx 33 KB

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