Files
2024-02-15 17:01:50 +00:00

1090 lines
34 KiB
TypeScript

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 type {Location} from 'history';
import type {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 {IconArrow, IconChevron, IconList, IconWarning} from 'sentry/icons';
import {t, tct, tn} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {
Organization,
PlatformKey,
ReleaseProject,
ReleaseWithHealth,
SessionApiResponse,
} from 'sentry/types';
import {
ReleaseComparisonChartType,
SessionFieldWithOperation,
SessionStatus,
} from 'sentry/types';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
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 type {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(),
dataset: DiscoverDatasets.METRICS_ENHANCED,
...commonQuery,
},
}),
api.requestPromise(url, {
query: {
field: ['failure_rate()', 'count()'],
query: new MutableSearch(['event.type:transaction']).formatString(),
dataset: DiscoverDatasets.METRICS_ENHANCED,
...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;