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 (