1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084 |
- import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
- import {browserHistory} from 'react-router';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import {Location} from 'history';
- import {Client} from 'sentry/api';
- import {Button} from 'sentry/components/button';
- import ErrorPanel from 'sentry/components/charts/errorPanel';
- import {ChartContainer} from 'sentry/components/charts/styles';
- import Count from 'sentry/components/count';
- import ErrorBoundary from 'sentry/components/errorBoundary';
- import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
- import NotAvailable from 'sentry/components/notAvailable';
- import Panel from 'sentry/components/panels/panel';
- import PanelTable from 'sentry/components/panels/panelTable';
- import {Tooltip} from 'sentry/components/tooltip';
- import {PlatformKey} from 'sentry/data/platformCategories';
- import {IconArrow, IconChevron, IconList, IconWarning} from 'sentry/icons';
- import {t, tct, tn} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {
- Organization,
- ReleaseComparisonChartType,
- ReleaseProject,
- ReleaseWithHealth,
- SessionApiResponse,
- SessionFieldWithOperation,
- SessionStatus,
- } from 'sentry/types';
- import {defined} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {formatPercentage} from 'sentry/utils/formatters';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import {decodeList, decodeScalar} from 'sentry/utils/queryString';
- import {getCount, getCrashFreeRate, getSessionStatusRate} from 'sentry/utils/sessions';
- import {Color} from 'sentry/utils/theme';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import {
- displaySessionStatusPercent,
- getReleaseBounds,
- getReleaseHandledIssuesUrl,
- getReleaseParams,
- getReleaseUnhandledIssuesUrl,
- } from 'sentry/views/releases/utils';
- import ReleaseComparisonChartRow from './releaseComparisonChartRow';
- import ReleaseEventsChart from './releaseEventsChart';
- import ReleaseSessionsChart from './releaseSessionsChart';
- export type ReleaseComparisonRow = {
- allReleases: React.ReactNode;
- diff: React.ReactNode;
- diffColor: Color | null;
- diffDirection: 'up' | 'down' | null;
- drilldown: React.ReactNode;
- role: 'parent' | 'children' | 'default';
- thisRelease: React.ReactNode;
- type: ReleaseComparisonChartType;
- };
- type Props = {
- allSessions: SessionApiResponse | null;
- api: Client;
- errored: boolean;
- hasHealthData: boolean;
- loading: boolean;
- location: Location;
- organization: Organization;
- platform: PlatformKey;
- project: ReleaseProject;
- release: ReleaseWithHealth;
- releaseSessions: SessionApiResponse | null;
- reloading: boolean;
- };
- type EventsTotals = {
- allErrorCount: number;
- allFailureRate: number;
- allTransactionCount: number;
- releaseErrorCount: number;
- releaseFailureRate: number;
- releaseTransactionCount: number;
- } | null;
- type IssuesTotals = {
- handled: number;
- unhandled: number;
- } | null;
- function ReleaseComparisonChart({
- release,
- project,
- releaseSessions,
- allSessions,
- platform,
- location,
- loading,
- reloading,
- errored,
- api,
- organization,
- hasHealthData,
- }: Props) {
- const [issuesTotals, setIssuesTotals] = useState<IssuesTotals>(null);
- const [eventsTotals, setEventsTotals] = useState<EventsTotals>(null);
- const [eventsLoading, setEventsLoading] = useState(false);
- const [expanded, setExpanded] = useState(new Set());
- const [isOtherExpanded, setIsOtherExpanded] = useState(false);
- const charts: ReleaseComparisonRow[] = [];
- const additionalCharts: ReleaseComparisonRow[] = [];
- const hasDiscover =
- organization.features.includes('discover-basic') ||
- organization.features.includes('performance-view');
- const hasPerformance = organization.features.includes('performance-view');
- const {
- statsPeriod: period,
- start,
- end,
- utc,
- } = useMemo(
- () =>
- // Memoizing this so that it does not calculate different `end` for releases without events+sessions each rerender
- getReleaseParams({
- location,
- releaseBounds: getReleaseBounds(release),
- }),
- [release, location]
- );
- useEffect(() => {
- const chartInUrl = decodeScalar(location.query.chart) as ReleaseComparisonChartType;
- if (
- [
- ReleaseComparisonChartType.HEALTHY_SESSIONS,
- ReleaseComparisonChartType.ABNORMAL_SESSIONS,
- ReleaseComparisonChartType.ERRORED_SESSIONS,
- ReleaseComparisonChartType.CRASHED_SESSIONS,
- ].includes(chartInUrl)
- ) {
- setExpanded(e => new Set(e.add(ReleaseComparisonChartType.CRASH_FREE_SESSIONS)));
- }
- if (
- [
- ReleaseComparisonChartType.HEALTHY_USERS,
- ReleaseComparisonChartType.ABNORMAL_USERS,
- ReleaseComparisonChartType.ERRORED_USERS,
- ReleaseComparisonChartType.CRASHED_USERS,
- ].includes(chartInUrl)
- ) {
- setExpanded(e => new Set(e.add(ReleaseComparisonChartType.CRASH_FREE_USERS)));
- }
- if (
- [
- ReleaseComparisonChartType.SESSION_COUNT,
- ReleaseComparisonChartType.USER_COUNT,
- ReleaseComparisonChartType.ERROR_COUNT,
- ReleaseComparisonChartType.TRANSACTION_COUNT,
- ].includes(chartInUrl)
- ) {
- setIsOtherExpanded(true);
- }
- }, [location.query.chart]);
- const fetchEventsTotals = useCallback(async () => {
- const url = `/organizations/${organization.slug}/events/`;
- const commonQuery = {
- environment: decodeList(location.query.environment),
- project: decodeList(location.query.project),
- start,
- end,
- ...(period ? {statsPeriod: period} : {}),
- };
- setEventsLoading(true);
- try {
- const [
- releaseTransactionTotals,
- allTransactionTotals,
- releaseErrorTotals,
- allErrorTotals,
- ] = await Promise.all([
- api.requestPromise(url, {
- query: {
- field: ['failure_rate()', 'count()'],
- query: new MutableSearch([
- 'event.type:transaction',
- `release:${release.version}`,
- ]).formatString(),
- ...commonQuery,
- },
- }),
- api.requestPromise(url, {
- query: {
- field: ['failure_rate()', 'count()'],
- query: new MutableSearch(['event.type:transaction']).formatString(),
- ...commonQuery,
- },
- }),
- api.requestPromise(url, {
- query: {
- field: ['count()'],
- query: new MutableSearch([
- 'event.type:error',
- `release:${release.version}`,
- ]).formatString(),
- ...commonQuery,
- },
- }),
- api.requestPromise(url, {
- query: {
- field: ['count()'],
- query: new MutableSearch(['event.type:error']).formatString(),
- ...commonQuery,
- },
- }),
- ]);
- setEventsTotals({
- allErrorCount: allErrorTotals.data[0]['count()'],
- releaseErrorCount: releaseErrorTotals.data[0]['count()'],
- allTransactionCount: allTransactionTotals.data[0]['count()'],
- releaseTransactionCount: releaseTransactionTotals.data[0]['count()'],
- releaseFailureRate: releaseTransactionTotals.data[0]['failure_rate()'],
- allFailureRate: allTransactionTotals.data[0]['failure_rate()'],
- });
- setEventsLoading(false);
- } catch (err) {
- setEventsTotals(null);
- setEventsLoading(false);
- Sentry.captureException(err);
- }
- }, [
- api,
- end,
- location.query.environment,
- location.query.project,
- organization.slug,
- period,
- release.version,
- start,
- ]);
- const fetchIssuesTotals = useCallback(async () => {
- const UNHANDLED_QUERY = `release:"${release.version}" error.handled:0`;
- const HANDLED_QUERY = `release:"${release.version}" error.handled:1`;
- try {
- const response = await api.requestPromise(
- `/organizations/${organization.slug}/issues-count/`,
- {
- query: {
- project: project.id,
- environment: decodeList(location.query.environment),
- start,
- end,
- ...(period ? {statsPeriod: period} : {}),
- query: [UNHANDLED_QUERY, HANDLED_QUERY],
- },
- }
- );
- setIssuesTotals({
- handled: response[HANDLED_QUERY] ?? 0,
- unhandled: response[UNHANDLED_QUERY] ?? 0,
- });
- } catch (err) {
- setIssuesTotals(null);
- Sentry.captureException(err);
- }
- }, [
- api,
- end,
- location.query.environment,
- organization.slug,
- period,
- project.id,
- release.version,
- start,
- ]);
- useEffect(() => {
- if (hasDiscover || hasPerformance) {
- fetchEventsTotals();
- fetchIssuesTotals();
- }
- }, [fetchEventsTotals, fetchIssuesTotals, hasDiscover, hasPerformance]);
- const releaseCrashFreeSessions = getCrashFreeRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.SESSIONS
- );
- const allCrashFreeSessions = getCrashFreeRate(
- allSessions?.groups,
- SessionFieldWithOperation.SESSIONS
- );
- const diffCrashFreeSessions =
- defined(releaseCrashFreeSessions) && defined(allCrashFreeSessions)
- ? releaseCrashFreeSessions - allCrashFreeSessions
- : null;
- const releaseHealthySessions = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.HEALTHY
- );
- const allHealthySessions = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.HEALTHY
- );
- const diffHealthySessions =
- defined(releaseHealthySessions) && defined(allHealthySessions)
- ? releaseHealthySessions - allHealthySessions
- : null;
- const releaseAbnormalSessions = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.ABNORMAL
- );
- const allAbnormalSessions = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.ABNORMAL
- );
- const diffAbnormalSessions =
- defined(releaseAbnormalSessions) && defined(allAbnormalSessions)
- ? releaseAbnormalSessions - allAbnormalSessions
- : null;
- const releaseErroredSessions = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.ERRORED
- );
- const allErroredSessions = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.ERRORED
- );
- const diffErroredSessions =
- defined(releaseErroredSessions) && defined(allErroredSessions)
- ? releaseErroredSessions - allErroredSessions
- : null;
- const releaseCrashedSessions = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.CRASHED
- );
- const allCrashedSessions = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.SESSIONS,
- SessionStatus.CRASHED
- );
- const diffCrashedSessions =
- defined(releaseCrashedSessions) && defined(allCrashedSessions)
- ? releaseCrashedSessions - allCrashedSessions
- : null;
- const releaseCrashFreeUsers = getCrashFreeRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.USERS
- );
- const allCrashFreeUsers = getCrashFreeRate(
- allSessions?.groups,
- SessionFieldWithOperation.USERS
- );
- const diffCrashFreeUsers =
- defined(releaseCrashFreeUsers) && defined(allCrashFreeUsers)
- ? releaseCrashFreeUsers - allCrashFreeUsers
- : null;
- const releaseHealthyUsers = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.HEALTHY
- );
- const allHealthyUsers = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.HEALTHY
- );
- const diffHealthyUsers =
- defined(releaseHealthyUsers) && defined(allHealthyUsers)
- ? releaseHealthyUsers - allHealthyUsers
- : null;
- const releaseAbnormalUsers = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.ABNORMAL
- );
- const allAbnormalUsers = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.ABNORMAL
- );
- const diffAbnormalUsers =
- defined(releaseAbnormalUsers) && defined(allAbnormalUsers)
- ? releaseAbnormalUsers - allAbnormalUsers
- : null;
- const releaseErroredUsers = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.ERRORED
- );
- const allErroredUsers = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.ERRORED
- );
- const diffErroredUsers =
- defined(releaseErroredUsers) && defined(allErroredUsers)
- ? releaseErroredUsers - allErroredUsers
- : null;
- const releaseCrashedUsers = getSessionStatusRate(
- releaseSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.CRASHED
- );
- const allCrashedUsers = getSessionStatusRate(
- allSessions?.groups,
- SessionFieldWithOperation.USERS,
- SessionStatus.CRASHED
- );
- const diffCrashedUsers =
- defined(releaseCrashedUsers) && defined(allCrashedUsers)
- ? releaseCrashedUsers - allCrashedUsers
- : null;
- const releaseSessionsCount = getCount(
- releaseSessions?.groups,
- SessionFieldWithOperation.SESSIONS
- );
- const allSessionsCount = getCount(
- allSessions?.groups,
- SessionFieldWithOperation.SESSIONS
- );
- const releaseUsersCount = getCount(
- releaseSessions?.groups,
- SessionFieldWithOperation.USERS
- );
- const allUsersCount = getCount(allSessions?.groups, SessionFieldWithOperation.USERS);
- const diffFailure =
- eventsTotals?.releaseFailureRate && eventsTotals?.allFailureRate
- ? eventsTotals.releaseFailureRate - eventsTotals.allFailureRate
- : null;
- if (hasHealthData) {
- charts.push({
- type: ReleaseComparisonChartType.CRASH_FREE_SESSIONS,
- role: 'parent',
- drilldown: null,
- thisRelease: defined(releaseCrashFreeSessions)
- ? displaySessionStatusPercent(releaseCrashFreeSessions)
- : null,
- allReleases: defined(allCrashFreeSessions)
- ? displaySessionStatusPercent(allCrashFreeSessions)
- : null,
- diff: defined(diffCrashFreeSessions)
- ? displaySessionStatusPercent(diffCrashFreeSessions)
- : null,
- diffDirection: diffCrashFreeSessions
- ? diffCrashFreeSessions > 0
- ? 'up'
- : 'down'
- : null,
- diffColor: diffCrashFreeSessions
- ? diffCrashFreeSessions > 0
- ? 'green300'
- : 'red300'
- : null,
- });
- if (expanded.has(ReleaseComparisonChartType.CRASH_FREE_SESSIONS)) {
- charts.push(
- {
- type: ReleaseComparisonChartType.HEALTHY_SESSIONS,
- role: 'children',
- drilldown: null,
- thisRelease: defined(releaseHealthySessions)
- ? displaySessionStatusPercent(releaseHealthySessions)
- : null,
- allReleases: defined(allHealthySessions)
- ? displaySessionStatusPercent(allHealthySessions)
- : null,
- diff: defined(diffHealthySessions)
- ? displaySessionStatusPercent(diffHealthySessions)
- : null,
- diffDirection: diffHealthySessions
- ? diffHealthySessions > 0
- ? 'up'
- : 'down'
- : null,
- diffColor: diffHealthySessions
- ? diffHealthySessions > 0
- ? 'green300'
- : 'red300'
- : null,
- },
- {
- type: ReleaseComparisonChartType.ABNORMAL_SESSIONS,
- role: 'children',
- drilldown: null,
- thisRelease: defined(releaseAbnormalSessions)
- ? displaySessionStatusPercent(releaseAbnormalSessions)
- : null,
- allReleases: defined(allAbnormalSessions)
- ? displaySessionStatusPercent(allAbnormalSessions)
- : null,
- diff: defined(diffAbnormalSessions)
- ? displaySessionStatusPercent(diffAbnormalSessions)
- : null,
- diffDirection: diffAbnormalSessions
- ? diffAbnormalSessions > 0
- ? 'up'
- : 'down'
- : null,
- diffColor: diffAbnormalSessions
- ? diffAbnormalSessions > 0
- ? 'red300'
- : 'green300'
- : null,
- },
- {
- type: ReleaseComparisonChartType.ERRORED_SESSIONS,
- role: 'children',
- drilldown: defined(issuesTotals?.handled) ? (
- <Tooltip title={t('Open in Issues')}>
- <GlobalSelectionLink
- to={getReleaseHandledIssuesUrl(
- organization.slug,
- project.id,
- release.version,
- {start, end, period: period ?? undefined}
- )}
- >
- {tct('([count] handled [issues])', {
- count: issuesTotals?.handled
- ? issuesTotals.handled >= 100
- ? '99+'
- : issuesTotals.handled
- : 0,
- issues: tn('issue', 'issues', issuesTotals?.handled),
- })}
- </GlobalSelectionLink>
- </Tooltip>
- ) : null,
- thisRelease: defined(releaseErroredSessions)
- ? displaySessionStatusPercent(releaseErroredSessions)
- : null,
- allReleases: defined(allErroredSessions)
- ? displaySessionStatusPercent(allErroredSessions)
- : null,
- diff: defined(diffErroredSessions)
- ? displaySessionStatusPercent(diffErroredSessions)
- : null,
- diffDirection: diffErroredSessions
- ? diffErroredSessions > 0
- ? 'up'
- : 'down'
- : null,
- diffColor: diffErroredSessions
- ? diffErroredSessions > 0
- ? 'red300'
- : 'green300'
- : null,
- },
- {
- type: ReleaseComparisonChartType.CRASHED_SESSIONS,
- role: 'default',
- drilldown: defined(issuesTotals?.unhandled) ? (
- <Tooltip title={t('Open in Issues')}>
- <GlobalSelectionLink
- to={getReleaseUnhandledIssuesUrl(
- organization.slug,
- project.id,
- release.version,
- {start, end, period: period ?? undefined}
- )}
- >
- {tct('([count] unhandled [issues])', {
- count: issuesTotals?.unhandled
- ? issuesTotals.unhandled >= 100
- ? '99+'
- : issuesTotals.unhandled
- : 0,
- issues: tn('issue', 'issues', issuesTotals?.unhandled),
- })}
- </GlobalSelectionLink>
- </Tooltip>
- ) : null,
- thisRelease: defined(releaseCrashedSessions)
- ? displaySessionStatusPercent(releaseCrashedSessions)
- : null,
- allReleases: defined(allCrashedSessions)
- ? displaySessionStatusPercent(allCrashedSessions)
- : null,
- diff: defined(diffCrashedSessions)
- ? displaySessionStatusPercent(diffCrashedSessions)
- : null,
- diffDirection: diffCrashedSessions
- ? diffCrashedSessions > 0
- ? 'up'
- : 'down'
- : null,
- diffColor: diffCrashedSessions
- ? diffCrashedSessions > 0
- ? 'red300'
- : 'green300'
- : null,
- }
- );
- }
- }
- const hasUsers = !!getCount(releaseSessions?.groups, SessionFieldWithOperation.USERS);
- if (hasHealthData && (hasUsers || loading)) {
- charts.push({
- type: ReleaseComparisonChartType.CRASH_FREE_USERS,
- role: 'parent',
- drilldown: null,
- thisRelease: defined(releaseCrashFreeUsers)
- ? displaySessionStatusPercent(releaseCrashFreeUsers)
- : null,
- allReleases: defined(allCrashFreeUsers)
- ? displaySessionStatusPercent(allCrashFreeUsers)
- : null,
- diff: defined(diffCrashFreeUsers)
- ? displaySessionStatusPercent(diffCrashFreeUsers)
- : null,
- diffDirection: diffCrashFreeUsers ? (diffCrashFreeUsers > 0 ? 'up' : 'down') : null,
- diffColor: diffCrashFreeUsers
- ? diffCrashFreeUsers > 0
- ? 'green300'
- : 'red300'
- : null,
- });
- if (expanded.has(ReleaseComparisonChartType.CRASH_FREE_USERS)) {
- charts.push(
- {
- type: ReleaseComparisonChartType.HEALTHY_USERS,
- role: 'children',
- drilldown: null,
- thisRelease: defined(releaseHealthyUsers)
- ? displaySessionStatusPercent(releaseHealthyUsers)
- : null,
- allReleases: defined(allHealthyUsers)
- ? displaySessionStatusPercent(allHealthyUsers)
- : null,
- diff: defined(diffHealthyUsers)
- ? displaySessionStatusPercent(diffHealthyUsers)
- : null,
- diffDirection: diffHealthyUsers ? (diffHealthyUsers > 0 ? 'up' : 'down') : null,
- diffColor: diffHealthyUsers
- ? diffHealthyUsers > 0
- ? 'green300'
- : 'red300'
- : null,
- },
- {
- type: ReleaseComparisonChartType.ABNORMAL_USERS,
- role: 'children',
- drilldown: null,
- thisRelease: defined(releaseAbnormalUsers)
- ? displaySessionStatusPercent(releaseAbnormalUsers)
- : null,
- allReleases: defined(allAbnormalUsers)
- ? displaySessionStatusPercent(allAbnormalUsers)
- : null,
- diff: defined(diffAbnormalUsers)
- ? displaySessionStatusPercent(diffAbnormalUsers)
- : null,
- diffDirection: diffAbnormalUsers
- ? diffAbnormalUsers > 0
- ? 'up'
- : 'down'
- : null,
- diffColor: diffAbnormalUsers
- ? diffAbnormalUsers > 0
- ? 'red300'
- : 'green300'
- : null,
- },
- {
- type: ReleaseComparisonChartType.ERRORED_USERS,
- role: 'children',
- drilldown: null,
- thisRelease: defined(releaseErroredUsers)
- ? displaySessionStatusPercent(releaseErroredUsers)
- : null,
- allReleases: defined(allErroredUsers)
- ? displaySessionStatusPercent(allErroredUsers)
- : null,
- diff: defined(diffErroredUsers)
- ? displaySessionStatusPercent(diffErroredUsers)
- : null,
- diffDirection: diffErroredUsers ? (diffErroredUsers > 0 ? 'up' : 'down') : null,
- diffColor: diffErroredUsers
- ? diffErroredUsers > 0
- ? 'red300'
- : 'green300'
- : null,
- },
- {
- type: ReleaseComparisonChartType.CRASHED_USERS,
- role: 'default',
- drilldown: null,
- thisRelease: defined(releaseCrashedUsers)
- ? displaySessionStatusPercent(releaseCrashedUsers)
- : null,
- allReleases: defined(allCrashedUsers)
- ? displaySessionStatusPercent(allCrashedUsers)
- : null,
- diff: defined(diffCrashedUsers)
- ? displaySessionStatusPercent(diffCrashedUsers)
- : null,
- diffDirection: diffCrashedUsers ? (diffCrashedUsers > 0 ? 'up' : 'down') : null,
- diffColor: diffCrashedUsers
- ? diffCrashedUsers > 0
- ? 'red300'
- : 'green300'
- : null,
- }
- );
- }
- }
- if (hasPerformance) {
- charts.push({
- type: ReleaseComparisonChartType.FAILURE_RATE,
- role: 'default',
- drilldown: null,
- thisRelease: eventsTotals?.releaseFailureRate
- ? formatPercentage(eventsTotals?.releaseFailureRate)
- : null,
- allReleases: eventsTotals?.allFailureRate
- ? formatPercentage(eventsTotals?.allFailureRate)
- : null,
- diff: diffFailure ? formatPercentage(Math.abs(diffFailure)) : null,
- diffDirection: diffFailure ? (diffFailure > 0 ? 'up' : 'down') : null,
- diffColor: diffFailure ? (diffFailure > 0 ? 'red300' : 'green300') : null,
- });
- }
- if (hasHealthData) {
- additionalCharts.push({
- type: ReleaseComparisonChartType.SESSION_COUNT,
- role: 'default',
- drilldown: null,
- thisRelease: defined(releaseSessionsCount) ? (
- <Count value={releaseSessionsCount} />
- ) : null,
- allReleases: defined(allSessionsCount) ? <Count value={allSessionsCount} /> : null,
- diff: null,
- diffDirection: null,
- diffColor: null,
- });
- if (hasUsers || loading) {
- additionalCharts.push({
- type: ReleaseComparisonChartType.USER_COUNT,
- role: 'default',
- drilldown: null,
- thisRelease: defined(releaseUsersCount) ? (
- <Count value={releaseUsersCount} />
- ) : null,
- allReleases: defined(allUsersCount) ? <Count value={allUsersCount} /> : null,
- diff: null,
- diffDirection: null,
- diffColor: null,
- });
- }
- }
- if (hasDiscover) {
- additionalCharts.push({
- type: ReleaseComparisonChartType.ERROR_COUNT,
- role: 'default',
- drilldown: null,
- thisRelease: defined(eventsTotals?.releaseErrorCount) ? (
- <Count value={eventsTotals?.releaseErrorCount!} />
- ) : null,
- allReleases: defined(eventsTotals?.allErrorCount) ? (
- <Count value={eventsTotals?.allErrorCount!} />
- ) : null,
- diff: null,
- diffDirection: null,
- diffColor: null,
- });
- }
- if (hasPerformance) {
- additionalCharts.push({
- type: ReleaseComparisonChartType.TRANSACTION_COUNT,
- role: 'default',
- drilldown: null,
- thisRelease: defined(eventsTotals?.releaseTransactionCount) ? (
- <Count value={eventsTotals?.releaseTransactionCount!} />
- ) : null,
- allReleases: defined(eventsTotals?.allTransactionCount) ? (
- <Count value={eventsTotals?.allTransactionCount!} />
- ) : null,
- diff: null,
- diffDirection: null,
- diffColor: null,
- });
- }
- function handleChartChange(chartType: ReleaseComparisonChartType) {
- trackAnalytics('releases.change_chart_type', {
- organization,
- chartType,
- });
- browserHistory.push({
- ...location,
- query: {
- ...location.query,
- chart: chartType,
- },
- });
- }
- function handleExpanderToggle(chartType: ReleaseComparisonChartType) {
- if (expanded.has(chartType)) {
- expanded.delete(chartType);
- setExpanded(new Set(expanded));
- } else {
- setExpanded(new Set(expanded.add(chartType)));
- }
- }
- function getTableHeaders(withExpanders: boolean) {
- const headers = [
- <DescriptionCell key="description">{t('Description')}</DescriptionCell>,
- <Cell key="releases">{t('All Releases')}</Cell>,
- <Cell key="release">{t('This Release')}</Cell>,
- <Cell key="change">{t('Change')}</Cell>,
- ];
- if (withExpanders) {
- headers.push(<Cell key="expanders" />);
- }
- return headers;
- }
- function getChartDiff(
- diff: ReleaseComparisonRow['diff'],
- diffColor: ReleaseComparisonRow['diffColor'],
- diffDirection: ReleaseComparisonRow['diffDirection']
- ) {
- return diff ? (
- <Change color={defined(diffColor) ? diffColor : undefined}>
- {diff}{' '}
- {defined(diffDirection) ? (
- <IconArrow direction={diffDirection} size="xs" />
- ) : diff === '0%' ? null : (
- <StyledNotAvailable />
- )}
- </Change>
- ) : null;
- }
- // if there are no sessions, we do not need to do row toggling because there won't be as many rows
- if (!hasHealthData) {
- charts.push(...additionalCharts);
- additionalCharts.splice(0, additionalCharts.length);
- }
- let activeChart = decodeScalar(
- location.query.chart,
- hasHealthData
- ? ReleaseComparisonChartType.CRASH_FREE_SESSIONS
- : hasPerformance
- ? ReleaseComparisonChartType.FAILURE_RATE
- : ReleaseComparisonChartType.ERROR_COUNT
- ) as ReleaseComparisonChartType;
- let chart = [...charts, ...additionalCharts].find(ch => ch.type === activeChart);
- if (!chart) {
- chart = charts[0];
- activeChart = charts[0].type;
- }
- const showPlaceholders = loading || eventsLoading;
- const withExpanders = hasHealthData || additionalCharts.length > 0;
- if (errored || !chart) {
- return (
- <Panel>
- <ErrorPanel>
- <IconWarning color="gray300" size="lg" />
- </ErrorPanel>
- </Panel>
- );
- }
- const titleChartDiff =
- chart.diff !== '0%' && chart.thisRelease !== '0%'
- ? getChartDiff(chart.diff, chart.diffColor, chart.diffDirection)
- : null;
- function renderChartRow({
- diff,
- diffColor,
- diffDirection,
- ...rest
- }: ReleaseComparisonRow) {
- return (
- <ReleaseComparisonChartRow
- {...rest}
- key={rest.type}
- diff={diff}
- showPlaceholders={showPlaceholders}
- activeChart={activeChart}
- onChartChange={handleChartChange}
- chartDiff={getChartDiff(diff, diffColor, diffDirection)}
- onExpanderToggle={handleExpanderToggle}
- expanded={expanded.has(rest.type)}
- withExpanders={withExpanders}
- />
- );
- }
- return (
- <Fragment>
- <ChartPanel>
- <ErrorBoundary mini>
- <ChartContainer>
- {[
- ReleaseComparisonChartType.ERROR_COUNT,
- ReleaseComparisonChartType.TRANSACTION_COUNT,
- ReleaseComparisonChartType.FAILURE_RATE,
- ].includes(activeChart)
- ? getDynamicText({
- value: (
- <ReleaseEventsChart
- release={release}
- project={project}
- chartType={activeChart}
- period={period ?? undefined}
- start={start}
- end={end}
- utc={utc === 'true'}
- value={chart.thisRelease}
- diff={titleChartDiff}
- />
- ),
- fixed: 'Events Chart',
- })
- : getDynamicText({
- value: (
- <ReleaseSessionsChart
- releaseSessions={releaseSessions}
- allSessions={allSessions}
- release={release}
- project={project}
- chartType={activeChart}
- platform={platform}
- period={period ?? undefined}
- start={start}
- end={end}
- utc={utc === 'true'}
- value={chart.thisRelease}
- diff={titleChartDiff}
- loading={loading}
- reloading={reloading}
- />
- ),
- fixed: 'Sessions Chart',
- })}
- </ChartContainer>
- </ErrorBoundary>
- </ChartPanel>
- <ChartTable
- headers={getTableHeaders(withExpanders)}
- data-test-id="release-comparison-table"
- withExpanders={withExpanders}
- >
- {charts.map(chartRow => renderChartRow(chartRow))}
- {isOtherExpanded && additionalCharts.map(chartRow => renderChartRow(chartRow))}
- {additionalCharts.length > 0 && (
- <ShowMoreWrapper onClick={() => setIsOtherExpanded(!isOtherExpanded)}>
- <ShowMoreTitle>
- <IconList size="xs" />
- {isOtherExpanded
- ? tn('Hide %s Other', 'Hide %s Others', additionalCharts.length)
- : tn('Show %s Other', 'Show %s Others', additionalCharts.length)}
- </ShowMoreTitle>
- <ShowMoreButton>
- <Button
- borderless
- size="zero"
- icon={<IconChevron direction={isOtherExpanded ? 'up' : 'down'} />}
- aria-label={t('Toggle additional charts')}
- />
- </ShowMoreButton>
- </ShowMoreWrapper>
- )}
- </ChartTable>
- </Fragment>
- );
- }
- const ChartPanel = styled(Panel)`
- margin-bottom: 0;
- border-bottom-left-radius: 0;
- border-bottom: none;
- border-bottom-right-radius: 0;
- `;
- const Cell = styled('div')`
- text-align: right;
- ${p => p.theme.overflowEllipsis}
- `;
- const DescriptionCell = styled(Cell)`
- text-align: left;
- overflow: visible;
- `;
- const Change = styled('div')<{color?: Color}>`
- font-size: ${p => p.theme.fontSizeMedium};
- ${p => p.color && `color: ${p.theme[p.color]}`}
- `;
- const ChartTable = styled(PanelTable)<{withExpanders: boolean}>`
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- grid-template-columns: minmax(400px, auto) repeat(3, minmax(min-content, 1fr)) ${p =>
- p.withExpanders ? '75px' : ''};
- > * {
- border-bottom: 1px solid ${p => p.theme.border};
- }
- @media (max-width: ${p => p.theme.breakpoints.large}) {
- grid-template-columns: repeat(4, minmax(min-content, 1fr)) ${p =>
- p.withExpanders ? '75px' : ''};
- }
- `;
- const StyledNotAvailable = styled(NotAvailable)`
- display: inline-block;
- `;
- const ShowMoreWrapper = styled('div')`
- display: contents;
- &:hover {
- cursor: pointer;
- }
- > * {
- padding: ${space(1)} ${space(2)};
- }
- `;
- const ShowMoreTitle = styled('div')`
- color: ${p => p.theme.gray300};
- font-size: ${p => p.theme.fontSizeMedium};
- display: inline-grid;
- grid-template-columns: auto auto;
- gap: 10px;
- align-items: center;
- justify-content: flex-start;
- svg {
- margin-left: ${space(0.25)};
- }
- `;
- const ShowMoreButton = styled('div')`
- grid-column: 2 / -1;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- `;
- export default ReleaseComparisonChart;
|