Browse Source

feat(ui): More prominent adoption and stability on release detail page (#23526)

Matej Minar 4 years ago
parent
commit
063af51cac

+ 4 - 0
docs-ui/components/notAvailable.stories.js

@@ -23,6 +23,10 @@ export const Default = () => (
         </div>
         </div>
       </PanelTable>
       </PanelTable>
     </div>
     </div>
+    <div className="section">
+      <h3>With Tooltip</h3>
+      <NotAvailable tooltip="Reason why this is not available" />
+    </div>
   </div>
   </div>
 );
 );
 
 

+ 15 - 2
src/sentry/static/sentry/app/components/notAvailable.tsx

@@ -1,8 +1,21 @@
 import React from 'react';
 import React from 'react';
 import styled from '@emotion/styled';
 import styled from '@emotion/styled';
 
 
-function NotAvailable() {
-  return <Wrapper>{'\u2014'}</Wrapper>;
+import Tooltip from 'app/components/tooltip';
+import {defined} from 'app/utils';
+
+type Props = {
+  tooltip?: React.ReactNode;
+};
+
+function NotAvailable({tooltip}: Props) {
+  return (
+    <Wrapper>
+      <Tooltip title={tooltip} disabled={!defined(tooltip)}>
+        {'\u2014'}
+      </Tooltip>
+    </Wrapper>
+  );
 }
 }
 
 
 const Wrapper = styled('div')`
 const Wrapper = styled('div')`

+ 9 - 0
src/sentry/static/sentry/app/constants/notAvailableMessages.tsx

@@ -0,0 +1,9 @@
+import {t} from 'app/locale';
+
+const NOT_AVAILABLE_MESSAGES = {
+  performance: t('This view is only available with Performance Monitoring.'),
+  discover: t('This view is only available with Discover.'),
+  releaseHealth: t('This view is only available with Release Health.'),
+};
+
+export default NOT_AVAILABLE_MESSAGES;

+ 3 - 4
src/sentry/static/sentry/app/views/projectDetail/projectCharts.tsx

@@ -14,6 +14,7 @@ import {
 } from 'app/components/charts/styles';
 } from 'app/components/charts/styles';
 import {Panel} from 'app/components/panels';
 import {Panel} from 'app/components/panels';
 import CHART_PALETTE from 'app/constants/chartPalette';
 import CHART_PALETTE from 'app/constants/chartPalette';
+import NOT_AVAILABLE_MESSAGES from 'app/constants/notAvailableMessages';
 import {t} from 'app/locale';
 import {t} from 'app/locale';
 import {Organization, SelectValue} from 'app/types';
 import {Organization, SelectValue} from 'app/types';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
@@ -85,10 +86,8 @@ class ProjectCharts extends React.Component<Props, State> {
     const {organization} = this.props;
     const {organization} = this.props;
     const hasPerformance = organization.features.includes('performance-view');
     const hasPerformance = organization.features.includes('performance-view');
     const hasDiscover = organization.features.includes('discover-basic');
     const hasDiscover = organization.features.includes('discover-basic');
-    const noPerformanceTooltip = t(
-      'This view is only available with Performance Monitoring.'
-    );
-    const noDiscoverTooltip = t('This view is only available with Discover.');
+    const noPerformanceTooltip = NOT_AVAILABLE_MESSAGES.performance;
+    const noDiscoverTooltip = NOT_AVAILABLE_MESSAGES.discover;
 
 
     return [
     return [
       {
       {

+ 4 - 5
src/sentry/static/sentry/app/views/releases/detail/overview/chart/releaseChartControls.tsx

@@ -9,6 +9,7 @@ import {
   SectionValue,
   SectionValue,
 } from 'app/components/charts/styles';
 } from 'app/components/charts/styles';
 import QuestionTooltip from 'app/components/questionTooltip';
 import QuestionTooltip from 'app/components/questionTooltip';
+import NOT_AVAILABLE_MESSAGES from 'app/constants/notAvailableMessages';
 import {t} from 'app/locale';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
 import space from 'app/styles/space';
 import {Organization, SelectValue} from 'app/types';
 import {Organization, SelectValue} from 'app/types';
@@ -68,13 +69,11 @@ const ReleaseChartControls = ({
   onVitalTypeChange,
   onVitalTypeChange,
 }: Props) => {
 }: Props) => {
   const noHealthDataTooltip = !hasHealthData
   const noHealthDataTooltip = !hasHealthData
-    ? t('This view is only available with release health data.')
-    : undefined;
-  const noDiscoverTooltip = !hasDiscover
-    ? t('This view is only available with Discover.')
+    ? NOT_AVAILABLE_MESSAGES.releaseHealth
     : undefined;
     : undefined;
+  const noDiscoverTooltip = !hasDiscover ? NOT_AVAILABLE_MESSAGES.discover : undefined;
   const noPerformanceTooltip = !hasPerformance
   const noPerformanceTooltip = !hasPerformance
-    ? t('This view is only available with Performance Monitoring.')
+    ? NOT_AVAILABLE_MESSAGES.performance
     : undefined;
     : undefined;
   const yAxisOptions: SelectValue<YAxis>[] = [
   const yAxisOptions: SelectValue<YAxis>[] = [
     {
     {

+ 158 - 85
src/sentry/static/sentry/app/views/releases/detail/overview/releaseStats.tsx

@@ -7,12 +7,16 @@ import {SectionHeading} from 'app/components/charts/styles';
 import Count from 'app/components/count';
 import Count from 'app/components/count';
 import DeployBadge from 'app/components/deployBadge';
 import DeployBadge from 'app/components/deployBadge';
 import GlobalSelectionLink from 'app/components/globalSelectionLink';
 import GlobalSelectionLink from 'app/components/globalSelectionLink';
+import NotAvailable from 'app/components/notAvailable';
+import ProgressBar from 'app/components/progressBar';
 import QuestionTooltip from 'app/components/questionTooltip';
 import QuestionTooltip from 'app/components/questionTooltip';
 import TimeSince from 'app/components/timeSince';
 import TimeSince from 'app/components/timeSince';
 import Tooltip from 'app/components/tooltip';
 import Tooltip from 'app/components/tooltip';
+import NOT_AVAILABLE_MESSAGES from 'app/constants/notAvailableMessages';
 import {t} from 'app/locale';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
 import space from 'app/styles/space';
 import {GlobalSelection, Organization, Release, ReleaseProject} from 'app/types';
 import {GlobalSelection, Organization, Release, ReleaseProject} from 'app/types';
+import {defined} from 'app/utils';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import {getAggregateAlias} from 'app/utils/discover/fields';
 import {getAggregateAlias} from 'app/utils/discover/fields';
 import {getTermHelp, PERFORMANCE_TERM} from 'app/views/performance/data';
 import {getTermHelp, PERFORMANCE_TERM} from 'app/views/performance/data';
@@ -22,6 +26,8 @@ import {
   sessionTerm,
   sessionTerm,
 } from 'app/views/releases/utils/sessionTerm';
 } from 'app/views/releases/utils/sessionTerm';
 
 
+import AdoptionTooltip from '../../list/adoptionTooltip';
+import CrashFree from '../../list/crashFree';
 import {getReleaseNewIssuesUrl, getReleaseUnhandledIssuesUrl} from '../../utils';
 import {getReleaseNewIssuesUrl, getReleaseUnhandledIssuesUrl} from '../../utils';
 import {getReleaseEventView} from '../utils';
 import {getReleaseEventView} from '../utils';
 
 
@@ -35,19 +41,28 @@ type Props = {
 
 
 function ReleaseStats({organization, release, project, location, selection}: Props) {
 function ReleaseStats({organization, release, project, location, selection}: Props) {
   const {lastDeploy, dateCreated, newGroups, version} = release;
   const {lastDeploy, dateCreated, newGroups, version} = release;
-  const {hasHealthData} = project;
-  const {sessionsCrashed} = project.healthData;
+  const {hasHealthData, healthData} = project;
+  const {
+    sessionsCrashed,
+    adoption,
+    crashFreeUsers,
+    crashFreeSessions,
+    totalUsers,
+    totalUsers24h,
+    totalSessions,
+    totalSessions24h,
+  } = healthData;
 
 
   return (
   return (
     <Container>
     <Container>
-      <DateStatWrapper>
+      <div>
         <SectionHeading>
         <SectionHeading>
           {lastDeploy?.dateFinished ? t('Date Deployed') : t('Date Created')}
           {lastDeploy?.dateFinished ? t('Date Deployed') : t('Date Created')}
         </SectionHeading>
         </SectionHeading>
         <div>
         <div>
           <TimeSince date={lastDeploy?.dateFinished ?? dateCreated} />
           <TimeSince date={lastDeploy?.dateFinished ?? dateCreated} />
         </div>
         </div>
-      </DateStatWrapper>
+      </div>
 
 
       <div>
       <div>
         <SectionHeading>{t('Last Deploy')}</SectionHeading>
         <SectionHeading>{t('Last Deploy')}</SectionHeading>
@@ -60,107 +75,159 @@ function ReleaseStats({organization, release, project, location, selection}: Pro
               projectId={project.id}
               projectId={project.id}
             />
             />
           ) : (
           ) : (
-            '\u2014'
+            <NotAvailable />
           )}
           )}
         </div>
         </div>
       </div>
       </div>
 
 
       <div>
       <div>
-        <SectionHeading>{t('New Issues')}</SectionHeading>
+        <SectionHeading>{t('Crash Free Users')}</SectionHeading>
         <div>
         <div>
-          <Tooltip title={t('Open in Issues')}>
-            <GlobalSelectionLink
-              to={getReleaseNewIssuesUrl(organization.slug, project.id, version)}
-            >
-              <Count value={newGroups} />
-            </GlobalSelectionLink>
-          </Tooltip>
+          {defined(crashFreeUsers) ? (
+            <CrashFree percent={crashFreeUsers} iconSize="md" />
+          ) : (
+            <NotAvailable tooltip={NOT_AVAILABLE_MESSAGES.releaseHealth} />
+          )}
         </div>
         </div>
       </div>
       </div>
 
 
       <div>
       <div>
-        <SectionHeading>
-          {t('Apdex')}
-          <QuestionTooltip
-            position="top"
-            title={getTermHelp(organization, PERFORMANCE_TERM.APDEX)}
-            size="sm"
-          />
-        </SectionHeading>
+        <SectionHeading>{t('Crash Free Sessions')}</SectionHeading>
         <div>
         <div>
-          <Feature features={['performance-view']}>
-            {hasFeature =>
-              hasFeature ? (
-                <DiscoverQuery
-                  eventView={getReleaseEventView(
-                    selection,
-                    release?.version,
-                    organization
-                  )}
-                  location={location}
-                  orgSlug={organization.slug}
-                >
-                  {({isLoading, error, tableData}) => {
-                    if (isLoading || error || !tableData || tableData.data.length === 0) {
-                      return '\u2014';
-                    }
-                    return (
-                      <GlobalSelectionLink
-                        to={{
-                          pathname: `/organizations/${organization.slug}/performance/`,
-                          query: {
-                            query: `release:${release?.version}`,
-                          },
-                        }}
-                      >
-                        <Count
-                          value={
-                            tableData.data[0][
-                              getAggregateAlias(`apdex(${organization.apdexThreshold})`)
-                            ]
-                          }
-                        />
-                      </GlobalSelectionLink>
-                    );
-                  }}
-                </DiscoverQuery>
-              ) : (
-                <Tooltip
-                  title={t('This view is only available with Performance Monitoring.')}
-                >
-                  {'\u2014'}
-                </Tooltip>
-              )
-            }
-          </Feature>
+          {defined(crashFreeSessions) ? (
+            <CrashFree percent={crashFreeSessions} iconSize="md" />
+          ) : (
+            <NotAvailable tooltip={NOT_AVAILABLE_MESSAGES.releaseHealth} />
+          )}
         </div>
         </div>
       </div>
       </div>
 
 
-      <div>
-        <SectionHeading>
-          {sessionTerm.crashes}
-          <QuestionTooltip
-            position="top"
-            title={getSessionTermDescription(SessionTerm.CRASHES, project.platform)}
-            size="sm"
-          />
-        </SectionHeading>
+      <AdoptionWrapper>
+        <SectionHeading>{t('User Adoption')}</SectionHeading>
+        {defined(adoption) ? (
+          <Tooltip
+            containerDisplayMode="block"
+            title={
+              <AdoptionTooltip
+                totalUsers={totalUsers}
+                totalSessions={totalSessions}
+                totalUsers24h={totalUsers24h}
+                totalSessions24h={totalSessions24h}
+              />
+            }
+          >
+            <ProgressBar value={Math.ceil(adoption)} />
+          </Tooltip>
+        ) : (
+          <NotAvailable tooltip={NOT_AVAILABLE_MESSAGES.releaseHealth} />
+        )}
+      </AdoptionWrapper>
+
+      <LinkedStatsWrapper>
         <div>
         <div>
-          {hasHealthData ? (
+          <SectionHeading>{t('New Issues')}</SectionHeading>
+          <div>
             <Tooltip title={t('Open in Issues')}>
             <Tooltip title={t('Open in Issues')}>
               <GlobalSelectionLink
               <GlobalSelectionLink
-                to={getReleaseUnhandledIssuesUrl(organization.slug, project.id, version)}
+                to={getReleaseNewIssuesUrl(organization.slug, project.id, version)}
               >
               >
-                <Count value={sessionsCrashed} />
+                <Count value={newGroups} />
               </GlobalSelectionLink>
               </GlobalSelectionLink>
             </Tooltip>
             </Tooltip>
-          ) : (
-            <Tooltip title={t('This view is only available with release health data.')}>
-              {'\u2014'}
-            </Tooltip>
-          )}
+          </div>
         </div>
         </div>
-      </div>
+
+        <div>
+          <SectionHeading>
+            {sessionTerm.crashes}
+            <QuestionTooltip
+              position="top"
+              title={getSessionTermDescription(SessionTerm.CRASHES, project.platform)}
+              size="sm"
+            />
+          </SectionHeading>
+          <div>
+            {hasHealthData ? (
+              <Tooltip title={t('Open in Issues')}>
+                <GlobalSelectionLink
+                  to={getReleaseUnhandledIssuesUrl(
+                    organization.slug,
+                    project.id,
+                    version
+                  )}
+                >
+                  <Count value={sessionsCrashed} />
+                </GlobalSelectionLink>
+              </Tooltip>
+            ) : (
+              <NotAvailable tooltip={NOT_AVAILABLE_MESSAGES.releaseHealth} />
+            )}
+          </div>
+        </div>
+
+        <div>
+          <SectionHeading>
+            {t('Apdex')}
+            <QuestionTooltip
+              position="top"
+              title={getTermHelp(organization, PERFORMANCE_TERM.APDEX)}
+              size="sm"
+            />
+          </SectionHeading>
+          <div>
+            <Feature features={['performance-view']}>
+              {hasFeature =>
+                hasFeature ? (
+                  <DiscoverQuery
+                    eventView={getReleaseEventView(
+                      selection,
+                      release?.version,
+                      organization
+                    )}
+                    location={location}
+                    orgSlug={organization.slug}
+                  >
+                    {({isLoading, error, tableData}) => {
+                      if (
+                        isLoading ||
+                        error ||
+                        !tableData ||
+                        tableData.data.length === 0
+                      ) {
+                        return <NotAvailable />;
+                      }
+                      return (
+                        <GlobalSelectionLink
+                          to={{
+                            pathname: `/organizations/${organization.slug}/performance/`,
+                            query: {
+                              query: `release:${release?.version}`,
+                            },
+                          }}
+                        >
+                          <Tooltip title={t('Open in Performance')}>
+                            <Count
+                              value={
+                                tableData.data[0][
+                                  getAggregateAlias(
+                                    `apdex(${organization.apdexThreshold})`
+                                  )
+                                ]
+                              }
+                            />
+                          </Tooltip>
+                        </GlobalSelectionLink>
+                      );
+                    }}
+                  </DiscoverQuery>
+                ) : (
+                  <NotAvailable tooltip={NOT_AVAILABLE_MESSAGES.performance} />
+                )
+              }
+            </Feature>
+          </div>
+        </div>
+      </LinkedStatsWrapper>
     </Container>
     </Container>
   );
   );
 }
 }
@@ -172,7 +239,13 @@ const Container = styled('div')`
   margin-bottom: ${space(3)};
   margin-bottom: ${space(3)};
 `;
 `;
 
 
-const DateStatWrapper = styled('div')`
+const LinkedStatsWrapper = styled('div')`
+  grid-column: 1/3;
+  display: flex;
+  justify-content: space-between;
+`;
+
+const AdoptionWrapper = styled('div')`
   grid-column: 1/3;
   grid-column: 1/3;
 `;
 `;
 
 

+ 8 - 6
src/sentry/static/sentry/app/views/releases/list/crashFree.tsx

@@ -4,32 +4,34 @@ import styled from '@emotion/styled';
 import {IconCheckmark, IconFire, IconWarning} from 'app/icons';
 import {IconCheckmark, IconFire, IconWarning} from 'app/icons';
 import overflowEllipsis from 'app/styles/overflowEllipsis';
 import overflowEllipsis from 'app/styles/overflowEllipsis';
 import space from 'app/styles/space';
 import space from 'app/styles/space';
+import {IconSize} from 'app/utils/theme';
 
 
 import {displayCrashFreePercent} from '../utils';
 import {displayCrashFreePercent} from '../utils';
 
 
 const CRASH_FREE_DANGER_THRESHOLD = 98;
 const CRASH_FREE_DANGER_THRESHOLD = 98;
 const CRASH_FREE_WARNING_THRESHOLD = 99.5;
 const CRASH_FREE_WARNING_THRESHOLD = 99.5;
 
 
-const getIcon = (percent: number) => {
+const getIcon = (percent: number, iconSize: IconSize) => {
   if (percent < CRASH_FREE_DANGER_THRESHOLD) {
   if (percent < CRASH_FREE_DANGER_THRESHOLD) {
-    return <IconFire color="red300" />;
+    return <IconFire color="red300" size={iconSize} />;
   }
   }
 
 
   if (percent < CRASH_FREE_WARNING_THRESHOLD) {
   if (percent < CRASH_FREE_WARNING_THRESHOLD) {
-    return <IconWarning color="yellow300" />;
+    return <IconWarning color="yellow300" size={iconSize} />;
   }
   }
 
 
-  return <IconCheckmark isCircled color="green300" />;
+  return <IconCheckmark isCircled color="green300" size={iconSize} />;
 };
 };
 
 
 type Props = {
 type Props = {
   percent: number;
   percent: number;
+  iconSize?: IconSize;
 };
 };
 
 
-const CrashFree = ({percent}: Props) => {
+const CrashFree = ({percent, iconSize = 'sm'}: Props) => {
   return (
   return (
     <Wrapper>
     <Wrapper>
-      {getIcon(percent)}
+      {getIcon(percent, iconSize)}
       <CrashFreePercent>{displayCrashFreePercent(percent)}</CrashFreePercent>
       <CrashFreePercent>{displayCrashFreePercent(percent)}</CrashFreePercent>
     </Wrapper>
     </Wrapper>
   );
   );

+ 6 - 0
tests/js/spec/components/notAvailable.spec.tsx

@@ -9,4 +9,10 @@ describe('NotAvailable', function () {
     const wrapper = mountWithTheme(<NotAvailable />);
     const wrapper = mountWithTheme(<NotAvailable />);
     expect(wrapper.text()).toEqual('\u2014');
     expect(wrapper.text()).toEqual('\u2014');
   });
   });
+
+  it('renders with tooltip', function () {
+    const wrapper = mountWithTheme(<NotAvailable tooltip="Tooltip text" />);
+    expect(wrapper.text()).toEqual('\u2014');
+    expect(wrapper.find('Tooltip').prop('title')).toBe('Tooltip text');
+  });
 });
 });