Browse Source

feat(toolbar): add crash free rate to release panel (#75021)

- closes https://github.com/getsentry/sentry/issues/74566
- relates to https://github.com/getsentry/sentry/issues/74565



https://github.com/user-attachments/assets/3de2b293-9491-43cb-8272-40fb064eee52
Michelle Zhang 7 months ago
parent
commit
99c37ee5bf

+ 1 - 1
static/app/components/devtoolbar/components/alerts/alertsPanel.tsx

@@ -43,7 +43,7 @@ export default function AlertsPanel() {
     <PanelLayout title="Alerts">
       <div css={[smallCss, panelSectionCss, panelInsetContentCss]}>
         <span css={[resetFlexRowCss, {gap: 'var(--space50)'}]}>
-          Active Alerts in{' '}
+          Active alerts in{' '}
           <SentryAppLink
             to={{url: `/projects/${projectSlug}/`}}
             onClick={() => {

+ 3 - 1
static/app/components/devtoolbar/components/featureFlags/featureFlagsPanel.tsx

@@ -26,7 +26,9 @@ export default function FeatureFlagsPanel() {
   return (
     <PanelLayout title="Feature Flags">
       <div css={[smallCss, panelSectionCss, panelInsetContentCss]}>
-        <span>Flags enabled for {organizationSlug}</span>
+        <span>
+          Flags enabled for <code>{organizationSlug}</code>
+        </span>
       </div>
       <PanelTable
         headers={[

+ 183 - 57
static/app/components/devtoolbar/components/releases/releasesPanel.tsx

@@ -1,22 +1,35 @@
 import {Fragment} from 'react';
 import {css} from '@emotion/react';
 
+import useReleaseSessions from 'sentry/components/devtoolbar/components/releases/useReleaseSessions';
 import useToolbarRelease from 'sentry/components/devtoolbar/components/releases/useToolbarRelease';
 import SentryAppLink from 'sentry/components/devtoolbar/components/sentryAppLink';
 import {listItemPlaceholderWrapperCss} from 'sentry/components/devtoolbar/styles/listItem';
+import {
+  infoHeaderCss,
+  subtextCss,
+} from 'sentry/components/devtoolbar/styles/releasesPanel';
 import {
   resetFlexColumnCss,
   resetFlexRowCss,
 } from 'sentry/components/devtoolbar/styles/reset';
+import type {ApiResult} from 'sentry/components/devtoolbar/types';
+import EmptyStateWarning from 'sentry/components/emptyStateWarning';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import PanelItem from 'sentry/components/panels/panelItem';
 import Placeholder from 'sentry/components/placeholder';
-import TextOverflow from 'sentry/components/textOverflow';
 import TimeSince from 'sentry/components/timeSince';
+import {IconArrow} from 'sentry/icons/iconArrow';
+import type {SessionApiResponse} from 'sentry/types/organization';
 import type {PlatformKey} from 'sentry/types/project';
 import type {Release} from 'sentry/types/release';
+import {defined} from 'sentry/utils';
 import {formatVersion} from 'sentry/utils/versions/formatVersion';
 import {
-  PackageName,
+  Change,
+  type ReleaseComparisonRow,
+} from 'sentry/views/releases/detail/overview/releaseComparisonChart';
+import {
   ReleaseInfoHeader,
   ReleaseInfoSubheader,
   VersionWrapper,
@@ -28,10 +41,39 @@ import {panelInsetContentCss, panelSectionCss} from '../../styles/panel';
 import {smallCss} from '../../styles/typography';
 import PanelLayout from '../panelLayout';
 
-function ReleaseHeader({release, orgSlug}: {orgSlug: string; release: Release}) {
+const estimateSize = 81;
+const placeholderHeight = `${estimateSize - 8}px`; // The real height of the items, minus the padding-block value
+
+function getCrashFreeRate(data: ApiResult<SessionApiResponse>): number {
+  // if `crash_free_rate(session)` is undefined
+  // (sometimes the case for brand new releases),
+  // assume it is 100%.
+  // round to 2 decimal points
+  return parseFloat(
+    ((data?.json.groups[0].totals['crash_free_rate(session)'] ?? 1) * 100).toFixed(2)
+  );
+}
+
+function getDiff(
+  diff: string,
+  diffColor: ReleaseComparisonRow['diffColor'],
+  diffDirection: 'up' | 'down' | undefined
+) {
+  return (
+    <Change
+      color={defined(diffColor) ? diffColor : 'black'}
+      css={[resetFlexRowCss, {alignItems: 'center', gap: 'var(--space25)'}]}
+    >
+      {diff}
+      {defined(diffDirection) ? <IconArrow direction={diffDirection} size="xs" /> : null}
+    </Change>
+  );
+}
+
+function ReleaseSummary({orgSlug, release}: {orgSlug: string; release: Release}) {
   return (
-    <div style={{padding: '12px'}}>
-      <ReleaseInfoHeader>
+    <PanelItem css={{width: '100%', alignItems: 'flex-start'}}>
+      <ReleaseInfoHeader css={infoHeaderCss}>
         <SentryAppLink
           to={{
             url: `/organizations/${orgSlug}/releases/${encodeURIComponent(release.version)}/`,
@@ -45,36 +87,150 @@ function ReleaseHeader({release, orgSlug}: {orgSlug: string; release: Release})
         )}
       </ReleaseInfoHeader>
       <ReleaseInfoSubheader
-        style={{display: 'flex', flexDirection: 'column', alignItems: 'flex-start'}}
+        css={[resetFlexColumnCss, subtextCss, {alignItems: 'flex-start'}]}
       >
-        {release.versionInfo?.package && (
-          <PackageName>
-            <TextOverflow ellipsisDirection="left">
-              {release.versionInfo.package}
-            </TextOverflow>
-          </PackageName>
-        )}
-        <span style={{display: 'flex', flexDirection: 'row', gap: '3px'}}>
+        <span css={[resetFlexRowCss, {gap: 'var(--space25)'}]}>
           <TimeSince date={release.lastDeploy?.dateFinished || release.dateCreated} />
           {release.lastDeploy?.dateFinished &&
             ` \u007C ${release.lastDeploy.environment}`}
         </span>
       </ReleaseInfoSubheader>
-    </div>
+    </PanelItem>
+  );
+}
+
+function CrashFreeRate({
+  prevReleaseVersion,
+  currReleaseVersion,
+}: {
+  currReleaseVersion: string;
+  prevReleaseVersion: string | undefined;
+}) {
+  const {
+    data: currSessionData,
+    isLoading: isCurrLoading,
+    isError: isCurrError,
+  } = useReleaseSessions({
+    releaseVersion: currReleaseVersion,
+  });
+  const {
+    data: prevSessionData,
+    isLoading: isPrevLoading,
+    isError: isPrevError,
+  } = useReleaseSessions({
+    releaseVersion: prevReleaseVersion,
+  });
+
+  if (isCurrError || isPrevError) {
+    return null;
+  }
+
+  if (isCurrLoading || isPrevLoading) {
+    return (
+      <PanelItem css={{width: '100%', padding: 'var(--space150)'}}>
+        <Placeholder
+          height={placeholderHeight}
+          css={[
+            resetFlexColumnCss,
+            panelSectionCss,
+            panelInsetContentCss,
+            listItemPlaceholderWrapperCss,
+          ]}
+        />
+      </PanelItem>
+    );
+  }
+
+  const currCrashFreeRate = getCrashFreeRate(currSessionData);
+  const prevCrashFreeRate = getCrashFreeRate(prevSessionData);
+  const diff = currCrashFreeRate - prevCrashFreeRate;
+  const sign = Math.sign(diff);
+
+  return (
+    <PanelItem>
+      <div css={infoHeaderCss}>Crash free session rate</div>
+      <ReleaseInfoSubheader css={subtextCss}>
+        <span css={[resetFlexRowCss, {gap: 'var(--space200)'}]}>
+          <span css={resetFlexColumnCss}>
+            <span>This release</span> {currCrashFreeRate}%
+          </span>
+          <span css={resetFlexColumnCss}>
+            <span>Prev release</span> {prevCrashFreeRate}%
+          </span>
+          <span css={resetFlexColumnCss}>
+            Change
+            {getDiff(
+              Math.abs(diff).toFixed(2) + '%',
+              sign === 0 ? 'black' : sign === 1 ? 'green400' : 'red400',
+              sign === 0 ? undefined : sign === 1 ? 'up' : 'down'
+            )}
+          </span>
+        </span>
+      </ReleaseInfoSubheader>
+    </PanelItem>
   );
 }
 
 export default function ReleasesPanel() {
-  const {data, isLoading, isError} = useToolbarRelease();
+  const {
+    data: releaseData,
+    isLoading: isReleaseDataLoading,
+    isError: isReleaseDataError,
+  } = useToolbarRelease();
+
   const {organizationSlug, projectSlug, projectId, projectPlatform, trackAnalytics} =
     useConfiguration();
 
-  const estimateSize = 515;
-  const placeholderHeight = `${estimateSize - 8}px`; // The real height of the items, minus the padding-block value
+  if (isReleaseDataError) {
+    return <EmptyStateWarning small>No data to show</EmptyStateWarning>;
+  }
 
   return (
     <PanelLayout title="Latest Release">
-      {isLoading || isError ? (
+      <span
+        css={[
+          smallCss,
+          panelSectionCss,
+          panelInsetContentCss,
+          resetFlexRowCss,
+          {gap: 'var(--space50)', flexGrow: 0},
+        ]}
+      >
+        Latest release for{' '}
+        <SentryAppLink
+          to={{
+            url: `/releases/`,
+            query: {project: projectId},
+          }}
+          onClick={() => {
+            trackAnalytics?.({
+              eventKey: `devtoolbar.releases-list.header.click`,
+              eventName: `devtoolbar: Click releases-list header`,
+            });
+          }}
+        >
+          <div
+            css={[
+              resetFlexRowCss,
+              {display: 'inline-flex', gap: 'var(--space50)', alignItems: 'center'},
+            ]}
+          >
+            <ProjectBadge
+              css={css({'&& img': {boxShadow: 'none'}})}
+              project={{
+                slug: projectSlug,
+                id: projectId,
+                platform: projectPlatform as PlatformKey,
+              }}
+              avatarSize={16}
+              hideName
+              avatarProps={{hasTooltip: false}}
+            />
+            {projectSlug}
+          </div>
+        </SentryAppLink>
+      </span>
+      {isReleaseDataLoading ? (
         <div
           css={[
             resetFlexColumnCss,
@@ -84,48 +240,18 @@ export default function ReleasesPanel() {
           ]}
         >
           <Placeholder height={placeholderHeight} />
+          <Placeholder height={placeholderHeight} />
         </div>
       ) : (
         <Fragment>
-          <div css={[smallCss, panelSectionCss, panelInsetContentCss]}>
-            <span css={[resetFlexRowCss, {gap: 'var(--space50)'}]}>
-              Latest release for{' '}
-              <SentryAppLink
-                to={{
-                  url: `/releases/`,
-                  query: {project: projectId},
-                }}
-                onClick={() => {
-                  trackAnalytics?.({
-                    eventKey: `devtoolbar.releases-list.header.click`,
-                    eventName: `devtoolbar: Click releases-list header`,
-                  });
-                }}
-              >
-                <div
-                  css={[
-                    resetFlexRowCss,
-                    {display: 'inline-flex', gap: 'var(--space50)', alignItems: 'center'},
-                  ]}
-                >
-                  <ProjectBadge
-                    css={css({'&& img': {boxShadow: 'none'}})}
-                    project={{
-                      slug: projectSlug,
-                      id: projectId,
-                      platform: projectPlatform as PlatformKey,
-                    }}
-                    avatarSize={16}
-                    hideName
-                    avatarProps={{hasTooltip: false}}
-                  />
-                  {projectSlug}
-                </div>
-              </SentryAppLink>
-            </span>
-          </div>
           <div style={{alignItems: 'start'}}>
-            <ReleaseHeader release={data.json[0]} orgSlug={organizationSlug} />
+            <ReleaseSummary release={releaseData.json[0]} orgSlug={organizationSlug} />
+            <CrashFreeRate
+              currReleaseVersion={releaseData.json[0].version}
+              prevReleaseVersion={
+                releaseData.json.length > 1 ? releaseData.json[1].version : undefined
+              }
+            />
           </div>
         </Fragment>
       )}

+ 34 - 0
static/app/components/devtoolbar/components/releases/useReleaseSessions.tsx

@@ -0,0 +1,34 @@
+import {useMemo} from 'react';
+
+import useConfiguration from 'sentry/components/devtoolbar/hooks/useConfiguration';
+import useFetchApiData from 'sentry/components/devtoolbar/hooks/useFetchApiData';
+import type {ApiEndpointQueryKey} from 'sentry/components/devtoolbar/types';
+import type {SessionApiResponse} from 'sentry/types/organization';
+
+export default function useReleaseSessions({
+  releaseVersion,
+}: {
+  releaseVersion: string | undefined;
+}) {
+  const {organizationSlug, projectId} = useConfiguration();
+  return useFetchApiData<SessionApiResponse>({
+    queryKey: useMemo(
+      (): ApiEndpointQueryKey => [
+        'io.sentry.toolbar',
+        `/organizations/${organizationSlug}/sessions/`,
+        {
+          query: {
+            queryReferrer: 'devtoolbar',
+            project: [projectId],
+            field: 'crash_free_rate(session)',
+            interval: '10m',
+            statsPeriod: '24h',
+            query: `release:${releaseVersion}`,
+          },
+        },
+      ],
+      [organizationSlug, projectId, releaseVersion]
+    ),
+    cacheTime: 5000,
+  });
+}

+ 1 - 1
static/app/components/devtoolbar/components/releases/useToolbarRelease.tsx

@@ -23,6 +23,6 @@ export default function useToolbarRelease() {
       ],
       [organizationSlug, projectSlug]
     ),
-    cacheTime: Infinity,
+    cacheTime: 5000,
   });
 }

+ 10 - 0
static/app/components/devtoolbar/styles/releasesPanel.ts

@@ -0,0 +1,10 @@
+import {css} from '@emotion/react';
+
+export const infoHeaderCss = css`
+  font-size: 16px;
+  font-weight: bold;
+`;
+
+export const subtextCss = css`
+  font-size: 14px;
+`;

+ 6 - 8
static/app/views/releases/detail/overview/releaseComparisonChart/index.tsx

@@ -17,18 +17,16 @@ import {Tooltip} from 'sentry/components/tooltip';
 import {IconArrow, IconChevron, IconList, IconWarning} from 'sentry/icons';
 import {t, tct, tn} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import type {
-  Organization,
-  PlatformKey,
-  ReleaseProject,
-  ReleaseWithHealth,
-  SessionApiResponse,
-} from 'sentry/types';
 import {
+  type PlatformKey,
   ReleaseComparisonChartType,
+  type ReleaseProject,
+  type ReleaseWithHealth,
+  type SessionApiResponse,
   SessionFieldWithOperation,
   SessionStatus,
 } from 'sentry/types';
+import type {Organization} from 'sentry/types/organization';
 import {defined} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {browserHistory} from 'sentry/utils/browserHistory';
@@ -1031,7 +1029,7 @@ const DescriptionCell = styled(Cell)`
   overflow: visible;
 `;
 
-const Change = styled('div')<{color?: Color}>`
+export const Change = styled('div')<{color?: Color}>`
   font-size: ${p => p.theme.fontSizeMedium};
   ${p => p.color && `color: ${p.theme[p.color]}`}
 `;