From d0a92ef3e662fdcecf2f86dae6e78a5a8a12c4ce Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 28 Sep 2026 14:03:56 -0700 Subject: [PATCH] ref(seer): Route AI gates through the Seer access helpers Move call sites that inlined the gen-ai-features and hide_ai_features check onto has_seer_access on the backend and areAiFeaturesAllowed on the frontend, so they share one definition of Seer access ahead of removing the flag. No behavior change on SaaS; on self-hosted these sites already evaluated false because the flag is unset there. --- src/sentry/integrations/utils/external_issues.py | 5 ++--- src/sentry/seer/code_review/preflight.py | 9 ++------- src/sentry/tasks/llm_issue_detection/detection.py | 7 ++----- src/sentry/tasks/web_vitals_issue_detection.py | 9 +++------ src/sentry/testutils/helpers/github.py | 2 ++ static/app/components/group/issueSeerBadge.tsx | 5 ++--- static/app/components/groupMetaRow.tsx | 5 ++--- static/app/components/searchQueryBuilder/context.tsx | 5 ++--- static/app/views/dashboards/manage/index.tsx | 6 ++---- .../components/uptime/useUptimeAssertionFeatures.tsx | 4 ++-- static/app/views/issueDetails/autofix/index.tsx | 7 ++----- static/app/views/issueDetails/eventNavigation/index.tsx | 5 ++--- static/app/views/issueDetails/header/seerBadge.tsx | 7 ++----- static/app/views/issueDetails/sidebar/seerDrawer.tsx | 6 ++---- static/app/views/issueDetails/sidebar/sidebar.tsx | 4 ++-- .../views/issueList/pages/useInboxPreviewPrefetch.tsx | 4 ++-- static/app/views/seerExplorer/utils.tsx | 7 +++---- .../seerWorkflows/overview/useOverviewSeerDrawer.tsx | 4 ++-- static/app/views/settings/projectPerformance/index.tsx | 4 ++-- .../integrations/cursor_origin/test_code_review.py | 2 ++ tests/sentry/integrations/utils/test_external_issues.py | 2 ++ tests/sentry/seer/code_review/test_preflight.py | 3 +++ .../seer/code_review/webhooks/test_merge_request.py | 3 +++ .../seer/code_review/webhooks/test_review_request.py | 2 ++ tests/sentry/tasks/test_web_vitals_issue_detection.py | 2 ++ 25 files changed, 54 insertions(+), 65 deletions(-) diff --git a/src/sentry/integrations/utils/external_issues.py b/src/sentry/integrations/utils/external_issues.py index 122a546f4c27..1f0c5b40df57 100644 --- a/src/sentry/integrations/utils/external_issues.py +++ b/src/sentry/integrations/utils/external_issues.py @@ -5,6 +5,7 @@ from sentry import features from sentry.models.group import Group +from sentry.seer.seer_setup import has_seer_access from sentry.seer.signed_seer_api import ( LlmGenerateRequest, SeerViewerContext, @@ -123,9 +124,7 @@ def maybe_generate_external_issue_details( ) -> GeneratedExternalIssueDetails: organization = group.organization empty_result = GeneratedExternalIssueDetails(title=None, description=None) - if not features.has("organizations:gen-ai-features", organization, actor=user): - return empty_result - if organization.get_option("sentry:hide_ai_features", False): + if not has_seer_access(organization, actor=user): return empty_result if not features.has("organizations:external-issues-ai-generate", organization, actor=user): return empty_result diff --git a/src/sentry/seer/code_review/preflight.py b/src/sentry/seer/code_review/preflight.py index 440b9a2cefd1..0254cbf1c853 100644 --- a/src/sentry/seer/code_review/preflight.py +++ b/src/sentry/seer/code_review/preflight.py @@ -9,7 +9,6 @@ from sentry import features, options, quotas from sentry.constants import ( - HIDE_AI_FEATURES_DEFAULT, DataCategory, ) from sentry.integrations.services.integration.model import RpcIntegration @@ -21,6 +20,7 @@ CodeReviewSettings, RepositorySettings, ) +from sentry.seer.seer_setup import has_seer_access class PreflightDenialReason(StrEnum): @@ -78,12 +78,7 @@ def check(self) -> CodeReviewPreflightResult: # ------------------------------------------------------------------------- def _check_legal_ai_consent(self) -> PreflightDenialReason | None: - has_gen_ai_flag = features.has("organizations:gen-ai-features", self.organization) - has_hidden_ai = self.organization.get_option( - "sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT - ) - - if not has_gen_ai_flag or has_hidden_ai: + if not has_seer_access(self.organization): return PreflightDenialReason.ORG_LEGAL_AI_CONSENT_NOT_GRANTED return None diff --git a/src/sentry/tasks/llm_issue_detection/detection.py b/src/sentry/tasks/llm_issue_detection/detection.py index 6952b2731409..df487ec5a0b2 100644 --- a/src/sentry/tasks/llm_issue_detection/detection.py +++ b/src/sentry/tasks/llm_issue_detection/detection.py @@ -29,6 +29,7 @@ from sentry.models.project import Project from sentry.net.http import connection_from_url from sentry.seer.agent.utils import normalize_description +from sentry.seer.seer_setup import has_seer_access from sentry.seer.signed_seer_api import SeerViewerContext, make_signed_seer_api_request from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import issues_tasks @@ -255,11 +256,7 @@ def _is_org_eligible(org_id: int) -> bool: org = Organization.objects.get_from_cache(id=org_id) except Organization.DoesNotExist: return False - return ( - features.has("organizations:ai-issue-detection", org) - and features.has("organizations:gen-ai-features", org) - and not org.get_option("sentry:hide_ai_features") - ) + return features.has("organizations:ai-issue-detection", org) and has_seer_access(org) @instrumented_task( diff --git a/src/sentry/tasks/web_vitals_issue_detection.py b/src/sentry/tasks/web_vitals_issue_detection.py index 1a853176a653..179edd080e99 100644 --- a/src/sentry/tasks/web_vitals_issue_detection.py +++ b/src/sentry/tasks/web_vitals_issue_detection.py @@ -5,7 +5,7 @@ from collections.abc import Generator from datetime import UTC, datetime, timedelta -from sentry import features, options +from sentry import options from sentry.constants import ObjectStatus from sentry.issue_detection.performance_detection import get_merged_settings from sentry.models.project import Project @@ -13,7 +13,7 @@ from sentry.search.events.types import SnubaParams from sentry.seer.agent.utils import normalize_description from sentry.seer.autofix.utils import get_autofix_repos_from_project_code_mappings -from sentry.seer.seer_setup import get_supported_scm_providers +from sentry.seer.seer_setup import get_supported_scm_providers, has_seer_access from sentry.snuba.referrer import Referrer from sentry.snuba.spans_rpc import Spans from sentry.tasks.base import instrumented_task @@ -340,10 +340,7 @@ def check_seer_setup_for_project(project: Project) -> bool: Checks if a project and it's organization have the necessary Seer setup to detect web vitals issues. The project must have seer feature flags, seer acknowledgement, and a supported SCM code mapping. """ - if not features.has("organizations:gen-ai-features", project.organization): - return False - - if project.organization.get_option("sentry:hide_ai_features"): + if not has_seer_access(project.organization): return False repos = get_autofix_repos_from_project_code_mappings(project) diff --git a/src/sentry/testutils/helpers/github.py b/src/sentry/testutils/helpers/github.py index e8a837581001..4ad4c8046938 100644 --- a/src/sentry/testutils/helpers/github.py +++ b/src/sentry/testutils/helpers/github.py @@ -12,6 +12,7 @@ import orjson from django.http.response import HttpResponseBase +from django.test import override_settings from sentry import options from sentry.integrations.github.webhook import GitHubIntegrationsWebhookEndpoint @@ -140,6 +141,7 @@ def send_github_webhook_event( ) +@override_settings(SENTRY_SELF_HOSTED=False) class GitHubWebhookCodeReviewTestCase(GitHubWebhookTestCase): # Code review features are org features as set in options automator CODE_REVIEW_FEATURES = {"organizations:gen-ai-features", "organizations:code-review-beta"} diff --git a/static/app/components/group/issueSeerBadge.tsx b/static/app/components/group/issueSeerBadge.tsx index 761a61ebf91d..d779ff514aff 100644 --- a/static/app/components/group/issueSeerBadge.tsx +++ b/static/app/components/group/issueSeerBadge.tsx @@ -10,6 +10,7 @@ import { import {IconSeer} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {makeSeerLocation} from 'sentry/views/issueDetails/autofix/utils'; @@ -25,9 +26,7 @@ export function IssueSeerBadge({group}: IssueSeerBadgeProps) { const autofixRunExists = getAutofixRunExists(group); const seerFixable = isIssueQuickFixable(group); const showSeer = - organization.features.includes('gen-ai-features') && - !organization.hideAiFeatures && - (autofixRunExists || seerFixable); + areAiFeaturesAllowed(organization) && (autofixRunExists || seerFixable); let seerTitle = null; if (autofixRunExists && seerFixable) { diff --git a/static/app/components/groupMetaRow.tsx b/static/app/components/groupMetaRow.tsx index 5444d0a967bd..fcbf63b4c7da 100644 --- a/static/app/components/groupMetaRow.tsx +++ b/static/app/components/groupMetaRow.tsx @@ -18,6 +18,7 @@ import {IconChat} from 'sentry/icons'; import type {Group} from 'sentry/types/group'; import {getTitle} from 'sentry/utils/events'; import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -73,9 +74,7 @@ export function GroupMetaRow({data, showLifetime = true}: Props) { const autofixRunExists = getAutofixRunExists(data); const seerFixable = isIssueQuickFixable(data); const showSeer = - organization.features.includes('gen-ai-features') && - !organization.hideAiFeatures && - (autofixRunExists || seerFixable); + areAiFeaturesAllowed(organization) && (autofixRunExists || seerFixable); const {subtitle} = getTitle(data); diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index 3feff938941c..7f3aceeb26d1 100644 --- a/static/app/components/searchQueryBuilder/context.tsx +++ b/static/app/components/searchQueryBuilder/context.tsx @@ -35,6 +35,7 @@ import type {SavedSearchType, TagCollection} from 'sentry/types/group'; import {defined} from 'sentry/utils/defined'; import {getFieldDefinition as defaultGetFieldDefinition} from 'sentry/utils/fields'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useDimensions} from 'sentry/utils/useDimensions'; import {useOrganization} from 'sentry/utils/useOrganization'; import {usePrevious} from 'sentry/utils/usePrevious'; @@ -229,9 +230,7 @@ export function SearchQueryBuilderProvider({ const organization = useOrganization(); const enableAISearch = - Boolean(enableAISearchProp) && - !organization.hideAiFeatures && - organization.features.includes('gen-ai-features'); + Boolean(enableAISearchProp) && areAiFeaturesAllowed(organization); const defaultToAskSeerOnFreeTextSearch = enableAISearch && Boolean(defaultToAskSeerOnFreeTextSearchProp); diff --git a/static/app/views/dashboards/manage/index.tsx b/static/app/views/dashboards/manage/index.tsx index a3a1a973b3e1..8ae57825cbb6 100644 --- a/static/app/views/dashboards/manage/index.tsx +++ b/static/app/views/dashboards/manage/index.tsx @@ -28,6 +28,7 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; import {dashboardsApiOptions} from 'sentry/utils/dashboards/dashboardsApiOptions'; import {decodeScalar} from 'sentry/utils/queryString'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useApi} from 'sentry/utils/useApi'; import {useHasProjectAccess} from 'sentry/utils/useHasProjectAccess'; @@ -111,9 +112,6 @@ function ManageDashboards() { ? CUSTOM_DASHBOARD_LABEL : t('All Dashboards'); - const areAiFeaturesAllowed = - !organization.hideAiFeatures && organization.features.includes('gen-ai-features'); - const {hasProjectAccess, projectsLoaded} = useHasProjectAccess(); const hasUserLastVisited = organization.features.includes( @@ -283,7 +281,7 @@ function ManageDashboards() { position="bottom-end" data-test-id="sort-by-select" /> - {areAiFeaturesAllowed ? ( + {areAiFeaturesAllowed(organization) ? ( {({ hasReachedDashboardLimit, diff --git a/static/app/views/detectors/components/uptime/useUptimeAssertionFeatures.tsx b/static/app/views/detectors/components/uptime/useUptimeAssertionFeatures.tsx index 8c2f2945007d..487abd901203 100644 --- a/static/app/views/detectors/components/uptime/useUptimeAssertionFeatures.tsx +++ b/static/app/views/detectors/components/uptime/useUptimeAssertionFeatures.tsx @@ -1,3 +1,4 @@ +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; export function useUptimeAssertionFeatures() { @@ -5,8 +6,7 @@ export function useUptimeAssertionFeatures() { const hasAiAssertionSuggestions = organization.features.includes('uptime-ai-assertion-suggestions') && - organization.features.includes('gen-ai-features') && - !organization.hideAiFeatures; + areAiFeaturesAllowed(organization); return {hasRuntimeAssertions: true, hasAiAssertionSuggestions}; } diff --git a/static/app/views/issueDetails/autofix/index.tsx b/static/app/views/issueDetails/autofix/index.tsx index 27d8acf7c96b..7d5a110301bd 100644 --- a/static/app/views/issueDetails/autofix/index.tsx +++ b/static/app/views/issueDetails/autofix/index.tsx @@ -10,6 +10,7 @@ import {Redirect} from 'sentry/components/redirect'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useAutofixPanel} from 'sentry/views/issueDetails/autofix/context'; import {hasAutofixPage} from 'sentry/views/issueDetails/autofix/utils'; @@ -28,11 +29,7 @@ export default function GroupAutofix() { // The same three conditions the Seer drawer refuses to open under. Sending // people back to the issue keeps a shared or bookmarked URL from dead-ending. - if ( - !hasAutofixPage(organization) || - !organization.features.includes('gen-ai-features') || - organization.hideAiFeatures - ) { + if (!hasAutofixPage(organization) || !areAiFeaturesAllowed(organization)) { return ; } diff --git a/static/app/views/issueDetails/eventNavigation/index.tsx b/static/app/views/issueDetails/eventNavigation/index.tsx index 249cea9d1e5b..27cc48cb0f3e 100644 --- a/static/app/views/issueDetails/eventNavigation/index.tsx +++ b/static/app/views/issueDetails/eventNavigation/index.tsx @@ -26,6 +26,7 @@ import {SavedQueryDatasets} from 'sentry/utils/discover/types'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; import {parseLinkHeader} from 'sentry/utils/parseLinkHeader'; import {useReplayCountForIssues} from 'sentry/utils/replayCount/useReplayCountForIssues'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; @@ -96,9 +97,7 @@ export function IssueEventNavigation({event, group}: IssueEventNavigationProps) // `autofix-page` rolls out with Seer, so the orgs that hide AI keep the // dropdown rather than getting the tab list ahead of everyone else. const showContentTabs = - hasAutofixPage(organization) && - organization.features.includes('gen-ai-features') && - !organization.hideAiFeatures; + hasAutofixPage(organization) && areAiFeaturesAllowed(organization); // Autofix does not run on some issue types or on sample events, so those // issues get the tab list without an Autofix tab. diff --git a/static/app/views/issueDetails/header/seerBadge.tsx b/static/app/views/issueDetails/header/seerBadge.tsx index be38b2d28d8f..be2454e24627 100644 --- a/static/app/views/issueDetails/header/seerBadge.tsx +++ b/static/app/views/issueDetails/header/seerBadge.tsx @@ -7,6 +7,7 @@ import {isIssueQuickFixable} from 'sentry/components/events/autofix/utils'; import {IconSeer} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; import {Divider} from 'sentry/views/issueDetails/divider'; @@ -14,11 +15,7 @@ export function SeerBadge({group}: {group: Group}) { const organization = useOrganization(); const seerFixable = isIssueQuickFixable(group); - if ( - !organization.features.includes('gen-ai-features') || - organization.hideAiFeatures || - !seerFixable - ) { + if (!areAiFeaturesAllowed(organization) || !seerFixable) { return null; } diff --git a/static/app/views/issueDetails/sidebar/seerDrawer.tsx b/static/app/views/issueDetails/sidebar/seerDrawer.tsx index 9e91b1897abe..233ac081ca3c 100644 --- a/static/app/views/issueDetails/sidebar/seerDrawer.tsx +++ b/static/app/views/issueDetails/sidebar/seerDrawer.tsx @@ -7,6 +7,7 @@ import {SeerDrawer} from 'sentry/components/events/autofix/v3/drawer'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; import type {Project} from 'sentry/types/project'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -27,10 +28,7 @@ export const useOpenSeerDrawer = ({group, project}: {group: Group; project: Proj const location = useLocation(); const openSeerDrawer = useCallback(() => { - if ( - !organization.features.includes('gen-ai-features') || - organization.hideAiFeatures - ) { + if (!areAiFeaturesAllowed(organization)) { return; } diff --git a/static/app/views/issueDetails/sidebar/sidebar.tsx b/static/app/views/issueDetails/sidebar/sidebar.tsx index ecd76c9160be..6a7e773394b1 100644 --- a/static/app/views/issueDetails/sidebar/sidebar.tsx +++ b/static/app/views/issueDetails/sidebar/sidebar.tsx @@ -14,6 +14,7 @@ import type {Group, TeamParticipant, UserParticipant} from 'sentry/types/group'; import type {Project} from 'sentry/types/project'; import {DemoTourStep, SharedTourElement} from 'sentry/utils/demoMode/demoTours'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useUser} from 'sentry/utils/useUser'; import {ActivitySection} from 'sentry/views/issueDetails/activitySection'; @@ -60,8 +61,7 @@ export function IssueDetailsSidebar({group, event, project}: Props) { // Check if Seer (AI features) will be shown - must match SeerSection's logic // SeerSection shows "Seer" title when the issue type supports it AND AI features are allowed const hasSeerFeatures = - organization.features.includes('gen-ai-features') && - !organization.hideAiFeatures && + areAiFeaturesAllowed(organization) && (issueTypeConfig.issueSummary.enabled || issueTypeConfig.autofix); const showSeerSection = hasSeerFeatures || issueTypeConfig.resources; diff --git a/static/app/views/issueList/pages/useInboxPreviewPrefetch.tsx b/static/app/views/issueList/pages/useInboxPreviewPrefetch.tsx index e0c0d8fa1251..aa2053f15c38 100644 --- a/static/app/views/issueList/pages/useInboxPreviewPrefetch.tsx +++ b/static/app/views/issueList/pages/useInboxPreviewPrefetch.tsx @@ -7,6 +7,7 @@ import {explorerAutofixApiOptions} from 'sentry/components/events/autofix/useExp import {linkedPullRequestsApiOptions} from 'sentry/components/group/externalIssuesList/linkedPullRequests'; import type {Group} from 'sentry/types/group'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; import {groupApiOptions} from 'sentry/views/issueDetails/useGroup'; import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils'; @@ -41,8 +42,7 @@ export function useInboxPreviewPrefetch(group: Group) { }); const shouldPrefetchAutofix = - !organization.hideAiFeatures && - organization.features.includes('gen-ai-features') && + areAiFeaturesAllowed(organization) && getConfigForIssueType(group, group.project).autofix; if (shouldPrefetchAutofix) { void queryClient.prefetchQuery({ diff --git a/static/app/views/seerExplorer/utils.tsx b/static/app/views/seerExplorer/utils.tsx index b2e33ca26187..7b118fad302a 100644 --- a/static/app/views/seerExplorer/utils.tsx +++ b/static/app/views/seerExplorer/utils.tsx @@ -20,6 +20,7 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import type {ApiQueryKey} from 'sentry/utils/api/apiQueryKey'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {getRouteStringFromRoutes} from 'sentry/utils/getRouteStringFromRoutes'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {isUUID} from 'sentry/utils/string/isUUID'; import {useLocation} from 'sentry/utils/useLocation'; import {useMedia} from 'sentry/utils/useMedia'; @@ -853,9 +854,8 @@ export function getExplorerFeedbackOptions( /** * Checks if Seer Explorer is enabled for the organization. * Requires the rollout flag and: - * - 'gen-ai-features' feature flag + * - AI features allowed for the organization (see areAiFeaturesAllowed) * - Organization has not disabled open membership - * - Organization has not disabled AI features (hideAiFeatures is false) */ export function isSeerExplorerEnabled(organization: Organization | null): boolean { if (!organization) { @@ -864,8 +864,7 @@ export function isSeerExplorerEnabled(organization: Organization | null): boolea return ( organization.openMembership && - !organization.hideAiFeatures && - organization.features.includes('gen-ai-features') && + areAiFeaturesAllowed(organization) && organization.features.includes('seer-explorer') ); } diff --git a/static/app/views/seerWorkflows/overview/useOverviewSeerDrawer.tsx b/static/app/views/seerWorkflows/overview/useOverviewSeerDrawer.tsx index f2243ca5435d..c4f839d214fd 100644 --- a/static/app/views/seerWorkflows/overview/useOverviewSeerDrawer.tsx +++ b/static/app/views/seerWorkflows/overview/useOverviewSeerDrawer.tsx @@ -7,6 +7,7 @@ import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import {decodeScalar} from 'sentry/utils/queryString'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -35,8 +36,7 @@ export function useOverviewSeerDrawer() { }); const openGroupIdRef = useRef(undefined); - const hasSeerAccess = - organization.features.includes('gen-ai-features') && !organization.hideAiFeatures; + const hasSeerAccess = areAiFeaturesAllowed(organization); const groupId = decodeScalar(location.query.seerDrawer); useEffect(() => { diff --git a/static/app/views/settings/projectPerformance/index.tsx b/static/app/views/settings/projectPerformance/index.tsx index 9b3043eb85a9..a9e3104d8cd0 100644 --- a/static/app/views/settings/projectPerformance/index.tsx +++ b/static/app/views/settings/projectPerformance/index.tsx @@ -12,6 +12,7 @@ import {t} from 'sentry/locale'; import type {Scope} from 'sentry/types/core'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import {useDetailedProject} from 'sentry/utils/project/useDetailedProject'; +import {areAiFeaturesAllowed} from 'sentry/utils/seer/areAiFeaturesAllowed'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {useHasSeerWebVitalsSuggestions} from 'sentry/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions'; @@ -65,9 +66,8 @@ export function ProjectPerformance() { const hasWebVitalsSeerSuggestions = useHasSeerWebVitalsSuggestions(project); const hasAIIssueDetection = - organization.features.includes('gen-ai-features') && organization.features.includes('ai-issue-detection') && - !organization.hideAiFeatures; + areAiFeaturesAllowed(organization); const { data: threshold, diff --git a/tests/sentry/integrations/cursor_origin/test_code_review.py b/tests/sentry/integrations/cursor_origin/test_code_review.py index 20d4b9a52b35..d65ddacf35c5 100644 --- a/tests/sentry/integrations/cursor_origin/test_code_review.py +++ b/tests/sentry/integrations/cursor_origin/test_code_review.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest +from django.test import override_settings from sentry.constants import ObjectStatus from sentry.integrations.cursor_origin.code_review import review_event @@ -97,6 +98,7 @@ def test_an_app_author_is_its_own_contributor(self) -> None: @cell_silo_test +@override_settings(SENTRY_SELF_HOSTED=False) class CodeReviewFromWebhookTest(TestCase): @pytest.fixture(autouse=True) def mock_seer_request(self) -> Generator[None]: diff --git a/tests/sentry/integrations/utils/test_external_issues.py b/tests/sentry/integrations/utils/test_external_issues.py index c292abb21b76..357adac5eedd 100644 --- a/tests/sentry/integrations/utils/test_external_issues.py +++ b/tests/sentry/integrations/utils/test_external_issues.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch import pytest +from django.test import override_settings from sentry.integrations.utils.external_issues import ( MAX_CONTEXT_LENGTH, @@ -99,6 +100,7 @@ def test_context_is_truncated(self, mock_request: MagicMock) -> None: assert len(request_body["prompt"]) <= MAX_CONTEXT_LENGTH + len(prompt_prefix) +@override_settings(SENTRY_SELF_HOSTED=False) class GenerateExternalIssueDetailsTest(TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/code_review/test_preflight.py b/tests/sentry/seer/code_review/test_preflight.py index 5492d3d8a5cf..5b3f3905a30e 100644 --- a/tests/sentry/seer/code_review/test_preflight.py +++ b/tests/sentry/seer/code_review/test_preflight.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +from django.test import override_settings + from sentry import features from sentry.integrations.services.integration.serial import serialize_integration from sentry.integrations.utils.hostname import InstanceHostnameError @@ -11,6 +13,7 @@ from sentry.testutils.silo import assume_test_silo_mode +@override_settings(SENTRY_SELF_HOSTED=False) class TestCodeReviewPreflightService(TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/seer/code_review/webhooks/test_merge_request.py b/tests/sentry/seer/code_review/webhooks/test_merge_request.py index c67e73914517..ab38411ec0e5 100644 --- a/tests/sentry/seer/code_review/webhooks/test_merge_request.py +++ b/tests/sentry/seer/code_review/webhooks/test_merge_request.py @@ -4,6 +4,7 @@ import orjson import pytest +from django.test import override_settings from pydantic import ValidationError from scm.types import CreatePullRequestCommentReactionProtocol @@ -65,6 +66,7 @@ def create_pull_request_reaction(self, *args: Any, **kwargs: Any) -> Any: ... def delete_pull_request_reaction(self, *args: Any, **kwargs: Any) -> Any: ... +@override_settings(SENTRY_SELF_HOSTED=False) class _MergeRequestHandlerTestBase(GitLabTestCase): """Shared setup for the GitLab merge-request handler tests. @@ -978,6 +980,7 @@ def _make_note_event(**overrides: object) -> dict[str, Any]: return event +@override_settings(SENTRY_SELF_HOSTED=False) class MergeRequestNoteEventTest(GitLabTestCase): """Tests for the @sentry review note handler. diff --git a/tests/sentry/seer/code_review/webhooks/test_review_request.py b/tests/sentry/seer/code_review/webhooks/test_review_request.py index dd8a1bd02399..a8f707d3ee3b 100644 --- a/tests/sentry/seer/code_review/webhooks/test_review_request.py +++ b/tests/sentry/seer/code_review/webhooks/test_review_request.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from django.test import override_settings from sentry.integrations.services.integration.serial import serialize_integration from sentry.models.repositorysettings import CodeReviewTrigger @@ -34,6 +35,7 @@ def _event(**overrides: object) -> PullRequestReviewEvent: return PullRequestReviewEvent(**fields) # type: ignore[arg-type] +@override_settings(SENTRY_SELF_HOSTED=False) class RequestReviewTest(TestCase): @pytest.fixture(autouse=True) def mock_seer_request(self) -> Generator[None]: diff --git a/tests/sentry/tasks/test_web_vitals_issue_detection.py b/tests/sentry/tasks/test_web_vitals_issue_detection.py index eae31a530a32..6086d7d0eaa0 100644 --- a/tests/sentry/tasks/test_web_vitals_issue_detection.py +++ b/tests/sentry/tasks/test_web_vitals_issue_detection.py @@ -2,6 +2,7 @@ from unittest.mock import patch import pytest +from django.test import override_settings from sentry.issues.grouptype import WebVitalsGroup from sentry.issues.ingest import hash_fingerprint @@ -12,6 +13,7 @@ from sentry.testutils.helpers.task_runner import TaskRunner +@override_settings(SENTRY_SELF_HOSTED=False) class WebVitalsIssueDetectionDataTest(TestCase, SnubaTestCase, SpanTestCase): def setUp(self) -> None: super().setUp()