Skip to content

Commit b876ebd

Browse files
priscilawebdevvuluongj20
authored andcommitted
feat(alerts): Add project incident aler to proj/issue stream page - NATIVE-216 & NATIVE-217 (#28726)
1 parent 5ebb0a1 commit b876ebd

File tree

10 files changed

+279
-3
lines changed

10 files changed

+279
-3
lines changed

src/sentry/api/serializers/models/project.py

+3
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,9 @@ def serialize(self, obj, attrs, user):
561561
"hasAccess": attrs["has_access"],
562562
"dateCreated": obj.date_added,
563563
"environments": attrs["environments"],
564+
"eventProcessing": {
565+
"symbolicationDegraded": False,
566+
},
564567
"features": attrs["features"],
565568
"firstEvent": obj.first_event,
566569
"firstTransactionEvent": True if obj.flags.has_transactions else False,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {Fragment} from 'react';
2+
3+
import Alert from 'app/components/alert';
4+
import ExternalLink from 'app/components/links/externalLink';
5+
import {IconInfo} from 'app/icons';
6+
import {tct} from 'app/locale';
7+
import {Project} from 'app/types';
8+
9+
const sentryStatusPageLink = 'https://status.sentry.io/';
10+
11+
type Props = {
12+
projects: Project[];
13+
className?: string;
14+
};
15+
16+
// This alert makes the user aware that one or more projects have been selected for the Low Priority Queue
17+
function GlobalEventProcessingAlert({className, projects}: Props) {
18+
const projectsInTheLowPriorityQueue = projects.filter(
19+
project => project.eventProcessing.symbolicationDegraded
20+
);
21+
22+
if (!projectsInTheLowPriorityQueue.length) {
23+
return null;
24+
}
25+
26+
return (
27+
<Alert className={className} type="info" icon={<IconInfo size="sm" />}>
28+
{projectsInTheLowPriorityQueue.length === 1
29+
? tct(
30+
'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.',
31+
{
32+
link: <ExternalLink href={sentryStatusPageLink} />,
33+
}
34+
)
35+
: tct(
36+
'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.',
37+
{
38+
projectSlugs: projectsInTheLowPriorityQueue.map(({slug}, index) => (
39+
<Fragment key={slug}>
40+
<strong>{slug}</strong>
41+
{index !== projectsInTheLowPriorityQueue.length - 1 && ', '}
42+
</Fragment>
43+
)),
44+
link: <ExternalLink href={sentryStatusPageLink} />,
45+
}
46+
)}
47+
</Alert>
48+
);
49+
}
50+
51+
export default GlobalEventProcessingAlert;

static/app/types/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ export type Project = {
281281
digestsMaxDelay: number;
282282
digestsMinDelay: number;
283283
environments: string[];
284+
eventProcessing: {
285+
symbolicationDegraded: boolean;
286+
};
284287

285288
// XXX: These are part of the DetailedProject serializer
286289
dynamicSampling: {

static/app/views/issueList/header.tsx

+24-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import GuideAnchor from 'app/components/assistant/guideAnchor';
66
import Badge from 'app/components/badge';
77
import Button from 'app/components/button';
88
import ButtonBar from 'app/components/buttonBar';
9+
import GlobalEventProcessingAlert from 'app/components/globalEventProcessingAlert';
910
import * as Layout from 'app/components/layouts/thirds';
1011
import Link from 'app/components/links/link';
1112
import QueryCount from 'app/components/queryCount';
1213
import Tooltip from 'app/components/tooltip';
1314
import {IconPause, IconPlay} from 'app/icons';
1415
import {t} from 'app/locale';
1516
import space from 'app/styles/space';
16-
import {Organization} from 'app/types';
17+
import {Organization, Project} from 'app/types';
1718
import {trackAnalyticsEvent} from 'app/utils/analytics';
19+
import withProjects from 'app/utils/withProjects';
1820

1921
import SavedSearchTab from './savedSearchTab';
2022
import {getTabs, IssueSortOptions, Query, QueryCounts, TAB_MAX_COUNT} from './utils';
@@ -47,6 +49,8 @@ type Props = {
4749
router: InjectedRouter;
4850
onRealtimeChange: (realtime: boolean) => void;
4951
displayReprocessingTab: boolean;
52+
selectedProjectIds: number[];
53+
projects: Project[];
5054
queryCount?: number;
5155
} & React.ComponentProps<typeof SavedSearchTab>;
5256

@@ -63,6 +67,8 @@ function IssueListHeader({
6367
savedSearchList,
6468
router,
6569
displayReprocessingTab,
70+
selectedProjectIds,
71+
projects,
6672
}: Props) {
6773
const tabs = getTabs(organization);
6874
const visibleTabs = displayReprocessingTab
@@ -85,6 +91,10 @@ function IssueListHeader({
8591
}
8692
}
8793

94+
const selectedProjects = projects.filter(({id}) =>
95+
selectedProjectIds.includes(Number(id))
96+
);
97+
8898
return (
8999
<React.Fragment>
90100
<BorderlessHeader>
@@ -103,6 +113,7 @@ function IssueListHeader({
103113
</Button>
104114
</ButtonBar>
105115
</Layout.HeaderActions>
116+
<StyledGlobalEventProcessingAlert projects={selectedProjects} />
106117
</BorderlessHeader>
107118
<TabLayoutHeader>
108119
<Layout.HeaderNavTabs underlined>
@@ -168,7 +179,7 @@ function IssueListHeader({
168179
);
169180
}
170181

171-
export default IssueListHeader;
182+
export default withProjects(IssueListHeader);
172183

173184
const StyledLayoutTitle = styled(Layout.Title)`
174185
margin-top: ${space(0.5)};
@@ -192,3 +203,14 @@ const StyledHeaderContent = styled(Layout.HeaderContent)`
192203
margin-bottom: 0;
193204
margin-right: ${space(2)};
194205
`;
206+
207+
const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
208+
grid-column: 1/-1;
209+
margin-top: ${space(1)};
210+
margin-bottom: ${space(1)};
211+
212+
@media (min-width: ${p => p.theme.breakpoints[1]}) {
213+
margin-top: ${space(2)};
214+
margin-bottom: 0;
215+
}
216+
`;

static/app/views/issueList/overview.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,7 @@ class IssueListOverview extends React.Component<Props, State> {
10991099
onSavedSearchSelect={this.onSavedSearchSelect}
11001100
onSavedSearchDelete={this.onSavedSearchDelete}
11011101
displayReprocessingTab={showReprocessingTab}
1102+
selectedProjectIds={selection.projects}
11021103
/>
11031104

11041105
<StyledPageContent>

static/app/views/organizationGroupDetails/quickTrace/index.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type Props = {
1414
location: Location;
1515
};
1616

17-
export default function QuickTrace({event, group, organization, location}: Props) {
17+
function QuickTrace({event, group, organization, location}: Props) {
1818
const hasPerformanceView = organization.features.includes('performance-view');
1919
const hasTraceContext = Boolean(event.contexts?.trace?.trace_id);
2020

@@ -33,3 +33,5 @@ export default function QuickTrace({event, group, organization, location}: Props
3333
</Fragment>
3434
);
3535
}
36+
37+
export default QuickTrace;

static/app/views/projectDetail/projectDetail.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Button from 'app/components/button';
1111
import ButtonBar from 'app/components/buttonBar';
1212
import CreateAlertButton from 'app/components/createAlertButton';
1313
import GlobalAppStoreConnectUpdateAlert from 'app/components/globalAppStoreConnectUpdateAlert';
14+
import GlobalEventProcessingAlert from 'app/components/globalEventProcessingAlert';
1415
import GlobalSdkUpdateAlert from 'app/components/globalSdkUpdateAlert';
1516
import IdBadge from 'app/components/idBadge';
1617
import * as Layout from 'app/components/layouts/thirds';
@@ -285,6 +286,7 @@ class ProjectDetail extends AsyncView<Props, State> {
285286
</Layout.Header>
286287

287288
<Layout.Body>
289+
{project && <StyledGlobalEventProcessingAlert projects={[project]} />}
288290
<StyledSdkUpdatesAlert />
289291
<StyledGlobalAppStoreConnectUpdateAlert
290292
project={project}
@@ -382,6 +384,12 @@ const StyledSdkUpdatesAlert = styled(GlobalSdkUpdateAlert)`
382384
}
383385
`;
384386

387+
const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
388+
@media (min-width: ${p => p.theme.breakpoints[1]}) {
389+
margin-bottom: 0;
390+
}
391+
`;
392+
385393
StyledSdkUpdatesAlert.defaultProps = {
386394
Wrapper: p => <Layout.Main fullWidth {...p} />,
387395
};

tests/fixtures/js-stubs/project.js

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export function Project(params) {
99
teams: [],
1010
environments: [],
1111
features: [],
12+
eventProcessing: {
13+
symbolicationDegraded: false,
14+
},
1215
...params,
1316
};
1417
}

tests/js/spec/views/issueList/overview.spec.jsx

+75
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
77

88
import StreamGroup from 'app/components/stream/group';
99
import GroupStore from 'app/stores/groupStore';
10+
import ProjectsStore from 'app/stores/projectsStore';
1011
import TagStore from 'app/stores/tagStore';
1112
import * as parseLinkHeader from 'app/utils/parseLinkHeader';
1213
import IssueListWithStores, {IssueListOverview} from 'app/views/issueList/overview';
@@ -1811,4 +1812,78 @@ describe('IssueList', function () {
18111812
expect(wrapper.instance().getGroupStatsPeriod()).toBe('auto');
18121813
});
18131814
});
1815+
1816+
describe('project low priority queue alert', function () {
1817+
const {routerContext} = initializeOrg();
1818+
1819+
beforeEach(function () {
1820+
ProjectsStore.reset();
1821+
});
1822+
1823+
it('does not render alert', function () {
1824+
ProjectsStore.loadInitialData([project]);
1825+
1826+
wrapper = mountWithTheme(<IssueListOverview {...props} />, routerContext);
1827+
1828+
const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
1829+
expect(eventProcessingAlert.exists()).toBe(true);
1830+
expect(eventProcessingAlert.isEmptyRender()).toBe(true);
1831+
});
1832+
1833+
describe('renders alert', function () {
1834+
it('for one project', function () {
1835+
ProjectsStore.loadInitialData([
1836+
{...project, eventProcessing: {symbolicationDegraded: true}},
1837+
]);
1838+
1839+
wrapper = mountWithTheme(<IssueListOverview {...props} />, routerContext);
1840+
1841+
const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
1842+
expect(eventProcessingAlert.exists()).toBe(true);
1843+
expect(eventProcessingAlert.isEmptyRender()).toBe(false);
1844+
expect(eventProcessingAlert.text()).toBe(
1845+
'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.'
1846+
);
1847+
});
1848+
1849+
it('for multiple projects', function () {
1850+
const projectBar = TestStubs.ProjectDetails({
1851+
id: '3560',
1852+
name: 'Bar Project',
1853+
slug: 'project-slug-bar',
1854+
});
1855+
1856+
ProjectsStore.loadInitialData([
1857+
{
1858+
...project,
1859+
slug: 'project-slug',
1860+
eventProcessing: {symbolicationDegraded: true},
1861+
},
1862+
{
1863+
...projectBar,
1864+
slug: 'project-slug-bar',
1865+
eventProcessing: {symbolicationDegraded: true},
1866+
},
1867+
]);
1868+
1869+
wrapper = mountWithTheme(
1870+
<IssueListOverview
1871+
{...props}
1872+
selection={{
1873+
...props.selection,
1874+
projects: [Number(project.id), Number(projectBar.id)],
1875+
}}
1876+
/>,
1877+
routerContext
1878+
);
1879+
1880+
const eventProcessingAlert = wrapper.find('StyledGlobalEventProcessingAlert');
1881+
expect(eventProcessingAlert.exists()).toBe(true);
1882+
expect(eventProcessingAlert.isEmptyRender()).toBe(false);
1883+
expect(eventProcessingAlert.text()).toBe(
1884+
`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.`
1885+
);
1886+
});
1887+
});
1888+
});
18141889
});

0 commit comments

Comments
 (0)