Browse Source

feat(crons): Show error content for each monitor (#71177)

Looks like:

<img width="904" alt="image"
src="https://github.com/getsentry/sentry/assets/9372512/80ea4c92-32aa-44bd-ba96-e5ad55169e6f">
David Wang 9 months ago
parent
commit
7824e71982

+ 88 - 0
static/app/views/monitors/components/processingErrors/monitorProcessingErrors.tsx

@@ -0,0 +1,88 @@
+import {useState} from 'react';
+import styled from '@emotion/styled';
+import groupBy from 'lodash/groupBy';
+
+import Accordion from 'sentry/components/accordion/accordion';
+import Alert from 'sentry/components/alert';
+import Tag from 'sentry/components/badge/tag';
+import {DateTime} from 'sentry/components/dateTime';
+import List from 'sentry/components/list';
+import ListItem from 'sentry/components/list/listItem';
+import {Tooltip} from 'sentry/components/tooltip';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {ProcessingErrorItem} from 'sentry/views/monitors/components/processingErrors/processingErrorItem';
+import {ProcessingErrorTitle} from 'sentry/views/monitors/components/processingErrors/processingErrorTitle';
+import type {CheckInPayload, CheckinProcessingError} from 'sentry/views/monitors/types';
+
+export default function MonitorProcessingErrors({
+  checkinErrors,
+}: {
+  checkinErrors: CheckinProcessingError[];
+}) {
+  const flattenedErrors = checkinErrors.flatMap(({errors, checkin}) =>
+    errors.map(error => ({error, checkin}))
+  );
+  const errorsByType = groupBy(flattenedErrors, ({error}) => error.type);
+
+  const renderCheckinTooltip = (checkin: CheckInPayload) => (
+    <Tooltip
+      skipWrapper
+      showUnderline
+      title={
+        <div>
+          {tct('[status] check-in sent on [date]', {
+            status: checkin.payload.status,
+            date: <DateTime timeZone date={checkin.message.start_time * 1000} />,
+          })}
+        </div>
+      }
+    />
+  );
+
+  const [expanded, setExpanded] = useState(-1);
+  const accordionErrors = (
+    <Accordion
+      items={Object.values(errorsByType).map(errors => {
+        return {
+          header: () => (
+            <ErrorHeader>
+              <Tag type="error">{errors.length}x</Tag>
+              <ProcessingErrorTitle type={errors[0].error.type} />
+            </ErrorHeader>
+          ),
+          content: () => (
+            <List symbol="bullet">
+              {errors.map(({error, checkin}, i) => (
+                <ListItem key={i}>
+                  <ProcessingErrorItem
+                    error={error}
+                    checkinTooltip={renderCheckinTooltip(checkin)}
+                  />
+                </ListItem>
+              ))}
+            </List>
+          ),
+        };
+      })}
+      expandedIndex={expanded}
+      setExpandedIndex={setExpanded}
+    />
+  );
+
+  return (
+    <ScrollableAlert type="error" showIcon expand={accordionErrors}>
+      {t('Errors were encountered while ingesting check-ins for this monitor')}
+    </ScrollableAlert>
+  );
+}
+
+const ErrorHeader = styled('div')`
+  display: flex;
+  gap: ${space(1)};
+`;
+
+const ScrollableAlert = styled(Alert)`
+  max-height: 400px;
+  overflow-y: auto;
+`;

+ 12 - 0
static/app/views/monitors/components/processingErrors/utils.tsx

@@ -0,0 +1,12 @@
+import type {Organization} from 'sentry/types';
+
+export function makeMonitorErrorsQueryKey(
+  organization: Organization,
+  projectId: string,
+  monitorSlug: string
+) {
+  return [
+    `/projects/${organization.slug}/${projectId}/monitors/${monitorSlug}/processing-errors/`,
+    {},
+  ] as const;
+}

+ 4 - 0
static/app/views/monitors/details.spec.tsx

@@ -33,6 +33,10 @@ describe('Monitor Details', () => {
       url: `/projects/${organization.slug}/${project.slug}/monitors/${monitor.slug}/checkins/`,
       body: [],
     });
+    MockApiClient.addMockResponse({
+      url: `/projects/${organization.slug}/${project.slug}/monitors/${monitor.slug}/processing-errors/`,
+      body: [],
+    });
   });
 
   it('renders', async function () {

+ 14 - 1
static/app/views/monitors/details.tsx

@@ -19,6 +19,8 @@ import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import DetailsSidebar from 'sentry/views/monitors/components/detailsSidebar';
 import {DetailsTimeline} from 'sentry/views/monitors/components/detailsTimeline';
+import MonitorProcessingErrors from 'sentry/views/monitors/components/processingErrors/monitorProcessingErrors';
+import {makeMonitorErrorsQueryKey} from 'sentry/views/monitors/components/processingErrors/utils';
 import {makeMonitorDetailsQueryKey} from 'sentry/views/monitors/utils';
 
 import MonitorCheckIns from './components/monitorCheckIns';
@@ -27,7 +29,7 @@ import MonitorIssues from './components/monitorIssues';
 import MonitorStats from './components/monitorStats';
 import MonitorOnboarding from './components/onboarding';
 import {StatusToggleButton} from './components/statusToggleButton';
-import type {Monitor} from './types';
+import type {CheckinProcessingError, Monitor} from './types';
 
 const DEFAULT_POLL_INTERVAL_MS = 5000;
 
@@ -65,6 +67,14 @@ function MonitorDetails({params, location}: Props) {
     },
   });
 
+  const {data: checkinErrors} = useApiQuery<CheckinProcessingError[]>(
+    makeMonitorErrorsQueryKey(organization, params.projectId, params.monitorSlug),
+    {
+      staleTime: 0,
+      refetchOnWindowFocus: true,
+    }
+  );
+
   function onUpdate(data: Monitor) {
     const updatedMonitor = {
       ...data,
@@ -134,6 +144,9 @@ function MonitorDetails({params, location}: Props) {
                 {t('This monitor is disabled and is not accepting check-ins.')}
               </Alert>
             )}
+            {!!checkinErrors?.length && (
+              <MonitorProcessingErrors checkinErrors={checkinErrors} />
+            )}
             {!hasLastCheckIn(monitor) ? (
               <MonitorOnboarding monitor={monitor} />
             ) : (