Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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/';
Expand All @@ -20,6 +22,8 @@ export function AutofixGithubAppPermissionsModal({
Footer,
closeModal,
installationUrl,
description,
onUpdatePermissionsClick,
}: AutofixGithubAppPermissionsModalProps) {
const settingsUrl = installationUrl ?? DEFAULT_INSTALLATIONS_URL;

Expand All @@ -30,18 +34,27 @@ export function AutofixGithubAppPermissionsModal({
</Header>
<Body>
<Text as="p">
{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: <ExternalLink href={settingsUrl} />,
}
)}
{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: <ExternalLink href={settingsUrl} />,
}
)}
</Text>
</Body>
<Footer>
<Grid flow="column" align="center" gap="md">
<Button onClick={closeModal}>{t('Remind me later')}</Button>
<LinkButton href={settingsUrl} external variant="primary">
<LinkButton
href={settingsUrl}
external
variant="primary"
onClick={() => {
onUpdatePermissionsClick?.();
closeModal();
}}
>
{t('Update Permissions')}
</LinkButton>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -148,6 +149,11 @@ export interface ExplorerAutofixState {
} | null;
queued_feedback?: RawFeedback[];
repo_pr_states?: Record<string, RepoPRState>;
warnings?: Array<{
warning_type: string;
installation_id?: string;
repo_name?: string;
}>;
}

/**
Expand Down Expand Up @@ -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 => (
<AutofixGithubAppPermissionsModal
Expand Down Expand Up @@ -839,6 +845,7 @@ export function useExplorerAutofix(
*/
dismissCodingAgentError: (id: number) =>
setCodingAgentErrors(prev => prev.filter(e => e.id !== id)),
warnings: runState?.warnings ?? [],
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const mockAutofix: ReturnType<typeof useExplorerAutofix> = {
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
warnings: [],
};

const mockAutofixWithRunState: ReturnType<typeof useExplorerAutofix> = {
Expand Down
52 changes: 52 additions & 0 deletions static/app/components/events/autofix/v3/drawer.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(
<AutofixWarnings
groupId="1"
warnings={[
{
warning_type: 'github_app_permissions',
repo_name: 'getsentry/sentry',
},
{
warning_type: 'github_app_permissions',
repo_name: 'getsentry/sentry',
},
]}
/>,
{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(
<AutofixWarnings
groupId="1"
warnings={[
{
warning_type: 'github_app_permissions',
},
]}
/>,
{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();
});
});
136 changes: 134 additions & 2 deletions static/app/components/events/autofix/v3/drawer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -72,6 +80,7 @@ export function SeerDrawer({group, project}: SeerDrawerProps) {
onReset={handleRestart}
referrer={referrer}
/>
<AutofixWarnings warnings={aiAutofix.warnings} groupId={group.id} />
<SeerDrawerBody ref={containerRef} onScroll={onScrollHandler}>
{aiConfig.isAutofixSetupLoading ? (
<Flex data-test-id="ai-setup-loading-indicator" direction="column" gap="xl">
Expand Down Expand Up @@ -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 (
<Button
variant="primary"
size="xs"
onClick={() =>
openModal(deps => (
<AutofixGithubAppPermissionsModal
{...deps}
installationUrl={installationUrl}
description={tct(
'Seer had trouble talking to GitHub while running Autofix. Please update your [link:GitHub App installation settings] to grant the required permissions.',
{link: <ExternalLink href={installationUrl} />}
)}
/>
))
}
>
{t('Update Permissions')}
</Button>
);
}

function ConfigurationPermissionsButton() {
const organization = useOrganization();
const configurationUrl = `/settings/${organization.slug}/integrations/github/?tab=configurations`;

return (
<LinkButton to={configurationUrl} variant="primary" size="xs">
{t('Update Permissions')}
</LinkButton>
Comment thread
cursor[bot] marked this conversation as resolved.
);
}

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissal hides subsequent permission warnings

Medium Severity

AutofixWarnings stores dismissal for seven days under a fixed key per org and issue, and returns nothing whenever isDismissed is true. After the user dismisses once, later github_app_permissions warnings from polling (extra repos or a new failure after warnings cleared) stay hidden until expiry, so the nudge can miss active permission problems.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e88e2ca. Configure here.

}

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) ? (
<InstallationPermissionsButton installationId={installationId} />
) : (
<ConfigurationPermissionsButton />
);
Comment thread
cursor[bot] marked this conversation as resolved.

const repoNames = [
...new Set(permissionWarnings.map(w => w.repo_name).filter(defined)),
];

const repoNamesNode = repoNames.map((repoName, index) => (
<Fragment key={repoName}>
{index > 0 && ', '}
<code>{repoName}</code>
</Fragment>
));
Comment thread
sentry[bot] marked this conversation as resolved.

return (
<Flex direction="column" gap="md" padding="md 2xl 0">
<Alert
variant="warning"
trailingItems={
<Flex gap="sm" alignSelf="center">
{comp}
<Button
icon={<IconClose />}
variant="transparent"
size="xs"
aria-label={t('Dismiss')}
onClick={dismiss}
/>
</Flex>
}
>
{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.'
)}
</Alert>
</Flex>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
warnings: [],
isLoading: false,
isPolling: false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
warnings: [],
isLoading: false,
isPolling: false,
...overrides,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
warnings: [],
isLoading: false,
isPolling: false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function makeAutofix(
triggerCodingAgentHandoff: jest.fn(),
codingAgentErrors: [],
dismissCodingAgentError: jest.fn(),
warnings: [],
isLoading: false,
isPolling: false,
};
Expand Down
7 changes: 7 additions & 0 deletions static/app/utils/integrationUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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});

Expand Down
Loading