diff --git a/static/app/components/events/autofix/autofixGithubAppPermissionsModal.tsx b/static/app/components/events/autofix/autofixGithubAppPermissionsModal.tsx
index 7d6371f5d3f1..542c4e5cc982 100644
--- a/static/app/components/events/autofix/autofixGithubAppPermissionsModal.tsx
+++ b/static/app/components/events/autofix/autofixGithubAppPermissionsModal.tsx
@@ -9,7 +9,9 @@ import type {ModalRenderProps} from 'sentry/actionCreators/modal';
import {t, tct} from 'sentry/locale';
interface AutofixGithubAppPermissionsModalProps extends ModalRenderProps {
+ description?: React.ReactNode;
installationUrl?: string;
+ onUpdatePermissionsClick?: () => void;
}
const DEFAULT_INSTALLATIONS_URL = 'https://github.com/settings/installations/';
@@ -20,6 +22,8 @@ export function AutofixGithubAppPermissionsModal({
Footer,
closeModal,
installationUrl,
+ description,
+ onUpdatePermissionsClick,
}: AutofixGithubAppPermissionsModalProps) {
const settingsUrl = installationUrl ?? DEFAULT_INSTALLATIONS_URL;
@@ -30,18 +34,27 @@ export function AutofixGithubAppPermissionsModal({
- {tct(
- 'The Sentry GitHub App does not have sufficient permissions to launch a coding agent. Please update your [link:GitHub App installation settings] to grant the required permissions.',
- {
- link: ,
- }
- )}
+ {description ??
+ tct(
+ 'The Sentry GitHub App does not have sufficient permissions to launch a coding agent. Please update your [link:GitHub App installation settings] to grant the required permissions.',
+ {
+ link: ,
+ }
+ )}
{t('Remind me later')}
-
+ {
+ onUpdatePermissionsClick?.();
+ closeModal();
+ }}
+ >
{t('Update Permissions')}
diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx
index 6e8472964b55..6b5322cf060e 100644
--- a/static/app/components/events/autofix/useExplorerAutofix.tsx
+++ b/static/app/components/events/autofix/useExplorerAutofix.tsx
@@ -23,6 +23,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {defined} from 'sentry/utils/defined';
+import {getGithubPermissionsUpdateUrl} from 'sentry/utils/integrationUtil';
import {useApi} from 'sentry/utils/useApi';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useUser} from 'sentry/utils/useUser';
@@ -148,6 +149,11 @@ export interface ExplorerAutofixState {
} | null;
queued_feedback?: RawFeedback[];
repo_pr_states?: Record;
+ warnings?: Array<{
+ warning_type: string;
+ installation_id?: string;
+ repo_name?: string;
+ }>;
}
/**
@@ -736,7 +742,7 @@ export function useExplorerAutofix(
if (permissionFailures.length > 0) {
const installationId = permissionFailures[0]?.github_installation_id;
const installationUrl = installationId
- ? `https://github.com/settings/installations/${installationId}`
+ ? getGithubPermissionsUpdateUrl(installationId)
: undefined;
openModal(deps => (
setCodingAgentErrors(prev => prev.filter(e => e.id !== id)),
+ warnings: runState?.warnings ?? [],
};
}
diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx
index 13d6283c302e..d4472579b7c3 100644
--- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx
+++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx
@@ -110,6 +110,7 @@ const mockAutofix: ReturnType = {
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
+ warnings: [],
};
const mockAutofixWithRunState: ReturnType = {
diff --git a/static/app/components/events/autofix/v3/drawer.spec.tsx b/static/app/components/events/autofix/v3/drawer.spec.tsx
new file mode 100644
index 000000000000..b68412de452b
--- /dev/null
+++ b/static/app/components/events/autofix/v3/drawer.spec.tsx
@@ -0,0 +1,52 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {AutofixWarnings} from 'sentry/components/events/autofix/v3/drawer';
+
+describe('AutofixWarnings', () => {
+ const organization = OrganizationFixture();
+
+ it('deduplicates repo names', () => {
+ render(
+ ,
+ {organization}
+ );
+
+ expect(screen.getAllByText('getsentry/sentry')).toHaveLength(1);
+ expect(screen.getByText(/The configured GitHub App for/)).toBeInTheDocument();
+ });
+
+ it('renders fallback copy when repo names are missing', () => {
+ render(
+ ,
+ {organization}
+ );
+
+ expect(
+ screen.getByText(
+ 'The configured GitHub App is missing permissions. Update the app and ask Seer to retry.'
+ )
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/The configured GitHub App for/)).not.toBeInTheDocument();
+ });
+});
diff --git a/static/app/components/events/autofix/v3/drawer.tsx b/static/app/components/events/autofix/v3/drawer.tsx
index c54de4f5124f..6c06aa5b58c0 100644
--- a/static/app/components/events/autofix/v3/drawer.tsx
+++ b/static/app/components/events/autofix/v3/drawer.tsx
@@ -1,7 +1,12 @@
-import {useCallback, useMemo, useRef} from 'react';
+import {Fragment, useCallback, useMemo, useRef} from 'react';
+import {Alert} from '@sentry/scraps/alert';
+import {Button, LinkButton} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
+import {ExternalLink} from '@sentry/scraps/link';
+import {useModal} from '@sentry/scraps/modal';
+import {AutofixGithubAppPermissionsModal} from 'sentry/components/events/autofix/autofixGithubAppPermissionsModal';
import {getReferrerFromBlocks} from 'sentry/components/events/autofix/autofixReferrer';
import {
getAutofixArtifactFromSection,
@@ -13,12 +18,15 @@ import {SeerDrawerContent} from 'sentry/components/events/autofix/v3/content';
import {SeerDrawerHeader} from 'sentry/components/events/autofix/v3/header';
import {artifactToMarkdown} from 'sentry/components/events/autofix/v3/utils';
import {Placeholder} from 'sentry/components/placeholder';
-import {t} from 'sentry/locale';
+import {IconClose} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
import type {Group} from 'sentry/types/group';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils/defined';
+import {getGithubPermissionsUpdateUrl} from 'sentry/utils/integrationUtil';
import {useAutoScroll} from 'sentry/utils/useAutoScroll';
import {useCopyToClipboard} from 'sentry/utils/useCopyToClipboard';
+import {useDismissAlert} from 'sentry/utils/useDismissAlert';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useAiConfig} from 'sentry/views/issueDetails/hooks/useAiConfig';
import {useSeerExplorerDrawer} from 'sentry/views/seerExplorer/components/drawer/useSeerExplorerDrawer';
@@ -72,6 +80,7 @@ export function SeerDrawer({group, project}: SeerDrawerProps) {
onReset={handleRestart}
referrer={referrer}
/>
+
{aiConfig.isAutofixSetupLoading ? (
@@ -138,3 +147,126 @@ function useHandleOpenSeerAgent({
return () => openSeerExplorerDrawer({runId});
}, [openSeerExplorerDrawer, runId]);
}
+
+type AutofixWarning = {
+ warning_type: string;
+ installation_id?: string;
+ repo_name?: string;
+};
+
+function InstallationPermissionsButton({installationId}: {installationId: string}) {
+ const {openModal} = useModal();
+ const installationUrl = getGithubPermissionsUpdateUrl(installationId);
+
+ return (
+
+ openModal(deps => (
+ }
+ )}
+ />
+ ))
+ }
+ >
+ {t('Update Permissions')}
+
+ );
+}
+
+function ConfigurationPermissionsButton() {
+ const organization = useOrganization();
+ const configurationUrl = `/settings/${organization.slug}/integrations/github/?tab=configurations`;
+
+ return (
+
+ {t('Update Permissions')}
+
+ );
+}
+
+export function AutofixWarnings({
+ warnings,
+ groupId,
+}: {
+ groupId: string;
+ warnings: AutofixWarning[];
+}) {
+ const organization = useOrganization();
+ const {dismiss, isDismissed} = useDismissAlert({
+ key: `${organization.id}:${groupId}:autofix-github-permissions-warning`,
+ expirationDays: 7,
+ });
+
+ if (!warnings.length || isDismissed) {
+ return null;
+ }
+
+ const permissionWarnings = warnings.filter(
+ w => w.warning_type === 'github_app_permissions'
+ );
+
+ if (!permissionWarnings.length) {
+ return null;
+ }
+
+ const installationIds = [
+ ...new Set(permissionWarnings.map(w => w.installation_id).filter(defined)),
+ ];
+ const [installationId] = installationIds;
+
+ const comp =
+ installationIds.length === 1 && defined(installationId) ? (
+
+ ) : (
+
+ );
+
+ const repoNames = [
+ ...new Set(permissionWarnings.map(w => w.repo_name).filter(defined)),
+ ];
+
+ const repoNamesNode = repoNames.map((repoName, index) => (
+
+ {index > 0 && ', '}
+ {repoName}
+
+ ));
+
+ return (
+
+
+ {comp}
+ }
+ variant="transparent"
+ size="xs"
+ aria-label={t('Dismiss')}
+ onClick={dismiss}
+ />
+
+ }
+ >
+ {repoNames.length
+ ? tct(
+ 'The configured GitHub App for [repoNames] is missing permissions. Update the app and ask Seer to retry.',
+ {
+ repoNames: repoNamesNode,
+ }
+ )
+ : t(
+ 'The configured GitHub App is missing permissions. Update the app and ask Seer to retry.'
+ )}
+
+
+ );
+}
diff --git a/static/app/components/events/autofix/v3/nextStep.spec.tsx b/static/app/components/events/autofix/v3/nextStep.spec.tsx
index 815bbc4a4cbd..6b653d1f4360 100644
--- a/static/app/components/events/autofix/v3/nextStep.spec.tsx
+++ b/static/app/components/events/autofix/v3/nextStep.spec.tsx
@@ -26,6 +26,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
+ warnings: [],
isLoading: false,
isPolling: false,
};
diff --git a/static/app/components/events/autofix/v3/prIterationFeedbackForm.spec.tsx b/static/app/components/events/autofix/v3/prIterationFeedbackForm.spec.tsx
index 881d843ce67b..17f5812e6147 100644
--- a/static/app/components/events/autofix/v3/prIterationFeedbackForm.spec.tsx
+++ b/static/app/components/events/autofix/v3/prIterationFeedbackForm.spec.tsx
@@ -19,6 +19,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
+ warnings: [],
isLoading: false,
isPolling: false,
...overrides,
diff --git a/static/app/components/events/autofix/v3/useAutoTriggerAutofix.spec.tsx b/static/app/components/events/autofix/v3/useAutoTriggerAutofix.spec.tsx
index 23043d955dfc..e5d28f2c4643 100644
--- a/static/app/components/events/autofix/v3/useAutoTriggerAutofix.spec.tsx
+++ b/static/app/components/events/autofix/v3/useAutoTriggerAutofix.spec.tsx
@@ -16,6 +16,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
+ warnings: [],
isLoading: false,
isPolling: false,
};
diff --git a/static/app/components/events/autofix/v3/useResetAutofixStep.spec.tsx b/static/app/components/events/autofix/v3/useResetAutofixStep.spec.tsx
index 44813933786e..ed587513f702 100644
--- a/static/app/components/events/autofix/v3/useResetAutofixStep.spec.tsx
+++ b/static/app/components/events/autofix/v3/useResetAutofixStep.spec.tsx
@@ -17,6 +17,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
+ warnings: [],
isLoading: false,
isPolling: false,
};
diff --git a/static/app/utils/integrationUtil.tsx b/static/app/utils/integrationUtil.tsx
index d917d57f7f13..7cbb6bef9532 100644
--- a/static/app/utils/integrationUtil.tsx
+++ b/static/app/utils/integrationUtil.tsx
@@ -301,6 +301,13 @@ const isSlackIntegrationUpToDate = (integrations: Integration[]): boolean => {
export const integrationRequiresUpgrade = (integration: Integration): boolean =>
!isIntegrationUpToDate(integration);
+/**
+ * URL where a user can review and accept a GitHub App installation's updated
+ * permissions. Mirrors `_build_permissions_update_url` on the backend.
+ */
+export const getGithubPermissionsUpdateUrl = (installationId: string): string =>
+ `https://github.com/settings/installations/${installationId}/permissions/update`;
+
export const canManageIntegrations = (organization: Organization): boolean =>
isActiveSuperuser() || hasEveryAccess(['org:integrations'], {organization});