Browse Source

feat(ui): Show crash free sessions on Project Card (#26299)

Kelly Carino 3 years ago
parent
commit
ea0d8ae828

+ 1 - 0
static/app/components/scoreCard.tsx

@@ -92,4 +92,5 @@ const Trend = styled('div')<TrendProps>`
   overflow: hidden;
 `;
 
+export {HeaderTitle, StyledPanel, Score, ScoreWrapper, Trend};
 export default ScoreCard;

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

@@ -298,6 +298,11 @@ export type Project = {
   transactionStats?: TimeseriesValue[];
   latestRelease?: Release;
   options?: Record<string, boolean | string>;
+  sessionStats?: {
+    currentCrashFreeRate: number | null;
+    previousCrashFreeRate: number | null;
+    hasHealthData: boolean;
+  };
 } & AvatarProject;
 
 export type MinimalProject = Pick<Project, 'id' | 'slug' | 'platform'>;

+ 1 - 0
static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx

@@ -73,4 +73,5 @@ const StyledButtonBar = styled(ButtonBar)`
   grid-template-columns: minmax(auto, max-content) minmax(auto, max-content);
 `;
 
+export {StyledButtonBar};
 export default MissingReleasesButtons;

+ 10 - 4
static/app/views/projectsDashboard/deploys.tsx

@@ -15,9 +15,10 @@ const DEPLOY_COUNT = 2;
 
 type Props = {
   project: Project;
+  shorten?: boolean;
 };
 
-const Deploys = ({project}: Props) => {
+const Deploys = ({project, shorten}: Props) => {
   const flattenedDeploys = Object.entries(project.latestDeploys || {}).map(
     ([environment, value]): Pick<
       DeployType,
@@ -42,6 +43,7 @@ const Deploys = ({project}: Props) => {
           key={`${deploy.environment}-${deploy.version}`}
           deploy={deploy}
           project={project}
+          shorten={shorten}
         />
       ))}
     </DeployRows>
@@ -54,7 +56,7 @@ type DeployProps = Props & {
   deploy: Pick<DeployType, 'version' | 'dateFinished' | 'environment'>;
 };
 
-const Deploy = ({deploy, project}: DeployProps) => (
+const Deploy = ({deploy, project, shorten}: DeployProps) => (
   <Fragment>
     <IconReleases size="sm" />
     <TextOverflow>
@@ -70,7 +72,9 @@ const Deploy = ({deploy, project}: DeployProps) => (
     <DeployTime>
       {getDynamicText({
         fixed: '3 hours ago',
-        value: <TimeSince date={deploy.dateFinished} />,
+        value: (
+          <TimeSince date={deploy.dateFinished} shorten={shorten ? shorten : false} />
+        ),
       })}
     </DeployTime>
   </Fragment>
@@ -79,7 +83,7 @@ const Deploy = ({deploy, project}: DeployProps) => (
 const NoDeploys = () => (
   <GetStarted>
     <Button size="small" href="https://docs.sentry.io/product/releases/" external>
-      {t('Track deploys')}
+      {t('Track Deploys')}
     </Button>
   </GetStarted>
 );
@@ -115,3 +119,5 @@ const GetStarted = styled(DeployContainer)`
   align-items: center;
   justify-content: center;
 `;
+
+export {DeployRows, GetStarted, TextOverflow};

+ 193 - 3
static/app/views/projectsDashboard/projectCard.tsx

@@ -1,5 +1,6 @@
 import {Component, Fragment} from 'react';
 import styled from '@emotion/styled';
+import round from 'lodash/round';
 
 import {loadStatsForProject} from 'app/actionCreators/projects';
 import {Client} from 'app/api';
@@ -7,17 +8,34 @@ import IdBadge from 'app/components/idBadge';
 import Link from 'app/components/links/link';
 import BookmarkStar from 'app/components/projects/bookmarkStar';
 import QuestionTooltip from 'app/components/questionTooltip';
+import ScoreCard, {
+  HeaderTitle,
+  Score,
+  ScoreWrapper,
+  StyledPanel,
+  Trend,
+} from 'app/components/scoreCard';
+import {releaseHealth} from 'app/data/platformCategories';
+import {IconArrow} from 'app/icons';
 import {t} from 'app/locale';
 import ProjectsStatsStore from 'app/stores/projectsStatsStore';
 import space from 'app/styles/space';
 import {Organization, Project} from 'app/types';
+import {defined} from 'app/utils';
 import {callIfFunction} from 'app/utils/callIfFunction';
 import {formatAbbreviatedNumber} from 'app/utils/formatters';
 import withApi from 'app/utils/withApi';
 import withOrganization from 'app/utils/withOrganization';
+import MissingReleasesButtons, {
+  StyledButtonBar,
+} from 'app/views/projectDetail/missingFeatureButtons/missingReleasesButtons';
+import {
+  CRASH_FREE_DECIMAL_THRESHOLD,
+  displayCrashFreePercent,
+} from 'app/views/releases/utils';
 
 import Chart from './chart';
-import Deploys from './deploys';
+import Deploys, {DeployRows, GetStarted, TextOverflow} from './deploys';
 
 type Props = {
   api: Client;
@@ -36,6 +54,7 @@ class ProjectCard extends Component<Props> {
       projectId: project.id,
       query: {
         transactionStats: this.hasPerformance ? '1' : undefined,
+        sessionStats: '1',
       },
     });
   }
@@ -44,9 +63,69 @@ class ProjectCard extends Component<Props> {
     return this.props.organization.features.includes('performance-view');
   }
 
+  get crashFreeTrend() {
+    const {currentCrashFreeRate, previousCrashFreeRate} =
+      this.props.project.sessionStats || {};
+    if (!defined(currentCrashFreeRate) || !defined(previousCrashFreeRate)) {
+      return undefined;
+    }
+
+    return round(
+      currentCrashFreeRate - previousCrashFreeRate,
+      currentCrashFreeRate > CRASH_FREE_DECIMAL_THRESHOLD ? 3 : 0
+    );
+  }
+
+  renderMissingFeatureCard() {
+    const {organization, project} = this.props;
+    if (project.platform && releaseHealth.includes(project.platform)) {
+      return (
+        <ScoreCard
+          title={t('Crash Free Sessions')}
+          score={<MissingReleasesButtons organization={organization} health />}
+        />
+      );
+    }
+
+    return (
+      <ScoreCard
+        title={t('Crash Free Sessions')}
+        score={
+          <NotAvailable>
+            {t('Not Available')}
+            <QuestionTooltip
+              title={t('Release Health is not yet supported on this platform.')}
+              size="xs"
+            />
+          </NotAvailable>
+        }
+      />
+    );
+  }
+
+  renderTrend() {
+    const {currentCrashFreeRate} = this.props.project.sessionStats || {};
+
+    if (!defined(currentCrashFreeRate) || !defined(this.crashFreeTrend)) {
+      return null;
+    }
+
+    return (
+      <div>
+        {this.crashFreeTrend >= 0 ? (
+          <IconArrow direction="up" size="xs" />
+        ) : (
+          <IconArrow direction="down" size="xs" />
+        )}
+        {`${formatAbbreviatedNumber(Math.abs(this.crashFreeTrend))}\u0025`}
+      </div>
+    );
+  }
+
   render() {
     const {organization, project, hasProjectAccess} = this.props;
-    const {stats, slug, transactionStats} = project;
+    const {stats, slug, transactionStats, sessionStats} = project;
+    const {hasHealthData, currentCrashFreeRate} = sessionStats || {};
     const totalErrors = stats?.reduce((sum, [_, value]) => sum + value, 0) ?? 0;
     const totalTransactions =
       transactionStats?.reduce((sum, [_, value]) => sum + value, 0) ?? 0;
@@ -103,7 +182,34 @@ class ProjectCard extends Component<Props> {
                 transactionStats={transactionStats}
               />
             </ChartContainer>
-            <Deploys project={project} />
+            <FooterWrapper>
+              <ScoreCardWrapper>
+                {hasHealthData ? (
+                  <ScoreCard
+                    title={t('Crash Free Sessions')}
+                    score={
+                      defined(currentCrashFreeRate)
+                        ? displayCrashFreePercent(currentCrashFreeRate)
+                        : '\u2014'
+                    }
+                    trend={this.renderTrend()}
+                    trendStatus={
+                      this.crashFreeTrend
+                        ? this.crashFreeTrend > 0
+                          ? 'good'
+                          : 'bad'
+                        : undefined
+                    }
+                  />
+                ) : (
+                  this.renderMissingFeatureCard()
+                )}
+              </ScoreCardWrapper>
+              <DeploysWrapper>
+                <ReleaseTitle>{'Latest Deploys'}</ReleaseTitle>
+                <Deploys project={project} shorten />
+              </DeploysWrapper>
+            </FooterWrapper>
           </StyledProjectCard>
         ) : (
           <LoadingCard />
@@ -197,6 +303,81 @@ const StyledProjectCard = styled('div')`
   border: 1px solid ${p => p.theme.border};
   border-radius: ${p => p.theme.borderRadius};
   box-shadow: ${p => p.theme.dropShadowLight};
+  min-height: 326px;
+`;
+
+const FooterWrapper = styled('div')`
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  div {
+    border: none;
+    box-shadow: none;
+    font-size: ${p => p.theme.fontSizeMedium};
+    padding: 0;
+  }
+  ${StyledButtonBar} {
+    a {
+      background-color: ${p => p.theme.background};
+      border: 1px solid ${p => p.theme.border};
+      border-radius: ${p => p.theme.borderRadius};
+      color: ${p => p.theme.gray500};
+    }
+  }
+`;
+
+const ScoreCardWrapper = styled('div')`
+  margin: ${space(2)} 0 0 ${space(2)};
+  ${StyledPanel} {
+    min-height: auto;
+  }
+  ${HeaderTitle} {
+    color: ${p => p.theme.gray300};
+    font-weight: 600;
+  }
+  ${ScoreWrapper} {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+  ${Score} {
+    font-size: 28px;
+  }
+  ${Trend} {
+    margin-left: 0;
+  }
+`;
+
+const DeploysWrapper = styled('div')`
+  margin-top: ${space(2)};
+  ${GetStarted} {
+    display: block;
+    height: 100%;
+  }
+  ${TextOverflow} {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-column-gap: ${space(1)};
+    div {
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+    a {
+      display: grid;
+    }
+  }
+  ${DeployRows} {
+    grid-template-columns: 2fr auto;
+    margin-right: ${space(2)};
+    height: auto;
+    svg {
+      display: none;
+    }
+  }
+`;
+
+const ReleaseTitle = styled('span')`
+  color: ${p => p.theme.gray300};
+  font-weight: 600;
 `;
 
 const LoadingCard = styled('div')`
@@ -243,5 +424,14 @@ const TransactionsLink = styled(Link)`
   }
 `;
 
+const NotAvailable = styled('div')`
+  font-size: ${p => p.theme.fontSizeMedium};
+  font-weight: normal;
+  display: grid;
+  grid-template-columns: auto auto;
+  grid-gap: ${space(0.5)};
+  align-items: center;
+`;
+
 export {ProjectCard};
 export default withOrganization(withApi(ProjectCardContainer));

+ 1 - 5
static/app/views/projectsDashboard/teamSection.tsx

@@ -50,12 +50,8 @@ const ProjectCards = styled('div')`
     grid-template-columns: repeat(2, minmax(100px, 1fr));
   }
 
-  @media (min-width: ${p => p.theme.breakpoints[2]}) {
-    grid-template-columns: repeat(3, minmax(100px, 1fr));
-  }
-
   @media (min-width: ${p => p.theme.breakpoints[3]}) {
-    grid-template-columns: repeat(4, minmax(100px, 1fr));
+    grid-template-columns: repeat(3, minmax(100px, 1fr));
   }
 `;
 

+ 4 - 2
static/app/views/releases/utils/index.tsx

@@ -7,13 +7,15 @@ import {IssueSortOptions} from 'app/views/issueList/utils';
 
 import {DisplayOption} from '../list/utils';
 
+export const CRASH_FREE_DECIMAL_THRESHOLD = 95;
+
 export const roundDuration = (seconds: number) => {
   return round(seconds, seconds > 60 ? 0 : 3);
 };
 
 export const getCrashFreePercent = (
   percent: number,
-  decimalThreshold = 95,
+  decimalThreshold = CRASH_FREE_DECIMAL_THRESHOLD,
   decimalPlaces = 3
 ): number => {
   return round(percent, percent > decimalThreshold ? decimalPlaces : 0);
@@ -21,7 +23,7 @@ export const getCrashFreePercent = (
 
 export const displayCrashFreePercent = (
   percent: number,
-  decimalThreshold = 95,
+  decimalThreshold = CRASH_FREE_DECIMAL_THRESHOLD,
   decimalPlaces = 3
 ): string => {
   if (isNaN(percent)) {