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

626 lines
20 KiB
TypeScript

import {Component} from 'react';
import type {WithRouterProps} from 'react-router';
import type {Theme} from '@emotion/react';
import {withTheme} from '@emotion/react';
import round from 'lodash/round';
import type {AreaChartProps} from 'sentry/components/charts/areaChart';
import {AreaChart} from 'sentry/components/charts/areaChart';
import ChartZoom from 'sentry/components/charts/chartZoom';
import StackedAreaChart from 'sentry/components/charts/stackedAreaChart';
import {HeaderTitleLegend, HeaderValue} from 'sentry/components/charts/styles';
import TransitionChart from 'sentry/components/charts/transitionChart';
import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
import QuestionTooltip from 'sentry/components/questionTooltip';
import {t} from 'sentry/locale';
import type {
PlatformKey,
ReleaseProject,
ReleaseWithHealth,
SessionApiResponse,
} from 'sentry/types';
import {
ReleaseComparisonChartType,
SessionFieldWithOperation,
SessionStatus,
} from 'sentry/types';
import {defined} from 'sentry/utils';
import {
getCountSeries,
getCrashFreeRateSeries,
getSessionStatusRateSeries,
initSessionsChart,
MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
} from 'sentry/utils/sessions';
// eslint-disable-next-line no-restricted-imports
import withSentryRouter from 'sentry/utils/withSentryRouter';
import {displayCrashFreePercent} from 'sentry/views/releases/utils';
import {
generateReleaseMarkLines,
releaseComparisonChartHelp,
releaseComparisonChartTitles,
releaseMarkLinesLabels,
} from '../../utils';
type Props = {
allSessions: SessionApiResponse | null;
chartType: ReleaseComparisonChartType;
diff: React.ReactNode;
loading: boolean;
platform: PlatformKey;
project: ReleaseProject;
release: ReleaseWithHealth;
releaseSessions: SessionApiResponse | null;
reloading: boolean;
theme: Theme;
value: React.ReactNode;
end?: string;
period?: string | null;
start?: string;
utc?: boolean;
} & WithRouterProps;
class ReleaseSessionsChart extends Component<Props> {
formatTooltipValue = (value: string | number | null, label?: string) => {
if (label && Object.values(releaseMarkLinesLabels).includes(label)) {
return '';
}
const {chartType} = this.props;
if (value === null) {
return '\u2015';
}
switch (chartType) {
case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
case ReleaseComparisonChartType.HEALTHY_SESSIONS:
case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
case ReleaseComparisonChartType.ERRORED_SESSIONS:
case ReleaseComparisonChartType.CRASHED_SESSIONS:
case ReleaseComparisonChartType.CRASH_FREE_USERS:
case ReleaseComparisonChartType.HEALTHY_USERS:
case ReleaseComparisonChartType.ABNORMAL_USERS:
case ReleaseComparisonChartType.ERRORED_USERS:
case ReleaseComparisonChartType.CRASHED_USERS:
return defined(value) ? `${value}%` : '\u2015';
case ReleaseComparisonChartType.SESSION_COUNT:
case ReleaseComparisonChartType.USER_COUNT:
default:
return typeof value === 'number' ? value.toLocaleString() : value;
}
};
getYAxis() {
const {theme, chartType} = this.props;
switch (chartType) {
case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
case ReleaseComparisonChartType.CRASH_FREE_USERS:
return {
max: 100,
scale: true,
axisLabel: {
formatter: (value: number) => displayCrashFreePercent(value),
color: theme.chartLabel,
},
};
case ReleaseComparisonChartType.HEALTHY_SESSIONS:
case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
case ReleaseComparisonChartType.ERRORED_SESSIONS:
case ReleaseComparisonChartType.CRASHED_SESSIONS:
case ReleaseComparisonChartType.HEALTHY_USERS:
case ReleaseComparisonChartType.ABNORMAL_USERS:
case ReleaseComparisonChartType.ERRORED_USERS:
case ReleaseComparisonChartType.CRASHED_USERS:
return {
scale: true,
axisLabel: {
formatter: (value: number) => `${round(value, 2)}%`,
color: theme.chartLabel,
},
};
case ReleaseComparisonChartType.SESSION_COUNT:
case ReleaseComparisonChartType.USER_COUNT:
default:
return undefined;
}
}
getChart():
| React.ComponentType<StackedAreaChart['props']>
| React.ComponentType<AreaChartProps> {
const {chartType} = this.props;
switch (chartType) {
case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
case ReleaseComparisonChartType.HEALTHY_SESSIONS:
case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
case ReleaseComparisonChartType.ERRORED_SESSIONS:
case ReleaseComparisonChartType.CRASHED_SESSIONS:
case ReleaseComparisonChartType.CRASH_FREE_USERS:
case ReleaseComparisonChartType.HEALTHY_USERS:
case ReleaseComparisonChartType.ABNORMAL_USERS:
case ReleaseComparisonChartType.ERRORED_USERS:
case ReleaseComparisonChartType.CRASHED_USERS:
default:
return AreaChart;
case ReleaseComparisonChartType.SESSION_COUNT:
case ReleaseComparisonChartType.USER_COUNT:
return StackedAreaChart;
}
}
getColors() {
const {theme, chartType} = this.props;
const colors = theme.charts.getColorPalette(14);
switch (chartType) {
case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
return [colors[0]];
case ReleaseComparisonChartType.HEALTHY_SESSIONS:
return [theme.green300];
case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
return [colors[15]];
case ReleaseComparisonChartType.ERRORED_SESSIONS:
return [colors[12]];
case ReleaseComparisonChartType.CRASHED_SESSIONS:
return [theme.red300];
case ReleaseComparisonChartType.CRASH_FREE_USERS:
return [colors[6]];
case ReleaseComparisonChartType.HEALTHY_USERS:
return [theme.green300];
case ReleaseComparisonChartType.ABNORMAL_USERS:
return [colors[15]];
case ReleaseComparisonChartType.ERRORED_USERS:
return [colors[12]];
case ReleaseComparisonChartType.CRASHED_USERS:
return [theme.red300];
case ReleaseComparisonChartType.SESSION_COUNT:
case ReleaseComparisonChartType.USER_COUNT:
default:
return undefined;
}
}
getSeries(chartType: ReleaseComparisonChartType) {
const {releaseSessions, allSessions, release, location, project, theme} = this.props;
const countCharts = initSessionsChart(theme);
if (!releaseSessions) {
return {};
}
const markLines = generateReleaseMarkLines(release, project, theme, location);
switch (chartType) {
case ReleaseComparisonChartType.CRASH_FREE_SESSIONS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getCrashFreeRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.SESSIONS
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getCrashFreeRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.SESSIONS
),
},
],
markLines,
};
case ReleaseComparisonChartType.HEALTHY_SESSIONS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.HEALTHY
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.HEALTHY
),
},
],
markLines,
};
case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.ABNORMAL
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.ABNORMAL
),
},
],
markLines,
};
case ReleaseComparisonChartType.ERRORED_SESSIONS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.ERRORED
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.ERRORED
),
},
],
markLines,
};
case ReleaseComparisonChartType.CRASHED_SESSIONS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.CRASHED
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.SESSIONS,
SessionStatus.CRASHED
),
},
],
markLines,
};
case ReleaseComparisonChartType.CRASH_FREE_USERS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getCrashFreeRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.USERS
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getCrashFreeRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.USERS
),
},
],
markLines,
};
case ReleaseComparisonChartType.HEALTHY_USERS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.HEALTHY
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.HEALTHY
),
},
],
markLines,
};
case ReleaseComparisonChartType.ABNORMAL_USERS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.ABNORMAL
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.ABNORMAL
),
},
],
markLines,
};
case ReleaseComparisonChartType.ERRORED_USERS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.ERRORED
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.ERRORED
),
},
],
markLines,
};
case ReleaseComparisonChartType.CRASHED_USERS:
return {
series: [
{
seriesName: t('This Release'),
connectNulls: true,
data: getSessionStatusRateSeries(
releaseSessions?.groups,
releaseSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.CRASHED
),
},
],
previousSeries: [
{
seriesName: t('All Releases'),
data: getSessionStatusRateSeries(
allSessions?.groups,
allSessions?.intervals,
SessionFieldWithOperation.USERS,
SessionStatus.CRASHED
),
},
],
markLines,
};
case ReleaseComparisonChartType.SESSION_COUNT:
return {
series: [
{
...countCharts[SessionStatus.HEALTHY],
data: getCountSeries(
SessionFieldWithOperation.SESSIONS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.HEALTHY
),
releaseSessions.intervals
),
},
{
...countCharts[SessionStatus.ERRORED],
data: getCountSeries(
SessionFieldWithOperation.SESSIONS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.ERRORED
),
releaseSessions.intervals
),
},
{
...countCharts[SessionStatus.ABNORMAL],
data: getCountSeries(
SessionFieldWithOperation.SESSIONS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.ABNORMAL
),
releaseSessions.intervals
),
},
{
...countCharts[SessionStatus.CRASHED],
data: getCountSeries(
SessionFieldWithOperation.SESSIONS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.CRASHED
),
releaseSessions.intervals
),
},
],
markLines,
};
case ReleaseComparisonChartType.USER_COUNT:
return {
series: [
{
...countCharts[SessionStatus.HEALTHY],
data: getCountSeries(
SessionFieldWithOperation.USERS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.HEALTHY
),
releaseSessions.intervals
),
},
{
...countCharts[SessionStatus.ERRORED],
data: getCountSeries(
SessionFieldWithOperation.USERS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.ERRORED
),
releaseSessions.intervals
),
},
{
...countCharts[SessionStatus.ABNORMAL],
data: getCountSeries(
SessionFieldWithOperation.USERS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.ABNORMAL
),
releaseSessions.intervals
),
},
{
...countCharts[SessionStatus.CRASHED],
data: getCountSeries(
SessionFieldWithOperation.USERS,
releaseSessions.groups.find(
g => g.by['session.status'] === SessionStatus.CRASHED
),
releaseSessions.intervals
),
},
],
markLines,
};
default:
return {};
}
}
render() {
const {chartType, router, period, start, end, utc, value, diff, loading, reloading} =
this.props;
const Chart = this.getChart();
const {series, previousSeries, markLines} = this.getSeries(chartType);
const legend = {
right: 10,
top: 0,
textStyle: {
padding: [2, 0, 0, 0],
},
data: [...(series ?? []), ...(previousSeries ?? [])].map(s => s.seriesName),
};
return (
<TransitionChart loading={loading} reloading={reloading} height="240px">
<TransparentLoadingMask visible={reloading} />
<HeaderTitleLegend aria-label={t('Chart Title')}>
{releaseComparisonChartTitles[chartType]}
{releaseComparisonChartHelp[chartType] && (
<QuestionTooltip
size="sm"
position="top"
title={releaseComparisonChartHelp[chartType]}
/>
)}
</HeaderTitleLegend>
<HeaderValue aria-label={t('Chart Value')}>
{value} {diff}
</HeaderValue>
<ChartZoom
router={router}
period={period}
utc={utc}
start={start}
end={end}
usePageDate
>
{zoomRenderProps => (
<Chart
legend={legend}
series={[...(series ?? []), ...(markLines ?? [])]}
previousPeriod={previousSeries ?? []}
{...zoomRenderProps}
grid={{
left: '10px',
right: '10px',
top: '70px',
bottom: '0px',
}}
minutesThresholdToDisplaySeconds={MINUTES_THRESHOLD_TO_DISPLAY_SECONDS}
yAxis={this.getYAxis()}
tooltip={{valueFormatter: this.formatTooltipValue}}
colors={this.getColors()}
transformSinglePointToBar
height={240}
/>
)}
</ChartZoom>
</TransitionChart>
);
}
}
export default withTheme(withSentryRouter(ReleaseSessionsChart));