Просмотр исходного кода

feat(ui): Add session status release comparison charts (#27474)

Matej Minar 3 лет назад
Родитель
Сommit
d0a47544dd

+ 11 - 1
static/app/components/charts/eventsChart.tsx

@@ -58,6 +58,7 @@ type ChartProps = {
     | React.ComponentType<BarChart['props']>
     | React.ComponentType<AreaChart['props']>
     | React.ComponentType<LineChart['props']>;
+  height?: number;
 };
 
 type State = {
@@ -155,6 +156,7 @@ class Chart extends React.Component<ChartProps, State> {
       previousSeriesName,
       seriesTransformer,
       colors,
+      height,
       ...props
     } = this.props;
     const {seriesSelection} = this.state;
@@ -237,6 +239,7 @@ class Chart extends React.Component<ChartProps, State> {
         onLegendSelectChanged={this.handleLegendSelectChanged}
         series={series}
         previousPeriod={previousTimeseriesData ? [previousTimeseriesData] : undefined}
+        height={height}
       />
     );
   }
@@ -338,6 +341,7 @@ export type EventsChartProps = {
   | 'legendOptions'
   | 'chartOptions'
   | 'chartComponent'
+  | 'height'
 >;
 
 type ChartDataProps = {
@@ -386,6 +390,7 @@ class EventsChart extends React.Component<EventsChartProps> {
       disableableSeries,
       chartComponent,
       usePageZoom,
+      height,
       ...props
     } = this.props;
     // Include previous only on relative dates (defaults to relative if no start and end)
@@ -421,7 +426,11 @@ class EventsChart extends React.Component<EventsChartProps> {
       const seriesData = results ? results : timeseriesData;
 
       return (
-        <TransitionChart loading={loading} reloading={reloading}>
+        <TransitionChart
+          loading={loading}
+          reloading={reloading}
+          height={height ? `${height}px` : undefined}
+        >
           <TransparentLoadingMask visible={reloading} />
 
           {React.isValidElement(chartHeader) && chartHeader}
@@ -445,6 +454,7 @@ class EventsChart extends React.Component<EventsChartProps> {
             chartOptions={chartOptions}
             disableableSeries={disableableSeries}
             chartComponent={chartComponent}
+            height={height}
           />
         </TransitionChart>
       );

+ 15 - 0
static/app/types/index.tsx

@@ -2110,9 +2110,24 @@ export enum SessionField {
   USERS = 'count_unique(user)',
 }
 
+export enum SessionStatus {
+  HEALTHY = 'healthy',
+  ABNORMAL = 'abnormal',
+  ERRORED = 'errored',
+  CRASHED = 'crashed',
+}
+
 export enum ReleaseComparisonChartType {
   CRASH_FREE_USERS = 'crashFreeUsers',
+  HEALTHY_USERS = 'healthyUsers',
+  ABNORMAL_USERS = 'abnormalUsers',
+  ERRORED_USERS = 'erroredUsers',
+  CRASHED_USERS = 'crashedUsers',
   CRASH_FREE_SESSIONS = 'crashFreeSessions',
+  HEALTHY_SESSIONS = 'healthySessions',
+  ABNORMAL_SESSIONS = 'abnormalSessions',
+  ERRORED_SESSIONS = 'erroredSessions',
+  CRASHED_SESSIONS = 'crashedSessions',
   SESSION_COUNT = 'sessionCount',
   USER_COUNT = 'userCount',
   ERROR_COUNT = 'errorCount',

+ 51 - 15
static/app/utils/sessions.tsx

@@ -7,7 +7,7 @@ import {
   ONE_WEEK,
   TWO_WEEKS,
 } from 'app/components/charts/utils';
-import {SessionApiResponse, SessionField} from 'app/types';
+import {SessionApiResponse, SessionField, SessionStatus} from 'app/types';
 import {SeriesDataUnit} from 'app/types/echarts';
 import {defined, percent} from 'app/utils';
 import {getCrashFreePercent} from 'app/views/releases/utils';
@@ -16,30 +16,33 @@ export function getCount(groups: SessionApiResponse['groups'] = [], field: Sessi
   return groups.reduce((acc, group) => acc + group.totals[field], 0);
 }
 
-export function getCrashCount(
+export function getCrashFreeRate(
   groups: SessionApiResponse['groups'] = [],
   field: SessionField
 ) {
-  return getCount(
-    groups.filter(({by}) => by['session.status'] === 'crashed'),
-    field
-  );
+  const crashedRate = getSessionStatusRate(groups, field, SessionStatus.CRASHED);
+
+  return defined(crashedRate) ? getCrashFreePercent(100 - crashedRate) : null;
 }
 
-export function getCrashFreeRate(
+export function getSessionStatusRate(
   groups: SessionApiResponse['groups'] = [],
-  field: SessionField
+  field: SessionField,
+  status: SessionStatus
 ) {
-  const totalCount = groups.reduce((acc, group) => acc + group.totals[field], 0);
+  const totalCount = getCount(groups, field);
 
-  const crashedCount = getCrashCount(groups, field);
+  const crashedCount = getCount(
+    groups.filter(({by}) => by['session.status'] === status),
+    field
+  );
 
   return !defined(totalCount) || totalCount === 0
     ? null
-    : getCrashFreePercent(100 - percent(crashedCount ?? 0, totalCount ?? 0));
+    : percent(crashedCount ?? 0, totalCount ?? 0);
 }
 
-export function getCrashFreeSeries(
+export function getCrashFreeRateSeries(
   groups: SessionApiResponse['groups'] = [],
   intervals: SessionApiResponse['intervals'] = [],
   field: SessionField
@@ -52,9 +55,8 @@ export function getCrashFreeSeries(
       );
 
       const intervalCrashedSessions =
-        groups.find(group => group.by['session.status'] === 'crashed')?.series[field][
-          i
-        ] ?? 0;
+        groups.find(group => group.by['session.status'] === SessionStatus.CRASHED)
+          ?.series[field][i] ?? 0;
 
       const crashedSessionsPercent = percent(
         intervalCrashedSessions,
@@ -73,6 +75,40 @@ export function getCrashFreeSeries(
   );
 }
 
+export function getSessionStatusRateSeries(
+  groups: SessionApiResponse['groups'] = [],
+  intervals: SessionApiResponse['intervals'] = [],
+  field: SessionField,
+  status: SessionStatus
+): SeriesDataUnit[] {
+  return compact(
+    intervals.map((interval, i) => {
+      const intervalTotalSessions = groups.reduce(
+        (acc, group) => acc + group.series[field][i],
+        0
+      );
+
+      const intervalStatusSessions =
+        groups.find(group => group.by['session.status'] === status)?.series[field][i] ??
+        0;
+
+      const statusSessionsPercent = percent(
+        intervalStatusSessions,
+        intervalTotalSessions
+      );
+
+      if (intervalTotalSessions === 0) {
+        return null;
+      }
+
+      return {
+        name: interval,
+        value: statusSessionsPercent,
+      };
+    })
+  );
+}
+
 export function getAdoptionSeries(
   releaseGroups: SessionApiResponse['groups'] = [],
   allGroups: SessionApiResponse['groups'] = [],

+ 511 - 23
static/app/views/releases/detail/overview/releaseComparisonChart/index.tsx

@@ -8,8 +8,6 @@ import {Location} from 'history';
 import {Client} from 'app/api';
 import ErrorPanel from 'app/components/charts/errorPanel';
 import {ChartContainer} from 'app/components/charts/styles';
-import TransitionChart from 'app/components/charts/transitionChart';
-import TransparentLoadingMask from 'app/components/charts/transparentLoadingMask';
 import Count from 'app/components/count';
 import NotAvailable from 'app/components/notAvailable';
 import {Panel, PanelTable} from 'app/components/panels';
@@ -28,16 +26,24 @@ import {
   ReleaseWithHealth,
   SessionApiResponse,
   SessionField,
+  SessionStatus,
 } from 'app/types';
 import {defined} from 'app/utils';
 import {formatPercentage} from 'app/utils/formatters';
 import {decodeList, decodeScalar} from 'app/utils/queryString';
-import {getCount, getCrashFreeRate, getCrashFreeSeries} from 'app/utils/sessions';
+import {
+  getCount,
+  getCrashFreeRate,
+  getCrashFreeRateSeries,
+  getSessionStatusRate,
+  getSessionStatusRateSeries,
+} from 'app/utils/sessions';
 import {Color, Theme} from 'app/utils/theme';
 import {QueryResults} from 'app/utils/tokenizeSearch';
 import {
   displayCrashFreeDiff,
   displayCrashFreePercent,
+  displaySessionStatusPercent,
   getReleaseBounds,
   getReleaseParams,
 } from 'app/views/releases/utils';
@@ -201,6 +207,66 @@ function ReleaseComparisonChart({
       ? releaseCrashFreeSessions - allCrashFreeSessions
       : null;
 
+  const releaseHealthySessions = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.HEALTHY
+  );
+  const allHealthySessions = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.HEALTHY
+  );
+  const diffHealthySessions =
+    defined(releaseHealthySessions) && defined(allHealthySessions)
+      ? releaseHealthySessions - allHealthySessions
+      : null;
+
+  const releaseAbnormalSessions = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.ABNORMAL
+  );
+  const allAbnormalSessions = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.ABNORMAL
+  );
+  const diffAbnormalSessions =
+    defined(releaseAbnormalSessions) && defined(allAbnormalSessions)
+      ? releaseAbnormalSessions - allAbnormalSessions
+      : null;
+
+  const releaseErroredSessions = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.ERRORED
+  );
+  const allErroredSessions = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.ERRORED
+  );
+  const diffErroredSessions =
+    defined(releaseErroredSessions) && defined(allErroredSessions)
+      ? releaseErroredSessions - allErroredSessions
+      : null;
+
+  const releaseCrashedSessions = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.CRASHED
+  );
+  const allCrashedSessions = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.SESSIONS,
+    SessionStatus.CRASHED
+  );
+  const diffCrashedSessions =
+    defined(releaseCrashedSessions) && defined(allCrashedSessions)
+      ? releaseCrashedSessions - allCrashedSessions
+      : null;
+
   const releaseCrashFreeUsers = getCrashFreeRate(
     releaseSessions?.groups,
     SessionField.USERS
@@ -211,6 +277,66 @@ function ReleaseComparisonChart({
       ? releaseCrashFreeUsers - allCrashFreeUsers
       : null;
 
+  const releaseHealthyUsers = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.HEALTHY
+  );
+  const allHealthyUsers = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.HEALTHY
+  );
+  const diffHealthyUsers =
+    defined(releaseHealthyUsers) && defined(allHealthyUsers)
+      ? releaseHealthyUsers - allHealthyUsers
+      : null;
+
+  const releaseAbnormalUsers = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.ABNORMAL
+  );
+  const allAbnormalUsers = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.ABNORMAL
+  );
+  const diffAbnormalUsers =
+    defined(releaseAbnormalUsers) && defined(allAbnormalUsers)
+      ? releaseAbnormalUsers - allAbnormalUsers
+      : null;
+
+  const releaseErroredUsers = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.ERRORED
+  );
+  const allErroredUsers = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.ERRORED
+  );
+  const diffErroredUsers =
+    defined(releaseErroredUsers) && defined(allErroredUsers)
+      ? releaseErroredUsers - allErroredUsers
+      : null;
+
+  const releaseCrashedUsers = getSessionStatusRate(
+    releaseSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.CRASHED
+  );
+  const allCrashedUsers = getSessionStatusRate(
+    allSessions?.groups,
+    SessionField.USERS,
+    SessionStatus.CRASHED
+  );
+  const diffCrashedUsers =
+    defined(releaseCrashedUsers) && defined(allCrashedUsers)
+      ? releaseCrashedUsers - allCrashedUsers
+      : null;
+
   const releaseSessionsCount = getCount(releaseSessions?.groups, SessionField.SESSIONS);
   const allSessionsCount = getCount(allSessions?.groups, SessionField.SESSIONS);
 
@@ -246,6 +372,94 @@ function ReleaseComparisonChart({
           : 'red300'
         : null,
     },
+    {
+      type: ReleaseComparisonChartType.HEALTHY_SESSIONS,
+      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,
+      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,
+      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,
+      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,
+    },
     {
       type: ReleaseComparisonChartType.CRASH_FREE_USERS,
       thisRelease: defined(releaseCrashFreeUsers)
@@ -264,6 +478,66 @@ function ReleaseComparisonChart({
           : 'red300'
         : null,
     },
+    {
+      type: ReleaseComparisonChartType.HEALTHY_USERS,
+      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,
+      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,
+      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,
+      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,
+    },
     {
       type: ReleaseComparisonChartType.FAILURE_RATE,
       thisRelease: eventsTotals?.releaseFailureRate
@@ -336,7 +610,7 @@ function ReleaseComparisonChart({
             {
               seriesName: t('This Release'),
               connectNulls: true,
-              data: getCrashFreeSeries(
+              data: getCrashFreeRateSeries(
                 releaseSessions?.groups,
                 releaseSessions?.intervals,
                 SessionField.SESSIONS
@@ -346,7 +620,7 @@ function ReleaseComparisonChart({
           previousSeries: [
             {
               seriesName: t('All Releases'),
-              data: getCrashFreeSeries(
+              data: getCrashFreeRateSeries(
                 allSessions?.groups,
                 allSessions?.intervals,
                 SessionField.SESSIONS
@@ -355,13 +629,121 @@ function ReleaseComparisonChart({
           ],
           markLines,
         };
+      case ReleaseComparisonChartType.HEALTHY_SESSIONS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.HEALTHY
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.HEALTHY
+              ),
+            },
+          ],
+          markLines,
+        };
+      case ReleaseComparisonChartType.ABNORMAL_SESSIONS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.ABNORMAL
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.ABNORMAL
+              ),
+            },
+          ],
+          markLines,
+        };
+      case ReleaseComparisonChartType.ERRORED_SESSIONS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.ERRORED
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.ERRORED
+              ),
+            },
+          ],
+          markLines,
+        };
+      case ReleaseComparisonChartType.CRASHED_SESSIONS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.CRASHED
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.SESSIONS,
+                SessionStatus.CRASHED
+              ),
+            },
+          ],
+          markLines,
+        };
       case ReleaseComparisonChartType.CRASH_FREE_USERS:
         return {
           series: [
             {
               seriesName: t('This Release'),
               connectNulls: true,
-              data: getCrashFreeSeries(
+              data: getCrashFreeRateSeries(
                 releaseSessions?.groups,
                 releaseSessions?.intervals,
                 SessionField.USERS
@@ -371,7 +753,7 @@ function ReleaseComparisonChart({
           previousSeries: [
             {
               seriesName: t('All Releases'),
-              data: getCrashFreeSeries(
+              data: getCrashFreeRateSeries(
                 allSessions?.groups,
                 allSessions?.intervals,
                 SessionField.USERS
@@ -380,6 +762,114 @@ function ReleaseComparisonChart({
           ],
           markLines,
         };
+      case ReleaseComparisonChartType.HEALTHY_USERS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.HEALTHY
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.HEALTHY
+              ),
+            },
+          ],
+          markLines,
+        };
+      case ReleaseComparisonChartType.ABNORMAL_USERS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.ABNORMAL
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.ABNORMAL
+              ),
+            },
+          ],
+          markLines,
+        };
+      case ReleaseComparisonChartType.ERRORED_USERS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.ERRORED
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.ERRORED
+              ),
+            },
+          ],
+          markLines,
+        };
+      case ReleaseComparisonChartType.CRASHED_USERS:
+        return {
+          series: [
+            {
+              seriesName: t('This Release'),
+              connectNulls: true,
+              data: getSessionStatusRateSeries(
+                releaseSessions?.groups,
+                releaseSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.CRASHED
+              ),
+            },
+          ],
+          previousSeries: [
+            {
+              seriesName: t('All Releases'),
+              data: getSessionStatusRateSeries(
+                allSessions?.groups,
+                allSessions?.intervals,
+                SessionField.USERS,
+                SessionStatus.CRASHED
+              ),
+            },
+          ],
+          markLines,
+        };
       case ReleaseComparisonChartType.SESSION_COUNT:
         return {
           series: Object.values(
@@ -461,22 +951,20 @@ function ReleaseComparisonChart({
               diff={chartDiff}
             />
           ) : (
-            <TransitionChart loading={loading} reloading={reloading}>
-              <TransparentLoadingMask visible={reloading} />
-
-              <ReleaseSessionsChart
-                series={[...(series ?? []), ...(markLines ?? [])]}
-                previousSeries={previousSeries ?? []}
-                chartType={activeChart}
-                platform={platform}
-                period={period ?? undefined}
-                start={start}
-                end={end}
-                utc={utc === 'true'}
-                value={chart.thisRelease}
-                diff={chartDiff}
-              />
-            </TransitionChart>
+            <ReleaseSessionsChart
+              series={[...(series ?? []), ...(markLines ?? [])]}
+              previousSeries={previousSeries ?? []}
+              chartType={activeChart}
+              platform={platform}
+              period={period ?? undefined}
+              start={start}
+              end={end}
+              utc={utc === 'true'}
+              value={chart.thisRelease}
+              diff={chartDiff}
+              loading={loading}
+              reloading={reloading}
+            />
           )}
         </ChartContainer>
       </ChartPanel>

+ 3 - 3
static/app/views/releases/detail/overview/releaseComparisonChart/releaseEventsChart.tsx

@@ -15,7 +15,7 @@ import withApi from 'app/utils/withApi';
 import withOrganization from 'app/utils/withOrganization';
 import {getTermHelp, PERFORMANCE_TERM} from 'app/views/performance/data';
 
-import {releaseComparisonChartLabels} from '../../utils';
+import {releaseComparisonChartTitles} from '../../utils';
 
 type Props = WithRouterProps & {
   version: string;
@@ -140,7 +140,7 @@ function ReleaseEventsChart({
       chartHeader={
         <Fragment>
           <HeaderTitleLegend>
-            {releaseComparisonChartLabels[chartType]}
+            {releaseComparisonChartTitles[chartType]}
             {getHelp() && <QuestionTooltip size="sm" position="top" title={getHelp()} />}
           </HeaderTitleLegend>
 
@@ -152,9 +152,9 @@ function ReleaseEventsChart({
       legendOptions={{right: 10, top: 0}}
       chartOptions={{
         grid: {left: '10px', right: '10px', top: '70px', bottom: '0px'},
-        ['height' as any]: 240, // TODO(ts): echart types
       }}
       usePageZoom
+      height={240}
     />
   );
 }

+ 75 - 4
static/app/views/releases/detail/overview/releaseComparisonChart/releaseSessionsChart.tsx

@@ -2,11 +2,14 @@ import * as React from 'react';
 import {withRouter} from 'react-router';
 import {WithRouterProps} from 'react-router/lib/withRouter';
 import {withTheme} from '@emotion/react';
+import round from 'lodash/round';
 
 import AreaChart from 'app/components/charts/areaChart';
 import ChartZoom from 'app/components/charts/chartZoom';
 import StackedAreaChart from 'app/components/charts/stackedAreaChart';
 import {HeaderTitleLegend, HeaderValue} from 'app/components/charts/styles';
+import TransitionChart from 'app/components/charts/transitionChart';
+import TransparentLoadingMask from 'app/components/charts/transparentLoadingMask';
 import QuestionTooltip from 'app/components/questionTooltip';
 import {PlatformKey} from 'app/data/platformCategories';
 import {ReleaseComparisonChartType} from 'app/types';
@@ -17,7 +20,7 @@ import {displayCrashFreePercent} from 'app/views/releases/utils';
 
 import {
   releaseComparisonChartHelp,
-  releaseComparisonChartLabels,
+  releaseComparisonChartTitles,
   releaseMarkLinesLabels,
 } from '../../utils';
 
@@ -29,6 +32,8 @@ type Props = {
   platform: PlatformKey;
   value: React.ReactNode;
   diff: React.ReactNode;
+  loading: boolean;
+  reloading: boolean;
   period?: string;
   start?: string;
   end?: string;
@@ -48,7 +53,15 @@ class ReleaseSessionsChart extends React.Component<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:
         return defined(value) ? `${value}%` : '\u2015';
       case ReleaseComparisonChartType.SESSION_COUNT:
       case ReleaseComparisonChartType.USER_COUNT:
@@ -70,6 +83,21 @@ class ReleaseSessionsChart extends React.Component<Props> {
             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:
@@ -83,7 +111,15 @@ class ReleaseSessionsChart extends React.Component<Props> {
     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:
@@ -92,6 +128,37 @@ class ReleaseSessionsChart extends React.Component<Props> {
     }
   }
 
+  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;
+    }
+  }
+
   render() {
     const {
       series,
@@ -104,6 +171,8 @@ class ReleaseSessionsChart extends React.Component<Props> {
       utc,
       value,
       diff,
+      loading,
+      reloading,
     } = this.props;
 
     const Chart = this.getChart();
@@ -118,9 +187,10 @@ class ReleaseSessionsChart extends React.Component<Props> {
     };
 
     return (
-      <React.Fragment>
+      <TransitionChart loading={loading} reloading={reloading} height="240px">
+        <TransparentLoadingMask visible={reloading} />
         <HeaderTitleLegend>
-          {releaseComparisonChartLabels[chartType]}
+          {releaseComparisonChartTitles[chartType]}
           {releaseComparisonChartHelp[chartType] && (
             <QuestionTooltip
               size="sm"
@@ -156,12 +226,13 @@ class ReleaseSessionsChart extends React.Component<Props> {
               }}
               yAxis={this.configureYAxis()}
               tooltip={{valueFormatter: this.formatTooltipValue}}
+              colors={this.getColors()}
               transformSinglePointToBar
               height={240}
             />
           )}
         </ChartZoom>
-      </React.Fragment>
+      </TransitionChart>
     );
   }
 }

+ 28 - 2
static/app/views/releases/detail/utils.tsx

@@ -140,8 +140,34 @@ export function getReleaseEventView(
 }
 
 export const releaseComparisonChartLabels = {
-  [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]: t('Crash Free Sessions'),
-  [ReleaseComparisonChartType.CRASH_FREE_USERS]: t('Crash Free Users'),
+  [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]: t('Crash Free Session Rate'),
+  [ReleaseComparisonChartType.HEALTHY_SESSIONS]: t('Healthy'),
+  [ReleaseComparisonChartType.ABNORMAL_SESSIONS]: t('Abnormal'),
+  [ReleaseComparisonChartType.ERRORED_SESSIONS]: t('Errored'),
+  [ReleaseComparisonChartType.CRASHED_SESSIONS]: t('Crashed Session Rate'),
+  [ReleaseComparisonChartType.CRASH_FREE_USERS]: t('Crash Free User Rate'),
+  [ReleaseComparisonChartType.HEALTHY_USERS]: t('Healthy'),
+  [ReleaseComparisonChartType.ABNORMAL_USERS]: t('Abnormal'),
+  [ReleaseComparisonChartType.ERRORED_USERS]: t('Errored'),
+  [ReleaseComparisonChartType.CRASHED_USERS]: t('Crashed User Rate'),
+  [ReleaseComparisonChartType.SESSION_COUNT]: t('Session Count'),
+  [ReleaseComparisonChartType.USER_COUNT]: t('User Count'),
+  [ReleaseComparisonChartType.ERROR_COUNT]: t('Error Count'),
+  [ReleaseComparisonChartType.TRANSACTION_COUNT]: t('Transaction Count'),
+  [ReleaseComparisonChartType.FAILURE_RATE]: t('Failure Rate'),
+};
+
+export const releaseComparisonChartTitles = {
+  [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]: t('Crash Free Session Rate'),
+  [ReleaseComparisonChartType.HEALTHY_SESSIONS]: t('Healthy Session Rate'),
+  [ReleaseComparisonChartType.ABNORMAL_SESSIONS]: t('Abnormal Session Rate'),
+  [ReleaseComparisonChartType.ERRORED_SESSIONS]: t('Errored Session Rate'),
+  [ReleaseComparisonChartType.CRASHED_SESSIONS]: t('Crashed Session Rate'),
+  [ReleaseComparisonChartType.CRASH_FREE_USERS]: t('Crash Free User Rate'),
+  [ReleaseComparisonChartType.HEALTHY_USERS]: t('Healthy User Rate'),
+  [ReleaseComparisonChartType.ABNORMAL_USERS]: t('Abnormal User Rate'),
+  [ReleaseComparisonChartType.ERRORED_USERS]: t('Errored User Rate'),
+  [ReleaseComparisonChartType.CRASHED_USERS]: t('Crashed User Rate'),
   [ReleaseComparisonChartType.SESSION_COUNT]: t('Session Count'),
   [ReleaseComparisonChartType.USER_COUNT]: t('User Count'),
   [ReleaseComparisonChartType.ERROR_COUNT]: t('Error Count'),

+ 8 - 0
static/app/views/releases/utils/index.tsx

@@ -48,6 +48,14 @@ export const displayCrashFreePercent = (
   return `${rounded}\u0025`;
 };
 
+export const getSessionStatusPercent = (percent: number) => {
+  return round(percent, 3);
+};
+
+export const displaySessionStatusPercent = (percent: number) => {
+  return `${getSessionStatusPercent(percent).toLocaleString()}\u0025`;
+};
+
 export const displayCrashFreeDiff = (
   diffPercent: number,
   crashFreePercent?: number | null