Repository: getsentry/sentry
Pull request: feat(toolbar): add red indicator badge if crashes >> healthy sessions
- closes https://github.com/getsentry/sentry/issues/75184
- add red indicator badge with fire icon if over 10% of sessions are crashes (this threshold can be changed; i picked it arbitrarily)
- for ratios < 10%, no indicator badge

<img width="190" alt="SCR-20240729-ocbq" src="https://github.com/user-attachments/assets/ec1608f7-cd01-4aef-b8ba-3e7ad589138b">


Problem statement:
[DevToolbar] Show an indicator on the Open/Close Releases button that indicates whether the latest release has more or less crashes than the prev release
We could do like a subtle green indicator with a checkbox icon when things are good, and a red one with an arrow when things are bad. 

Add a tooltip so we know if it's crash-free rate (which is what's inside the panel) or crash-count. 
- Crash-free rate, higher is better, up to 100%
- Crash-count, lower is better, down to zero.

Hints from issue discussion:

Commit message: feat(toolbar): add red indicator badge if crashes >> healthy sessions

Patch to review:
diff --git a/static/app/components/devtoolbar/components/indicatorBadge.tsx b/static/app/components/devtoolbar/components/indicatorBadge.tsx
new file mode 100644
index 00000000000000..f5cc67d9d586ea
--- /dev/null
+++ b/static/app/components/devtoolbar/components/indicatorBadge.tsx
@@ -0,0 +1,37 @@
+import type React from 'react';
+import {css} from '@emotion/react';
+
+import {smallCss} from '../styles/typography';
+
+const variants = {
+  red: css`
+    background: var(--red400);
+    color: var(--gray100);
+  `,
+};
+
+interface Props {
+  icon: React.ReactNode;
+  variant: keyof typeof variants;
+}
+
+export default function IndicatorBadge({icon, variant}: Props) {
+  return <div css={[smallCss, counterCss, variants[variant]]}>{icon}</div>;
+}
+
+const counterCss = css`
+  background: var(--red400);
+  border-radius: 50%;
+  border: 1px solid transparent;
+  box-sizing: content-box;
+  color: var(--gray100);
+  height: 1rem;
+  line-height: 1rem;
+  position: absolute;
+  right: -6px;
+  top: -6px;
+  width: 1rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
diff --git a/static/app/components/devtoolbar/components/navigation.tsx b/static/app/components/devtoolbar/components/navigation.tsx
index 08d30818682fb3..24607c54b3750f 100644
--- a/static/app/components/devtoolbar/components/navigation.tsx
+++ b/static/app/components/devtoolbar/components/navigation.tsx
@@ -1,5 +1,6 @@
 import type {ReactNode} from 'react';
 
+import SessionStatusBadge from 'sentry/components/devtoolbar/components/releases/sessionStatusBadge';
 import {
   IconClose,
   IconFlag,
@@ -54,7 +55,9 @@ export default function Navigation({
         <AlertCountBadge />
       </NavButton>
       <NavButton panelName="featureFlags" label="Feature Flags" icon={<IconFlag />} />
-      <NavButton panelName="releases" label="Releases" icon={<IconReleases />} />
+      <NavButton panelName="releases" label="Releases" icon={<IconReleases />}>
+        <SessionStatusBadge />
+      </NavButton>
     </dialog>
   );
 }
diff --git a/static/app/components/devtoolbar/components/releases/sessionStatusBadge.tsx b/static/app/components/devtoolbar/components/releases/sessionStatusBadge.tsx
new file mode 100644
index 00000000000000..bb1f8f7fcf871e
--- /dev/null
+++ b/static/app/components/devtoolbar/components/releases/sessionStatusBadge.tsx
@@ -0,0 +1,24 @@
+import IndicatorBadge from 'sentry/components/devtoolbar/components/indicatorBadge';
+import useSessionStatus from 'sentry/components/devtoolbar/components/releases/useSessionStatus';
+import {IconFire} from 'sentry/icons';
+
+export default function CrashCountBadge() {
+  const {data} = useSessionStatus();
+  const healthySessions = data?.json.groups.find(
+    g => g.by['session.status'] === 'healthy'
+  )?.totals['sum(session)'];
+  const crashSessions = data?.json.groups.find(g => g.by['session.status'] === 'crashed')
+    ?.totals['sum(session)'];
+
+  if (!crashSessions || !healthySessions) {
+    return null;
+  }
+
+  const crashPercent = (crashSessions / healthySessions) * 100;
+
+  // over 10% of sessions were crashes
+  if (crashPercent > 10) {
+    return <IndicatorBadge icon={<IconFire size="xs" />} variant="red" />;
+  }
+  return null;
+}
diff --git a/static/app/components/devtoolbar/components/releases/useSessionStatus.tsx b/static/app/components/devtoolbar/components/releases/useSessionStatus.tsx
new file mode 100644
index 00000000000000..7ab262bf8d28f2
--- /dev/null
+++ b/static/app/components/devtoolbar/components/releases/useSessionStatus.tsx
@@ -0,0 +1,30 @@
+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 useSessionStatus() {
+  const {organizationSlug, projectId} = useConfiguration();
+  return useFetchApiData<SessionApiResponse>({
+    queryKey: useMemo(
+      (): ApiEndpointQueryKey => [
+        'io.sentry.toolbar',
+        `/organizations/${organizationSlug}/sessions/`,
+        {
+          query: {
+            queryReferrer: 'devtoolbar',
+            project: [projectId],
+            field: ['sum(session)'],
+            interval: '10m',
+            statsPeriod: '24h',
+            groupBy: ['session.status'],
+          },
+        },
+      ],
+      [organizationSlug, projectId]
+    ),
+    cacheTime: 5000,
+  });
+}


Code review comments:
diff_hunk: @@ -0,0 +1,24 @@
+import IndicatorBadge from 'sentry/components/devtoolbar/components/indicatorBadge';
+import useSessionStatus from 'sentry/components/devtoolbar/components/releases/useSessionStatus';
+import {IconFire} from 'sentry/icons';
+
+export default function CrashCountBadge() {
+  const {data} = useSessionStatus();
+  const healthySessions = data?.json.groups.find(
+    g => g.by['session.status'] === 'healthy'
+  )?.totals['sum(session)'];
+  const crashSessions = data?.json.groups.find(g => g.by['session.status'] === 'crashed')
+    ?.totals['sum(session)'];
+
+  if (!crashSessions || !healthySessions) {
+    return null;
+  }
+
+  const crashPercent = (crashSessions / healthySessions) * 100;
path: static/app/components/devtoolbar/components/releases/sessionStatusBadge.tsx
text: @user1:
Since it's percentage of sessions that were crashes, should we be using total number sessions instead?
```suggestion
  const crashPercent = (crashSessions / (crashSessions + healthySessions)) * 100;
```

@author:
oh yes good catch