Browse Source

feat(ui): Add crash free users to project details (#31998)

Matej Minar 3 years ago
parent
commit
048e9254e4

+ 26 - 19
static/app/views/projectDetail/charts/projectBaseSessionsChart.tsx

@@ -32,7 +32,10 @@ import ProjectSessionsChartRequest from './projectSessionsChartRequest';
 
 type Props = {
   api: Client;
-  displayMode: DisplayModes.SESSIONS | DisplayModes.STABILITY;
+  displayMode:
+    | DisplayModes.SESSIONS
+    | DisplayModes.STABILITY_USERS
+    | DisplayModes.STABILITY;
   onTotalValuesChange: (value: number | null) => void;
   organization: Organization;
   router: InjectedRouter;
@@ -140,7 +143,10 @@ function ProjectBaseSessionsChart({
 }
 
 type ChartProps = {
-  displayMode: DisplayModes.SESSIONS | DisplayModes.STABILITY;
+  displayMode:
+    | DisplayModes.SESSIONS
+    | DisplayModes.STABILITY
+    | DisplayModes.STABILITY_USERS;
   releaseSeries: Series[];
   reloading: boolean;
   theme: Theme;
@@ -206,6 +212,12 @@ class Chart extends Component<ChartProps, ChartState> {
     );
   };
 
+  get isCrashFree() {
+    const {displayMode} = this.props;
+
+    return [DisplayModes.STABILITY, DisplayModes.STABILITY_USERS].includes(displayMode);
+  }
+
   get legend(): LegendComponentOption {
     const {theme, timeSeries, previousTimeSeries, releaseSeries} = this.props;
     const {seriesSelection} = this.state;
@@ -250,8 +262,6 @@ class Chart extends Component<ChartProps, ChartState> {
   }
 
   get chartOptions() {
-    const {displayMode} = this.props;
-
     return {
       grid: {left: '10px', right: '10px', top: '40px', bottom: '0px'},
       seriesOptions: {
@@ -265,32 +275,29 @@ class Chart extends Component<ChartProps, ChartState> {
             return '\u2014';
           }
 
-          if (displayMode === DisplayModes.STABILITY) {
+          if (this.isCrashFree) {
             return displayCrashFreePercent(value, 0, 3);
           }
 
           return typeof value === 'number' ? value.toLocaleString() : value;
         },
       },
-      yAxis:
-        displayMode === DisplayModes.STABILITY
-          ? {
-              axisLabel: {
-                formatter: (value: number) => displayCrashFreePercent(value),
-              },
-              scale: true,
-              max: 100,
-            }
-          : {min: 0},
+      yAxis: this.isCrashFree
+        ? {
+            axisLabel: {
+              formatter: (value: number) => displayCrashFreePercent(value),
+            },
+            scale: true,
+            max: 100,
+          }
+        : {min: 0},
     };
   }
 
   render() {
-    const {zoomRenderProps, timeSeries, previousTimeSeries, releaseSeries, displayMode} =
-      this.props;
+    const {zoomRenderProps, timeSeries, previousTimeSeries, releaseSeries} = this.props;
 
-    const ChartComponent =
-      displayMode === DisplayModes.STABILITY ? LineChart : StackedAreaChart;
+    const ChartComponent = this.isCrashFree ? LineChart : StackedAreaChart;
 
     return (
       <ChartComponent

+ 19 - 13
static/app/views/projectDetail/charts/projectSessionsChartRequest.tsx

@@ -45,7 +45,10 @@ type ProjectSessionsChartRequestRenderProps = {
 type Props = {
   api: Client;
   children: (renderProps: ProjectSessionsChartRequestRenderProps) => React.ReactNode;
-  displayMode: DisplayModes.SESSIONS | DisplayModes.STABILITY;
+  displayMode:
+    | DisplayModes.SESSIONS
+    | DisplayModes.STABILITY
+    | DisplayModes.STABILITY_USERS;
   onTotalValuesChange: (value: number | null) => void;
   organization: Organization;
   selection: PageFilters;
@@ -156,12 +159,19 @@ class ProjectSessionsChartRequest extends React.Component<Props, State> {
     return `/organizations/${organization.slug}/sessions/`;
   }
 
+  get field() {
+    const {displayMode} = this.props;
+    return displayMode === DisplayModes.STABILITY_USERS
+      ? SessionField.USERS
+      : SessionField.SESSIONS;
+  }
+
   queryParams({shouldFetchWithPrevious = false}): Record<string, any> {
     const {selection, query, organization} = this.props;
     const {datetime, projects, environments: environment} = selection;
 
     const baseParams = {
-      field: 'sum(session)',
+      field: this.field,
       groupBy: 'session.status',
       interval: getSessionsInterval(datetime, {
         highFidelity: organization.features.includes('minute-resolution-sessions'),
@@ -192,6 +202,7 @@ class ProjectSessionsChartRequest extends React.Component<Props, State> {
 
   transformData(responseData: SessionApiResponse, {fetchedWithPrevious = false}) {
     const {theme} = this.props;
+    const {field} = this;
 
     // Take the floor just in case, but data should always be divisible by 2
     const dataMiddleIndex = Math.floor(responseData.intervals.length / 2);
@@ -200,7 +211,7 @@ class ProjectSessionsChartRequest extends React.Component<Props, State> {
     const totalSessions = responseData.groups.reduce(
       (acc, group) =>
         acc +
-        group.series['sum(session)']
+        group.series[field]
           .slice(fetchedWithPrevious ? dataMiddleIndex : 0)
           .reduce((value, groupAcc) => groupAcc + value, 0),
       0
@@ -210,7 +221,7 @@ class ProjectSessionsChartRequest extends React.Component<Props, State> {
       ? responseData.groups.reduce(
           (acc, group) =>
             acc +
-            group.series['sum(session)']
+            group.series[field]
               .slice(0, dataMiddleIndex)
               .reduce((value, groupAcc) => groupAcc + value, 0),
           0
@@ -228,18 +239,14 @@ class ProjectSessionsChartRequest extends React.Component<Props, State> {
             const totalIntervalSessions = responseData.groups.reduce(
               (acc, group) =>
                 acc +
-                group.series['sum(session)'].slice(
-                  fetchedWithPrevious ? dataMiddleIndex : 0
-                )[i],
+                group.series[field].slice(fetchedWithPrevious ? dataMiddleIndex : 0)[i],
               0
             );
 
             const intervalCrashedSessions =
               responseData.groups
                 .find(group => group.by['session.status'] === 'crashed')
-                ?.series['sum(session)'].slice(fetchedWithPrevious ? dataMiddleIndex : 0)[
-                i
-              ] ?? 0;
+                ?.series[field].slice(fetchedWithPrevious ? dataMiddleIndex : 0)[i] ?? 0;
 
             const crashedSessionsPercent = percent(
               intervalCrashedSessions,
@@ -264,15 +271,14 @@ class ProjectSessionsChartRequest extends React.Component<Props, State> {
           seriesName: t('Previous Period'),
           data: responseData.intervals.slice(0, dataMiddleIndex).map((_interval, i) => {
             const totalIntervalSessions = responseData.groups.reduce(
-              (acc, group) =>
-                acc + group.series['sum(session)'].slice(0, dataMiddleIndex)[i],
+              (acc, group) => acc + group.series[field].slice(0, dataMiddleIndex)[i],
               0
             );
 
             const intervalCrashedSessions =
               responseData.groups
                 .find(group => group.by['session.status'] === 'crashed')
-                ?.series['sum(session)'].slice(0, dataMiddleIndex)[i] ?? 0;
+                ?.series[field].slice(0, dataMiddleIndex)[i] ?? 0;
 
             const crashedSessionsPercent = percent(
               intervalCrashedSessions,

+ 23 - 0
static/app/views/projectDetail/projectCharts.tsx

@@ -51,6 +51,7 @@ export enum DisplayModes {
   ERRORS = 'errors',
   TRANSACTIONS = 'transactions',
   STABILITY = 'crash_free',
+  STABILITY_USERS = 'crash_free_users',
   SESSIONS = 'sessions',
 }
 
@@ -135,6 +136,14 @@ class ProjectCharts extends Component<Props, State> {
           this.otherActiveDisplayModes.includes(DisplayModes.STABILITY) || !hasSessions,
         tooltip: !hasSessions ? noHealthTooltip : undefined,
       },
+      {
+        value: DisplayModes.STABILITY_USERS,
+        label: t('Crash Free Users'),
+        disabled:
+          this.otherActiveDisplayModes.includes(DisplayModes.STABILITY_USERS) ||
+          !hasSessions,
+        tooltip: !hasSessions ? noHealthTooltip : undefined,
+      },
       {
         value: DisplayModes.APDEX,
         label: t('Apdex'),
@@ -202,6 +211,8 @@ class ProjectCharts extends Component<Props, State> {
       case DisplayModes.STABILITY:
       case DisplayModes.SESSIONS:
         return t('Total Sessions');
+      case DisplayModes.STABILITY_USERS:
+        return t('Total Users');
       case DisplayModes.APDEX:
       case DisplayModes.FAILURE_RATE:
       case DisplayModes.TPM:
@@ -388,6 +399,18 @@ class ProjectCharts extends Component<Props, State> {
                   query={query}
                 />
               )}
+              {displayMode === DisplayModes.STABILITY_USERS && (
+                <ProjectBaseSessionsChart
+                  title={t('Crash Free Users')}
+                  help={getSessionTermDescription(SessionTerm.CRASH_FREE_USERS, null)}
+                  router={router}
+                  api={api}
+                  organization={organization}
+                  onTotalValuesChange={this.handleTotalValuesChange}
+                  displayMode={displayMode}
+                  query={query}
+                />
+              )}
               {displayMode === DisplayModes.SESSIONS && (
                 <ProjectBaseSessionsChart
                   title={t('Number of Sessions')}

+ 16 - 2
static/app/views/projectDetail/projectScoreCards/projectScoreCards.tsx

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled';
 
 import space from 'sentry/styles/space';
-import {Organization, PageFilters} from 'sentry/types';
+import {Organization, PageFilters, SessionField} from 'sentry/types';
 
 import ProjectApdexScoreCard from './projectApdexScoreCard';
 import ProjectStabilityScoreCard from './projectStabilityScoreCard';
@@ -32,6 +32,16 @@ function ProjectScoreCards({
         isProjectStabilized={isProjectStabilized}
         hasSessions={hasSessions}
         query={query}
+        field={SessionField.SESSIONS}
+      />
+
+      <ProjectStabilityScoreCard
+        organization={organization}
+        selection={selection}
+        isProjectStabilized={isProjectStabilized}
+        hasSessions={hasSessions}
+        query={query}
+        field={SessionField.USERS}
       />
 
       <ProjectVelocityScoreCard
@@ -54,9 +64,13 @@ function ProjectScoreCards({
 
 const CardWrapper = styled('div')`
   display: grid;
-  grid-template-columns: repeat(3, minmax(0, 1fr));
+  grid-template-columns: repeat(4, minmax(0, 1fr));
   grid-column-gap: ${space(2)};
 
+  @media (max-width: 1600px) {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
   @media (max-width: ${p => p.theme.breakpoints[0]}) {
     grid-template-columns: 1fr;
     grid-template-rows: repeat(3, 1fr);

+ 18 - 11
static/app/views/projectDetail/projectScoreCards/projectStabilityScoreCard.tsx

@@ -11,7 +11,7 @@ import ScoreCard from 'sentry/components/scoreCard';
 import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
 import {IconArrow} from 'sentry/icons';
 import {t} from 'sentry/locale';
-import {Organization, PageFilters, SessionApiResponse} from 'sentry/types';
+import {Organization, PageFilters, SessionApiResponse, SessionField} from 'sentry/types';
 import {defined, percent} from 'sentry/utils';
 import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
 import {getPeriod} from 'sentry/utils/getPeriod';
@@ -24,6 +24,7 @@ import {
 import MissingReleasesButtons from '../missingFeatureButtons/missingReleasesButtons';
 
 type Props = AsyncComponent['props'] & {
+  field: SessionField.SESSIONS | SessionField.USERS;
   hasSessions: boolean | null;
   isProjectStabilized: boolean;
   organization: Organization;
@@ -48,7 +49,8 @@ class ProjectStabilityScoreCard extends AsyncComponent<Props, State> {
   }
 
   getEndpoints() {
-    const {organization, selection, isProjectStabilized, hasSessions, query} = this.props;
+    const {organization, selection, isProjectStabilized, hasSessions, query, field} =
+      this.props;
 
     if (!isProjectStabilized || !hasSessions) {
       return [];
@@ -59,10 +61,10 @@ class ProjectStabilityScoreCard extends AsyncComponent<Props, State> {
     const commonQuery = {
       environment,
       project: projects[0],
-      field: 'sum(session)',
       groupBy: 'session.status',
       interval: getDiffInMinutes(datetime) > 24 * 60 ? '1d' : '1h',
       query,
+      field,
     };
 
     // Unfortunately we can't do something like statsPeriod=28d&interval=14d to get scores for this and previous interval with the single request
@@ -110,15 +112,18 @@ class ProjectStabilityScoreCard extends AsyncComponent<Props, State> {
   }
 
   get cardTitle() {
-    return t('Crash Free Sessions');
+    return this.props.field === SessionField.SESSIONS
+      ? t('Crash Free Sessions')
+      : t('Crash Free Users');
   }
 
   get cardHelp() {
-    return this.trend
-      ? t(
-          'The percentage of crash free sessions and how it has changed since the last period.'
-        )
-      : getSessionTermDescription(SessionTerm.STABILITY, null);
+    return getSessionTermDescription(
+      this.props.field === SessionField.SESSIONS
+        ? SessionTerm.CRASH_FREE_SESSIONS
+        : SessionTerm.CRASH_FREE_USERS,
+      null
+    );
   }
 
   get score() {
@@ -161,18 +166,20 @@ class ProjectStabilityScoreCard extends AsyncComponent<Props, State> {
   }
 
   calculateCrashFree(data?: SessionApiResponse | null) {
+    const {field} = this.props;
+
     if (!data) {
       return undefined;
     }
 
     const totalSessions = data.groups.reduce(
-      (acc, group) => acc + group.totals['sum(session)'],
+      (acc, group) => acc + group.totals[field],
       0
     );
 
     const crashedSessions = data.groups.find(
       group => group.by['session.status'] === 'crashed'
-    )?.totals['sum(session)'];
+    )?.totals[field];
 
     if (totalSessions === 0 || !defined(totalSessions) || !defined(crashedSessions)) {
       return undefined;