From 00fb4895b24f05256eb9a44cacbd2f5bd1b4cfe2 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 17 Sep 2026 14:37:38 -0700 Subject: [PATCH 01/10] feat(autofix): Serve Autofix as an issue details tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Autofix moves out of the Seer drawer and onto `/issues//autofix/`, behind `organizations:autofix-page`. The route is registered among the issue tabs rather than beside them, so the page inherits the group header, event details header, page filters, sidebar and group/project context from the issue details route instead of standing them up again. Inside `IssueDetailsColumn` it renders its own section in place of the event details one: the same chrome and tab navigation, minus the event details tour, whose copy is about stack traces and tags that this tab does not show. Every existing "open Seer" entry point now resolves its destination through one helper, so the issue stream badge, the inbox preview, the browser notification and the drawer opener all land on whichever surface the flag selects. Legacy `?seerDrawer=true` links still work: they reach the drawer opener and are forwarded to the tab. The panel body itself is untouched. What the drawer used to own directly — running Autofix, the toolbar handlers, the permission warnings — moved into `useSeerPanel` and `warnings.tsx` so both surfaces share one implementation. Auto-scroll deliberately stayed with the drawer: it needs a bounded scroll container, and the tab scrolls with the page. Two earlier attempts (#124676, #124753) made this a sibling route and had to re-create `PageFiltersContainer`, `GroupIdProvider` and `IssueDetailsContextProvider` by hand; both are closed. Feature flag: `organizations:autofix-page` (already registered). --- .../events/autofix/useExplorerAutofix.tsx | 20 +- .../components/events/autofix/v3/drawer.tsx | 256 ++---------------- .../components/events/autofix/v3/header.tsx | 106 ++++---- .../events/autofix/v3/useSeerPanel.tsx | 127 +++++++++ .../v3/{drawer.spec.tsx => warnings.spec.tsx} | 2 +- .../components/events/autofix/v3/warnings.tsx | 137 ++++++++++ .../components/group/issueSeerBadge.spec.tsx | 40 +++ .../app/components/group/issueSeerBadge.tsx | 7 +- static/app/router/routes.tsx | 4 + .../views/issueDetails/autofix/index.spec.tsx | 134 +++++++++ .../app/views/issueDetails/autofix/index.tsx | 82 ++++++ .../views/issueDetails/autofix/utils.spec.tsx | 54 ++++ .../app/views/issueDetails/autofix/utils.tsx | 62 +++++ .../issueDetails/eventNavigation/index.tsx | 23 ++ .../app/views/issueDetails/groupDetails.tsx | 7 +- .../views/issueDetails/groupDetailsLayout.tsx | 74 +++-- .../views/issueDetails/sidebar/seerDrawer.tsx | 31 ++- static/app/views/issueDetails/types.tsx | 2 + .../pages/inbox/issuePreview/issuePreview.tsx | 23 +- 19 files changed, 843 insertions(+), 348 deletions(-) create mode 100644 static/app/components/events/autofix/v3/useSeerPanel.tsx rename static/app/components/events/autofix/v3/{drawer.spec.tsx => warnings.spec.tsx} (99%) create mode 100644 static/app/components/events/autofix/v3/warnings.tsx create mode 100644 static/app/components/group/issueSeerBadge.spec.tsx create mode 100644 static/app/views/issueDetails/autofix/index.spec.tsx create mode 100644 static/app/views/issueDetails/autofix/index.tsx create mode 100644 static/app/views/issueDetails/autofix/utils.spec.tsx create mode 100644 static/app/views/issueDetails/autofix/utils.tsx diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index 827724a5601e..eff87ced0101 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -29,10 +29,10 @@ 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 {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useUser} from 'sentry/utils/useUser'; +import {makeSeerPathname, makeSeerQuery} from 'sentry/views/issueDetails/autofix/utils'; import {groupQueryKey} from 'sentry/views/issueDetails/useGroup'; import { isArtifact, @@ -866,12 +866,8 @@ export function useExplorerAutofix( issueId: groupId, notification: { navigateTo: { - pathname: normalizeUrl( - `/organizations/${orgSlug}/issues/${groupId}/` - ), - query: { - seerDrawer: 'true', - }, + pathname: makeSeerPathname(organization, groupId), + query: makeSeerQuery(organization), }, project: { avatar: 'https://sentry.io/favicon.ico', // TODO(ryan953): Use the project avatar url or base64 encoded bytes @@ -942,15 +938,7 @@ export function useExplorerAutofix( throw e; } }, - [ - api, - group.shortId, - groupId, - orgSlug, - organization.features, - queryClient, - serviceWorker, - ] + [api, group.shortId, groupId, orgSlug, organization, queryClient, serviceWorker] ); /** diff --git a/static/app/components/events/autofix/v3/drawer.tsx b/static/app/components/events/autofix/v3/drawer.tsx index 8bbba1df0456..483c1cc6ad1e 100644 --- a/static/app/components/events/autofix/v3/drawer.tsx +++ b/static/app/components/events/autofix/v3/drawer.tsx @@ -1,36 +1,14 @@ -import {Fragment, useCallback, useMemo} from 'react'; +import {Stack} from '@sentry/scraps/layout'; -import {Alert} from '@sentry/scraps/alert'; -import {Button, LinkButton} from '@sentry/scraps/button'; -import {Flex, Stack} from '@sentry/scraps/layout'; -import {useModal} from '@sentry/scraps/modal'; - -import {AutofixGithubAppPermissionsModal} from 'sentry/components/events/autofix/autofixGithubAppPermissionsModal'; -import {getReferrerFromBlocks} from 'sentry/components/events/autofix/autofixReferrer'; -import {getAutofixRunId} from 'sentry/components/events/autofix/autofixRunId'; -import { - getAutofixArtifactFromSection, - getOrderedAutofixSections, - useExplorerAutofix, - type AutofixExplorerStep, -} from 'sentry/components/events/autofix/useExplorerAutofix'; import {SeerDrawerBody} from 'sentry/components/events/autofix/v3/body'; import {SeerDrawerContent} from 'sentry/components/events/autofix/v3/content'; import {SeerDrawerHeader} from 'sentry/components/events/autofix/v3/header'; -import {useForceBashMode} from 'sentry/components/events/autofix/v3/useForceBashMode'; -import {artifactToMarkdown} from 'sentry/components/events/autofix/v3/utils'; +import {useSeerPanel} from 'sentry/components/events/autofix/v3/useSeerPanel'; +import {AutofixWarnings} from 'sentry/components/events/autofix/v3/warnings'; import {Placeholder} from 'sentry/components/placeholder'; -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 {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'; interface SeerDrawerProps { group: Group; @@ -38,41 +16,19 @@ interface SeerDrawerProps { } export function SeerDrawer({group, project}: SeerDrawerProps) { - const organization = useOrganization(); - const aiConfig = useAiConfig(group, project); - const aiAutofix = useExplorerAutofix(group, { - // Automated CI iteration pushes commits with no user action, so poll for both. - pollPR: - organization.features.includes('autofix-pr-iteration') || - organization.features.includes('autofix-pr-iteration-manual'), - }); - const [enableBashTools, setEnableBashTools] = useForceBashMode(); - - const autofix = useMemo( - () => ({ - ...aiAutofix, - startStep: ( - step: AutofixExplorerStep, - options?: Parameters['startStep']>[1] - ) => - aiAutofix.startStep(step, { - ...options, - enableBashTools: enableBashTools || undefined, - }), - }), - [aiAutofix, enableBashTools] - ); - - const handleCopyMarkdown = useHandleCopyMarkdown({aiAutofix: autofix}); - const handleRestart = useHandleRestart({aiAutofix: autofix}); - const handleOpenSeerAgent = useHandleOpenSeerAgent({aiAutofix: autofix}); - - const referrer = useMemo( - () => getReferrerFromBlocks(aiAutofix.runState?.blocks ?? []), - [aiAutofix.runState?.blocks] - ); - - const {containerRef, onScrollHandler} = useAutoScroll({key: aiAutofix.runState}); + const { + aiConfig, + autofix, + enableBashTools, + handleCopyMarkdown, + handleOpenSeerAgent, + handleRestart, + referrer, + runState, + setEnableBashTools, + warnings, + } = useSeerPanel({group, project}); + const {containerRef, onScrollHandler} = useAutoScroll({key: runState}); return ( - + {aiConfig.isAutofixSetupLoading ? ( @@ -106,179 +62,3 @@ export function SeerDrawer({group, project}: SeerDrawerProps) { ); } - -function useHandleCopyMarkdown({ - aiAutofix, -}: { - aiAutofix: ReturnType; -}): (() => void) | undefined { - const {copy} = useCopyToClipboard(); - - return useMemo(() => { - if (!aiAutofix.runState) { - return; - } - - return () => { - const markdown = getOrderedAutofixSections(aiAutofix.runState) - .map(getAutofixArtifactFromSection) - .filter(defined) - .map(artifact => artifactToMarkdown(artifact)) - .filter(defined) - .join('\n\n'); - copy(markdown, {successMessage: t('Analysis copied to clipboard.')}); - }; - }, [aiAutofix, copy]); -} - -function useHandleRestart({ - aiAutofix, -}: { - aiAutofix: ReturnType; -}): () => void { - const {startStep} = aiAutofix; - - return useCallback(() => { - startStep('root_cause'); - }, [startStep]); -} - -function useHandleOpenSeerAgent({ - aiAutofix, -}: { - aiAutofix: ReturnType; -}): (() => void) | undefined { - const {openSeerExplorerDrawer} = useSeerExplorerDrawer(); - const runId = getAutofixRunId(aiAutofix.runState); - - return useMemo(() => { - if (!defined(runId)) { - return; - } - return () => openSeerExplorerDrawer({runId}); - }, [openSeerExplorerDrawer, runId]); -} - -type AutofixWarning = { - warning_type: string; - installation_id?: string; - installation_url?: string; - repo_name?: string; -}; - -function InstallationPermissionsButton({installationUrl}: {installationUrl?: string}) { - const {openModal} = useModal(); - return ( - - ); -} - -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) ? ( - w.installation_id === installationId) - ?.installation_url - } - /> - ) : ( - - ); - - const repoNames = [ - ...new Set(permissionWarnings.map(w => w.repo_name).filter(defined)), - ]; - - const repoNamesNode = repoNames.map((repoName, index) => ( - - {index > 0 && ', '} - {repoName} - - )); - - return ( - - - {comp} - + ); +} + +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) ? ( + w.installation_id === installationId) + ?.installation_url + } + /> + ) : ( + + ); + + const repoNames = [ + ...new Set(permissionWarnings.map(w => w.repo_name).filter(defined)), + ]; + + const repoNamesNode = repoNames.map((repoName, index) => ( + + {index > 0 && ', '} + {repoName} + + )); + + return ( + + + {comp} +