diff --git a/static/app/views/sentryAppExternalInstallation/index.spec.tsx b/static/app/views/sentryAppExternalInstallation/index.spec.tsx
index caf5a52117a0a8..3680466dbe1b70 100644
--- a/static/app/views/sentryAppExternalInstallation/index.spec.tsx
+++ b/static/app/views/sentryAppExternalInstallation/index.spec.tsx
@@ -327,4 +327,62 @@ describe('SentryAppExternalInstallation', () => {
expect(getFeaturesMock).toHaveBeenCalled();
});
});
+
+ describe('sentry app fetch error', () => {
+ beforeEach(() => {
+ MockApiClient.addMockResponse({
+ url: '/organizations/',
+ body: [org1Lite],
+ });
+
+ // These endpoints are fetched concurrently with the sentry app request.
+ // Mock them so the test framework does not complain about unmocked requests,
+ // even though the error state renders before their responses are used.
+ MockApiClient.addMockResponse({
+ url: `/organizations/${org1.slug}/`,
+ body: org1,
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${org1.slug}/sentry-app-installations/`,
+ body: [],
+ });
+
+ window.__initialData = ConfigFixture({
+ customerDomain: {
+ subdomain: 'org1',
+ organizationUrl: 'https://org1.sentry.io',
+ sentryUrl: 'https://sentry.io',
+ },
+ links: {
+ ...window.__initialData?.links,
+ sentryUrl: 'https://sentry.io',
+ },
+ });
+ ConfigStore.loadInitialData(window.__initialData);
+ });
+
+ it('shows an error when the sentry app request fails (e.g. 403)', async () => {
+ MockApiClient.addMockResponse({
+ url: `/sentry-apps/${sentryApp.slug}/`,
+ statusCode: 403,
+ body: {
+ detail: "User must be in the app owner's organization for unpublished apps",
+ },
+ });
+
+ render(, {
+ initialRouterConfig: {
+ route: '/sentry-apps/:sentryAppSlug/external-install/',
+ location: {
+ pathname: `/sentry-apps/${sentryApp.slug}/external-install/`,
+ },
+ },
+ });
+
+ expect(
+ await screen.findByText('There was an error loading this integration.')
+ ).toBeInTheDocument();
+ });
+ });
});
diff --git a/static/app/views/sentryAppExternalInstallation/index.tsx b/static/app/views/sentryAppExternalInstallation/index.tsx
index db27c4cbbd3140..b3a57abbcb32d8 100644
--- a/static/app/views/sentryAppExternalInstallation/index.tsx
+++ b/static/app/views/sentryAppExternalInstallation/index.tsx
@@ -9,6 +9,7 @@ import {addErrorMessage} from 'sentry/actionCreators/indicator';
import {fetchOrganizations} from 'sentry/actionCreators/organizations';
import {installSentryApp} from 'sentry/actionCreators/sentryAppInstallations';
import {FieldGroup} from 'sentry/components/forms/fieldGroup';
+import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {SentryAppDetailsModal} from 'sentry/components/modals/sentryAppDetailsModal';
import {NarrowLayout} from 'sentry/components/narrowLayout';
@@ -51,10 +52,15 @@ function SentryAppExternalInstallationContent() {
const [organizations, setOrganizations] = useState([]);
const [orgsLoading, setOrgsLoading] = useState(true);
+ const [orgsError, setOrgsError] = useState(false);
const [isInstalled, setIsInstalled] = useState();
// Load data on mount.
- const {data: sentryApp, isPending: sentryAppLoading} = useApiQuery(
+ const {
+ data: sentryApp,
+ isPending: sentryAppLoading,
+ isError: sentryAppError,
+ } = useApiQuery(
[
getApiUrl('/sentry-apps/$sentryAppIdOrSlug/', {
path: {sentryAppIdOrSlug: params.sentryAppSlug},
@@ -62,6 +68,7 @@ function SentryAppExternalInstallationContent() {
],
{
staleTime: 0,
+ retry: false,
}
);
@@ -73,7 +80,7 @@ function SentryAppExternalInstallationContent() {
setOrgsLoading(false);
} catch (e) {
setOrgsLoading(false);
- // Do nothing.
+ setOrgsError(true);
}
}
loadOrgs();
@@ -192,10 +199,22 @@ function SentryAppExternalInstallationContent() {
return onClose();
}, [api, organization, sentryApp, onClose, location.query.state]);
- if (sentryAppLoading || orgsLoading || !sentryApp) {
+ if (sentryAppLoading || orgsLoading) {
return ;
}
+ if (sentryAppError) {
+ return ;
+ }
+
+ if (orgsError) {
+ return ;
+ }
+
+ if (!sentryApp) {
+ return ;
+ }
+
return (