123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619 |
- 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 {SessionApiResponse} from 'sentry/types/organization';
- import {SessionFieldWithOperation, SessionStatus} from 'sentry/types/organization';
- import type {PlatformKey} from 'sentry/types/project';
- import type {ReleaseProject, ReleaseWithHealth} from 'sentry/types/release';
- import {ReleaseComparisonChartType} from 'sentry/types/release';
- 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));
|