Browse Source

feat(alerts): Add project incident aler to proj/issue stream page - NATIVE-216 & NATIVE-217 (#28726)

Priscila Oliveira 3 years ago
parent
commit
a1dba54d01

+ 3 - 0
src/sentry/api/serializers/models/project.py

@@ -561,6 +561,9 @@ class ProjectSummarySerializer(ProjectWithTeamSerializer):
             "hasAccess": attrs["has_access"],
             "dateCreated": obj.date_added,
             "environments": attrs["environments"],
+            "eventProcessing": {
+                "symbolicationDegraded": False,
+            },
             "features": attrs["features"],
             "firstEvent": obj.first_event,
             "firstTransactionEvent": True if obj.flags.has_transactions else False,

+ 51 - 0
static/app/components/globalEventProcessingAlert.tsx

@@ -0,0 +1,51 @@
+import {Fragment} from 'react';
+
+import Alert from 'app/components/alert';
+import ExternalLink from 'app/components/links/externalLink';
+import {IconInfo} from 'app/icons';
+import {tct} from 'app/locale';
+import {Project} from 'app/types';
+
+const sentryStatusPageLink = 'https://status.sentry.io/';
+
+type Props = {
+  projects: Project[];
+  className?: string;
+};
+
+// This alert makes the user aware that one or more projects have been selected for the Low Priority Queue
+function GlobalEventProcessingAlert({className, projects}: Props) {
+  const projectsInTheLowPriorityQueue = projects.filter(
+    project => project.eventProcessing.symbolicationDegraded
+  );
+
+  if (!projectsInTheLowPriorityQueue.length) {
+    return null;
+  }
+
+  return (
+    <Alert className={className} type="info" icon={<IconInfo size="sm" />}>
+      {projectsInTheLowPriorityQueue.length === 1
+        ? tct(
+            'Event Processing for this project is currently degraded. Events may appear with larger delays than usual or get dropped. Please check the [link:Status] page for a potential outage.',
+            {
+              link: <ExternalLink href={sentryStatusPageLink} />,
+            }
+          )
+        : tct(
+            'Event Processing for the [projectSlugs] projects is currently degraded. Events may appear with larger delays than usual or get dropped. Please check the [link:Status] page for a potential outage.',
+            {
+              projectSlugs: projectsInTheLowPriorityQueue.map(({slug}, index) => (
+                <Fragment key={slug}>
+                  <strong>{slug}</strong>
+                  {index !== projectsInTheLowPriorityQueue.length - 1 && ', '}
+                </Fragment>
+              )),
+              link: <ExternalLink href={sentryStatusPageLink} />,
+            }
+          )}
+    </Alert>
+  );
+}
+
+export default GlobalEventProcessingAlert;

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

@@ -281,6 +281,9 @@ export type Project = {
   digestsMaxDelay: number;
   digestsMinDelay: number;
   environments: string[];
+  eventProcessing: {
+    symbolicationDegraded: boolean;
+  };
 
   // XXX: These are part of the DetailedProject serializer
   dynamicSampling: {

+ 24 - 2
static/app/views/issueList/header.tsx

@@ -6,6 +6,7 @@ import GuideAnchor from 'app/components/assistant/guideAnchor';
 import Badge from 'app/components/badge';
 import Button from 'app/components/button';
 import ButtonBar from 'app/components/buttonBar';
+import GlobalEventProcessingAlert from 'app/components/globalEventProcessingAlert';
 import * as Layout from 'app/components/layouts/thirds';
 import Link from 'app/components/links/link';
 import QueryCount from 'app/components/queryCount';
@@ -13,8 +14,9 @@ import Tooltip from 'app/components/tooltip';
 import {IconPause, IconPlay} from 'app/icons';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
-import {Organization} from 'app/types';
+import {Organization, Project} from 'app/types';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
+import withProjects from 'app/utils/withProjects';
 
 import SavedSearchTab from './savedSearchTab';
 import {getTabs, IssueSortOptions, Query, QueryCounts, TAB_MAX_COUNT} from './utils';
@@ -47,6 +49,8 @@ type Props = {
   router: InjectedRouter;
   onRealtimeChange: (realtime: boolean) => void;
   displayReprocessingTab: boolean;
+  selectedProjectIds: number[];
+  projects: Project[];
   queryCount?: number;
 } & React.ComponentProps<typeof SavedSearchTab>;
 
@@ -63,6 +67,8 @@ function IssueListHeader({
   savedSearchList,
   router,
   displayReprocessingTab,
+  selectedProjectIds,
+  projects,
 }: Props) {
   const tabs = getTabs(organization);
   const visibleTabs = displayReprocessingTab
@@ -85,6 +91,10 @@ function IssueListHeader({
     }
   }
 
+  const selectedProjects = projects.filter(({id}) =>
+    selectedProjectIds.includes(Number(id))
+  );
+
   return (
     <React.Fragment>
       <BorderlessHeader>
@@ -103,6 +113,7 @@ function IssueListHeader({
             </Button>
           </ButtonBar>
         </Layout.HeaderActions>
+        <StyledGlobalEventProcessingAlert projects={selectedProjects} />
       </BorderlessHeader>
       <TabLayoutHeader>
         <Layout.HeaderNavTabs underlined>
@@ -168,7 +179,7 @@ function IssueListHeader({
   );
 }
 
-export default IssueListHeader;
+export default withProjects(IssueListHeader);
 
 const StyledLayoutTitle = styled(Layout.Title)`
   margin-top: ${space(0.5)};
@@ -192,3 +203,14 @@ const StyledHeaderContent = styled(Layout.HeaderContent)`
   margin-bottom: 0;
   margin-right: ${space(2)};
 `;
+
+const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
+  grid-column: 1/-1;
+  margin-top: ${space(1)};
+  margin-bottom: ${space(1)};
+
+  @media (min-width: ${p => p.theme.breakpoints[1]}) {
+    margin-top: ${space(2)};
+    margin-bottom: 0;
+  }
+`;

+ 1 - 0
static/app/views/issueList/overview.tsx

@@ -1099,6 +1099,7 @@ class IssueListOverview extends React.Component<Props, State> {
           onSavedSearchSelect={this.onSavedSearchSelect}
           onSavedSearchDelete={this.onSavedSearchDelete}
           displayReprocessingTab={showReprocessingTab}
+          selectedProjectIds={selection.projects}
         />
 
         <StyledPageContent>

+ 3 - 1
static/app/views/organizationGroupDetails/quickTrace/index.tsx

@@ -14,7 +14,7 @@ type Props = {
   location: Location;
 };
 
-export default function QuickTrace({event, group, organization, location}: Props) {
+function QuickTrace({event, group, organization, location}: Props) {
   const hasPerformanceView = organization.features.includes('performance-view');
   const hasTraceContext = Boolean(event.contexts?.trace?.trace_id);
 
@@ -33,3 +33,5 @@ export default function QuickTrace({event, group, organization, location}: Props
     </Fragment>
   );
 }
+
+export default QuickTrace;

+ 8 - 0
static/app/views/projectDetail/projectDetail.tsx

@@ -11,6 +11,7 @@ import Button from 'app/components/button';
 import ButtonBar from 'app/components/buttonBar';
 import CreateAlertButton from 'app/components/createAlertButton';
 import GlobalAppStoreConnectUpdateAlert from 'app/components/globalAppStoreConnectUpdateAlert';
+import GlobalEventProcessingAlert from 'app/components/globalEventProcessingAlert';
 import GlobalSdkUpdateAlert from 'app/components/globalSdkUpdateAlert';
 import IdBadge from 'app/components/idBadge';
 import * as Layout from 'app/components/layouts/thirds';
@@ -285,6 +286,7 @@ class ProjectDetail extends AsyncView<Props, State> {
             </Layout.Header>
 
             <Layout.Body>
+              {project && <StyledGlobalEventProcessingAlert projects={[project]} />}
               <StyledSdkUpdatesAlert />
               <StyledGlobalAppStoreConnectUpdateAlert
                 project={project}
@@ -382,6 +384,12 @@ const StyledSdkUpdatesAlert = styled(GlobalSdkUpdateAlert)`
   }
 `;
 
+const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
+  @media (min-width: ${p => p.theme.breakpoints[1]}) {
+    margin-bottom: 0;
+  }
+`;
+
 StyledSdkUpdatesAlert.defaultProps = {
   Wrapper: p => <Layout.Main fullWidth {...p} />,
 };

+ 3 - 0
tests/fixtures/js-stubs/project.js

@@ -9,6 +9,9 @@ export function Project(params) {
     teams: [],
     environments: [],
     features: [],
+    eventProcessing: {
+      symbolicationDegraded: false,
+    },
     ...params,
   };
 }

+ 75 - 0
tests/js/spec/views/issueList/overview.spec.jsx

@@ -7,6 +7,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
 
 import StreamGroup from 'app/components/stream/group';
 import GroupStore from 'app/stores/groupStore';
+import ProjectsStore from 'app/stores/projectsStore';
 import TagStore from 'app/stores/tagStore';
 import * as parseLinkHeader from 'app/utils/parseLinkHeader';
 import IssueListWithStores, {IssueListOverview} from 'app/views/issueList/overview';
@@ -1811,4 +1812,78 @@ describe('IssueList', function () {
       expect(wrapper.instance().getGroupStatsPeriod()).toBe('auto');
     });
   });
+
+  describe('project low priority queue alert', function () {
+    const {routerContext} = initializeOrg();
+
+    beforeEach(function () {
+      ProjectsStore.reset();
+    });
+
+    it('does not render alert', function () {
+      ProjectsStore.loadInitialData([project]);
+
+      wrapper = mountWithTheme(<IssueListOverview {...props} />, routerContext);
+
+      const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
+      expect(eventProcessingAlert.exists()).toBe(true);
+      expect(eventProcessingAlert.isEmptyRender()).toBe(true);
+    });
+
+    describe('renders alert', function () {
+      it('for one project', function () {
+        ProjectsStore.loadInitialData([
+          {...project, eventProcessing: {symbolicationDegraded: true}},
+        ]);
+
+        wrapper = mountWithTheme(<IssueListOverview {...props} />, routerContext);
+
+        const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
+        expect(eventProcessingAlert.exists()).toBe(true);
+        expect(eventProcessingAlert.isEmptyRender()).toBe(false);
+        expect(eventProcessingAlert.text()).toBe(
+          'Event Processing for this project is currently degraded. Events may appear with larger delays than usual or get dropped. Please check the Status page for a potential outage.'
+        );
+      });
+
+      it('for multiple projects', function () {
+        const projectBar = TestStubs.ProjectDetails({
+          id: '3560',
+          name: 'Bar Project',
+          slug: 'project-slug-bar',
+        });
+
+        ProjectsStore.loadInitialData([
+          {
+            ...project,
+            slug: 'project-slug',
+            eventProcessing: {symbolicationDegraded: true},
+          },
+          {
+            ...projectBar,
+            slug: 'project-slug-bar',
+            eventProcessing: {symbolicationDegraded: true},
+          },
+        ]);
+
+        wrapper = mountWithTheme(
+          <IssueListOverview
+            {...props}
+            selection={{
+              ...props.selection,
+              projects: [Number(project.id), Number(projectBar.id)],
+            }}
+          />,
+          routerContext
+        );
+
+        const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
+        expect(eventProcessingAlert.exists()).toBe(true);
+        expect(eventProcessingAlert.isEmptyRender()).toBe(false);
+        expect(eventProcessingAlert.text()).toBe(
+          `Event Processing for the ${project.slug}, ${projectBar.slug} projects is currently degraded. Events may appear with larger delays than usual or get dropped. Please check the Status page for a potential outage.`
+        );
+      });
+    });
+  });
 });

+ 108 - 0
tests/js/spec/views/projectDetail/index.spec.tsx

@@ -0,0 +1,108 @@
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {mountWithTheme, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary';
+import {findByTextContent} from 'sentry-test/utils';
+
+import GlobalSelectionStore from 'app/stores/globalSelectionStore';
+import ProjectsStore from 'app/stores/projectsStore';
+import ProjectDetails from 'app/views/projectDetail/projectDetail';
+
+describe('ProjectDetail', function () {
+  const {routerContext, organization, project, router} = initializeOrg();
+  const params = {...router.params, projectId: project.slug};
+
+  beforeEach(() => {
+    GlobalSelectionStore.reset();
+    ProjectsStore.reset();
+    // @ts-expect-error
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/sdk-updates/',
+      body: [],
+    });
+
+    // @ts-expect-error
+    MockApiClient.addMockResponse({
+      url: '/prompts-activity/',
+      body: {},
+    });
+  });
+
+  describe('project low priority queue alert', function () {
+    it('does not render alert', async function () {
+      const projects = [
+        {
+          ...project,
+          eventProcessing: {
+            symbolicationDegraded: false,
+          },
+        },
+      ];
+
+      // @ts-expect-error
+      MockApiClient.addMockResponse({
+        url: '/organizations/org-slug/projects/',
+        body: projects,
+      });
+
+      // @ts-expect-error
+      MockApiClient.addMockResponse({
+        url: '/projects/org-slug/project-slug/',
+        data: projects[0],
+      });
+
+      ProjectsStore.loadInitialData(projects);
+
+      const component = mountWithTheme(
+        <ProjectDetails organization={organization} {...router} params={params} />,
+        {context: routerContext}
+      );
+
+      await waitForElementToBeRemoved(() => component.getByText('Loading\u2026'));
+
+      expect(
+        component.queryByText(
+          'Event Processing for this project is currently degraded. Events may appear with larger delays than usual or get dropped.',
+          {exact: false}
+        )
+      ).toBe(null);
+    });
+
+    it('renders alert', async function () {
+      const projects = [
+        {
+          ...project,
+          eventProcessing: {
+            symbolicationDegraded: true,
+          },
+        },
+      ];
+
+      ProjectsStore.loadInitialData(projects);
+
+      // @ts-expect-error
+      MockApiClient.addMockResponse({
+        url: '/organizations/org-slug/projects/',
+        body: projects,
+      });
+
+      // @ts-expect-error
+      MockApiClient.addMockResponse({
+        url: '/projects/org-slug/project-slug/',
+        data: projects[0],
+      });
+
+      const component = mountWithTheme(
+        <ProjectDetails organization={organization} {...router} params={params} />,
+        {context: routerContext}
+      );
+
+      await waitForElementToBeRemoved(() => component.getByText('Loading\u2026'));
+
+      expect(
+        await findByTextContent(
+          component,
+          'Event Processing for this project is currently degraded. Events may appear with larger delays than usual or get dropped. Please check the Status page for a potential outage.'
+        )
+      ).toBeInTheDocument();
+    });
+  });
+});