123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089 |
- import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
- 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,
- type SessionApiResponse,
- SessionFieldWithOperation,
- SessionStatus,
- } from 'sentry/types/organization';
- import type {PlatformKey} from 'sentry/types/project';
- import {
- ReleaseComparisonChartType,
- type ReleaseProject,
- type ReleaseWithHealth,
- } from 'sentry/types/release';
- import {defined} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import {DiscoverDatasets} from 'sentry/utils/discover/types';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import {formatPercentage} from 'sentry/utils/number/formatPercentage';
- 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;
- `;
- export 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;
|