From 5d5f86acb1b41f61c61d1a206e1b37f4f0d46aa5 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Fri, 14 Nov 2025 13:09:11 -0500 Subject: [PATCH 1/2] feat(ai): Check gen-ai feature flag before org-level flags in Prevent AI Align `_can_use_prevent_ai_features` with other AI features by checking the `organizations:gen-ai-features` flag first before evaluating org-level settings. This ensures the global feature gate is respected consistently across all gen-ai feature checks. Changes: - Updated `_can_use_prevent_ai_features` in seer_rpc.py to check feature flag first - Updated duplicate function in overwatch_rpc.py with same fix - Added 4 new tests to verify feature flag checking behavior - Updated 4 existing tests to enable feature flag where needed This provides a consistent way to globally control gen-ai features at the feature flag level, matching the pattern used by other AI features like autofix and issue summaries. --- .../overwatch/endpoints/overwatch_rpc.py | 4 + src/sentry/seer/endpoints/seer_rpc.py | 3 + tests/sentry/seer/endpoints/test_seer_rpc.py | 86 +++++++++++++++++-- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/sentry/overwatch/endpoints/overwatch_rpc.py b/src/sentry/overwatch/endpoints/overwatch_rpc.py index bee94fd26bfb9e..7e781ee0d15c1a 100644 --- a/src/sentry/overwatch/endpoints/overwatch_rpc.py +++ b/src/sentry/overwatch/endpoints/overwatch_rpc.py @@ -11,6 +11,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import AuthenticationSiloLimit, StandardAuthentication @@ -87,6 +88,9 @@ def authenticate_token(self, request: Request, token: str) -> tuple[Any, Any]: def _can_use_prevent_ai_features(org: Organization) -> bool: """Check if organization has opted in to Prevent AI features.""" + if not features.has("organizations:gen-ai-features", org): + return False + hide_ai_features = org.get_option("sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT) pr_review_test_generation_enabled = bool( org.get_option( diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 536f04230c015a..bf1330b7a293ee 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -270,6 +270,9 @@ def get_organization_project_ids(*, org_id: int) -> dict: def _can_use_prevent_ai_features(org: Organization) -> bool: + if not features.has("organizations:gen-ai-features", org): + return False + hide_ai_features = org.get_option("sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT) pr_review_test_generation_enabled = bool( org.get_option( diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 6ae64ee3f072ea..892c41f2016dda 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -17,6 +17,7 @@ from sentry.models.options.organization_option import OrganizationOption from sentry.models.repository import Repository from sentry.seer.endpoints.seer_rpc import ( + _can_use_prevent_ai_features, check_repository_integrations_status, generate_request_signature, get_attributes_for_span, @@ -118,6 +119,8 @@ def test_get_organization_seer_consent_by_org_name_with_default_pr_review_enable def test_get_organization_seer_consent_by_org_name_multiple_orgs_one_with_consent(self) -> None: """Test when multiple organizations exist, one with consent""" + from sentry.testutils.helpers.features import with_feature + org_without_consent = self.create_organization(owner=self.user) org_with_consent = self.create_organization(owner=self.user) @@ -143,7 +146,8 @@ def test_get_organization_seer_consent_by_org_name_multiple_orgs_one_with_consen org_with_consent, "sentry:enable_pr_review_test_generation", True ) - result = get_organization_seer_consent_by_org_name(org_name="test-org") + with with_feature("organizations:gen-ai-features"): + result = get_organization_seer_consent_by_org_name(org_name="test-org") assert result == {"consent": True} @@ -289,6 +293,63 @@ def test_get_organization_seer_consent_by_org_name_hide_ai_false_pr_review_false "consent_url": self.organization.absolute_url("/settings/organization/"), } + def test_can_use_prevent_ai_features_without_gen_ai_flag(self) -> None: + """Test that _can_use_prevent_ai_features returns False when gen-ai-features flag is disabled""" + # Enable PR review and disable hide_ai_features (should normally pass) + OrganizationOption.objects.set_value( + self.organization, "sentry:enable_pr_review_test_generation", True + ) + OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", False) + + # Without the feature flag enabled, should return False + result = _can_use_prevent_ai_features(self.organization) + assert result is False + + def test_can_use_prevent_ai_features_with_gen_ai_flag(self) -> None: + """Test that _can_use_prevent_ai_features checks org-level flags when gen-ai-features is enabled""" + from sentry.testutils.helpers.features import with_feature + + # Enable PR review and disable hide_ai_features + OrganizationOption.objects.set_value( + self.organization, "sentry:enable_pr_review_test_generation", True + ) + OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", False) + + # With the feature flag enabled and correct org settings, should return True + with with_feature("organizations:gen-ai-features"): + result = _can_use_prevent_ai_features(self.organization) + assert result is True + + def test_can_use_prevent_ai_features_with_gen_ai_flag_but_hide_ai(self) -> None: + """Test that _can_use_prevent_ai_features returns False when hide_ai_features is True""" + from sentry.testutils.helpers.features import with_feature + + # Enable PR review but enable hide_ai_features + OrganizationOption.objects.set_value( + self.organization, "sentry:enable_pr_review_test_generation", True + ) + OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", True) + + # Even with feature flag enabled, should return False due to hide_ai_features + with with_feature("organizations:gen-ai-features"): + result = _can_use_prevent_ai_features(self.organization) + assert result is False + + def test_can_use_prevent_ai_features_with_gen_ai_flag_but_no_pr_review(self) -> None: + """Test that _can_use_prevent_ai_features returns False when PR review is disabled""" + from sentry.testutils.helpers.features import with_feature + + # Disable PR review but disable hide_ai_features + OrganizationOption.objects.set_value( + self.organization, "sentry:enable_pr_review_test_generation", False + ) + OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", False) + + # Even with feature flag enabled, should return False due to PR review being disabled + with with_feature("organizations:gen-ai-features"): + result = _can_use_prevent_ai_features(self.organization) + assert result is False + def test_get_attributes_for_span(self) -> None: project = self.create_project(organization=self.organization) @@ -505,6 +566,7 @@ def test_get_github_enterprise_integration_config_invalid_encrypt_key( def test_get_sentry_organization_ids_repository_found(self) -> None: """Test when repository exists and is active""" + from sentry.testutils.helpers.features import with_feature # Create a project project = self.create_project(organization=self.organization) @@ -546,7 +608,8 @@ def test_get_sentry_organization_ids_repository_found(self) -> None: OrganizationOption.objects.set_value( self.organization, "sentry:enable_pr_review_test_generation", True ) - result = get_sentry_organization_ids(external_id="1234567890") + with with_feature("organizations:gen-ai-features"): + result = get_sentry_organization_ids(external_id="1234567890") assert result == { "org_ids": [self.organization.id], "org_slugs": [self.organization.slug], @@ -659,8 +722,11 @@ def test_get_sentry_organization_ids_multiple_repos_same_name_different_provider ) OrganizationOption.objects.set_value(org2, "sentry:enable_pr_review_test_generation", True) + from sentry.testutils.helpers.features import with_feature + # Search for GitHub provider - result = get_sentry_organization_ids(external_id="1234567890") + with with_feature("organizations:gen-ai-features"): + result = get_sentry_organization_ids(external_id="1234567890") assert result == { "org_ids": [self.organization.id], @@ -668,10 +734,11 @@ def test_get_sentry_organization_ids_multiple_repos_same_name_different_provider } # Search for GitLab provider - result = get_sentry_organization_ids( - provider="integrations:gitlab", - external_id="1234567890", - ) + with with_feature("organizations:gen-ai-features"): + result = get_sentry_organization_ids( + provider="integrations:gitlab", + external_id="1234567890", + ) assert result == {"org_ids": [org2.id], "org_slugs": [org2.slug]} @@ -775,7 +842,10 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: ) # Search for GitHub provider - result = get_sentry_organization_ids(external_id="1234567890") + from sentry.testutils.helpers.features import with_feature + + with with_feature("organizations:gen-ai-features"): + result = get_sentry_organization_ids(external_id="1234567890") assert set(result["org_ids"]) == {self.organization.id, org2.id} assert set(result["org_slugs"]) == {self.organization.slug, org2.slug} From 890ed0667b6382a60aa1776bca8a51e2775a6c20 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Fri, 14 Nov 2025 15:53:01 -0500 Subject: [PATCH 2/2] test: Enable gen-ai-features flag in Prevent AI consent tests The endpoint was updated to check for the organizations:gen-ai-features feature flag before checking org-level consent options. The tests need to enable this feature flag for organizations to have consent granted. --- .../overwatch/endpoints/test_overwatch_rpc.py | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index d5f11031d4df05..4bce11e60a5d85 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -344,29 +344,30 @@ def test_returns_org_ids_with_consent(self): params = {"repoId": repo_id} auth = self._auth_header_for_get(url, params, "test-secret") - resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) - assert resp.status_code == 200 - # Should return both orgs with their consent status - expected_orgs = [ - { - "org_id": org_with_consent.id, - "org_slug": org_with_consent.slug, - "org_name": org_with_consent.name, - "has_consent": True, - }, - { - "org_id": org_without_consent.id, - "org_slug": org_without_consent.slug, - "org_name": org_without_consent.name, - "has_consent": False, - }, - ] - # Sort both lists by org_id to ensure consistent comparison - expected_orgs = sorted(expected_orgs, key=lambda x: x["org_id"]) - actual_data = { - "organizations": sorted(resp.data["organizations"], key=lambda x: x["org_id"]) - } - assert actual_data == {"organizations": expected_orgs} + with self.feature("organizations:gen-ai-features"): + resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) + assert resp.status_code == 200 + # Should return both orgs with their consent status + expected_orgs = [ + { + "org_id": org_with_consent.id, + "org_slug": org_with_consent.slug, + "org_name": org_with_consent.name, + "has_consent": True, + }, + { + "org_id": org_without_consent.id, + "org_slug": org_without_consent.slug, + "org_name": org_without_consent.name, + "has_consent": False, + }, + ] + # Sort both lists by org_id to ensure consistent comparison + expected_orgs = sorted(expected_orgs, key=lambda x: x["org_id"]) + actual_data = { + "organizations": sorted(resp.data["organizations"], key=lambda x: x["org_id"]) + } + assert actual_data == {"organizations": expected_orgs} @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", @@ -394,7 +395,8 @@ def test_filters_inactive_repositories(self): params = {"repoId": repo_id} auth = self._auth_header_for_get(url, params, "test-secret") - resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) - assert resp.status_code == 200 - # Should return empty list as the repository is inactive - assert resp.data == {"organizations": []} + with self.feature("organizations:gen-ai-features"): + resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) + assert resp.status_code == 200 + # Should return empty list as the repository is inactive + assert resp.data == {"organizations": []}