Skip to content

Commit 0149e9e

Browse files
authored
feat(ai): Check gen-ai feature flag before org-level flags in Prevent AI (#103386)
Fixes AIML-1596 Aligns `_can_use_prevent_ai_features` with other AI features by checking the `organizations:gen-ai-features` flag before evaluating org-level settings. This ensures the global feature gate is respected consistently across all gen-ai feature checks.
1 parent a2ade73 commit 0149e9e

File tree

4 files changed

+114
-35
lines changed

4 files changed

+114
-35
lines changed

src/sentry/overwatch/endpoints/overwatch_rpc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rest_framework.request import Request
1212
from rest_framework.response import Response
1313

14+
from sentry import features
1415
from sentry.api.api_owners import ApiOwner
1516
from sentry.api.api_publish_status import ApiPublishStatus
1617
from sentry.api.authentication import AuthenticationSiloLimit, StandardAuthentication
@@ -87,6 +88,9 @@ def authenticate_token(self, request: Request, token: str) -> tuple[Any, Any]:
8788

8889
def _can_use_prevent_ai_features(org: Organization) -> bool:
8990
"""Check if organization has opted in to Prevent AI features."""
91+
if not features.has("organizations:gen-ai-features", org):
92+
return False
93+
9094
hide_ai_features = org.get_option("sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT)
9195
pr_review_test_generation_enabled = bool(
9296
org.get_option(

src/sentry/seer/endpoints/seer_rpc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ def get_organization_project_ids(*, org_id: int) -> dict:
270270

271271

272272
def _can_use_prevent_ai_features(org: Organization) -> bool:
273+
if not features.has("organizations:gen-ai-features", org):
274+
return False
275+
273276
hide_ai_features = org.get_option("sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT)
274277
pr_review_test_generation_enabled = bool(
275278
org.get_option(

tests/sentry/overwatch/endpoints/test_overwatch_rpc.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -344,29 +344,30 @@ def test_returns_org_ids_with_consent(self):
344344
params = {"repoId": repo_id}
345345
auth = self._auth_header_for_get(url, params, "test-secret")
346346

347-
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
348-
assert resp.status_code == 200
349-
# Should return both orgs with their consent status
350-
expected_orgs = [
351-
{
352-
"org_id": org_with_consent.id,
353-
"org_slug": org_with_consent.slug,
354-
"org_name": org_with_consent.name,
355-
"has_consent": True,
356-
},
357-
{
358-
"org_id": org_without_consent.id,
359-
"org_slug": org_without_consent.slug,
360-
"org_name": org_without_consent.name,
361-
"has_consent": False,
362-
},
363-
]
364-
# Sort both lists by org_id to ensure consistent comparison
365-
expected_orgs = sorted(expected_orgs, key=lambda x: x["org_id"])
366-
actual_data = {
367-
"organizations": sorted(resp.data["organizations"], key=lambda x: x["org_id"])
368-
}
369-
assert actual_data == {"organizations": expected_orgs}
347+
with self.feature("organizations:gen-ai-features"):
348+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
349+
assert resp.status_code == 200
350+
# Should return both orgs with their consent status
351+
expected_orgs = [
352+
{
353+
"org_id": org_with_consent.id,
354+
"org_slug": org_with_consent.slug,
355+
"org_name": org_with_consent.name,
356+
"has_consent": True,
357+
},
358+
{
359+
"org_id": org_without_consent.id,
360+
"org_slug": org_without_consent.slug,
361+
"org_name": org_without_consent.name,
362+
"has_consent": False,
363+
},
364+
]
365+
# Sort both lists by org_id to ensure consistent comparison
366+
expected_orgs = sorted(expected_orgs, key=lambda x: x["org_id"])
367+
actual_data = {
368+
"organizations": sorted(resp.data["organizations"], key=lambda x: x["org_id"])
369+
}
370+
assert actual_data == {"organizations": expected_orgs}
370371

371372
@patch(
372373
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
@@ -394,7 +395,8 @@ def test_filters_inactive_repositories(self):
394395
params = {"repoId": repo_id}
395396
auth = self._auth_header_for_get(url, params, "test-secret")
396397

397-
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
398-
assert resp.status_code == 200
399-
# Should return empty list as the repository is inactive
400-
assert resp.data == {"organizations": []}
398+
with self.feature("organizations:gen-ai-features"):
399+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
400+
assert resp.status_code == 200
401+
# Should return empty list as the repository is inactive
402+
assert resp.data == {"organizations": []}

tests/sentry/seer/endpoints/test_seer_rpc.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from sentry.models.options.organization_option import OrganizationOption
1818
from sentry.models.repository import Repository
1919
from sentry.seer.endpoints.seer_rpc import (
20+
_can_use_prevent_ai_features,
2021
check_repository_integrations_status,
2122
generate_request_signature,
2223
get_attributes_for_span,
@@ -118,6 +119,8 @@ def test_get_organization_seer_consent_by_org_name_with_default_pr_review_enable
118119

119120
def test_get_organization_seer_consent_by_org_name_multiple_orgs_one_with_consent(self) -> None:
120121
"""Test when multiple organizations exist, one with consent"""
122+
from sentry.testutils.helpers.features import with_feature
123+
121124
org_without_consent = self.create_organization(owner=self.user)
122125
org_with_consent = self.create_organization(owner=self.user)
123126

@@ -143,7 +146,8 @@ def test_get_organization_seer_consent_by_org_name_multiple_orgs_one_with_consen
143146
org_with_consent, "sentry:enable_pr_review_test_generation", True
144147
)
145148

146-
result = get_organization_seer_consent_by_org_name(org_name="test-org")
149+
with with_feature("organizations:gen-ai-features"):
150+
result = get_organization_seer_consent_by_org_name(org_name="test-org")
147151

148152
assert result == {"consent": True}
149153

@@ -289,6 +293,63 @@ def test_get_organization_seer_consent_by_org_name_hide_ai_false_pr_review_false
289293
"consent_url": self.organization.absolute_url("/settings/organization/"),
290294
}
291295

296+
def test_can_use_prevent_ai_features_without_gen_ai_flag(self) -> None:
297+
"""Test that _can_use_prevent_ai_features returns False when gen-ai-features flag is disabled"""
298+
# Enable PR review and disable hide_ai_features (should normally pass)
299+
OrganizationOption.objects.set_value(
300+
self.organization, "sentry:enable_pr_review_test_generation", True
301+
)
302+
OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", False)
303+
304+
# Without the feature flag enabled, should return False
305+
result = _can_use_prevent_ai_features(self.organization)
306+
assert result is False
307+
308+
def test_can_use_prevent_ai_features_with_gen_ai_flag(self) -> None:
309+
"""Test that _can_use_prevent_ai_features checks org-level flags when gen-ai-features is enabled"""
310+
from sentry.testutils.helpers.features import with_feature
311+
312+
# Enable PR review and disable hide_ai_features
313+
OrganizationOption.objects.set_value(
314+
self.organization, "sentry:enable_pr_review_test_generation", True
315+
)
316+
OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", False)
317+
318+
# With the feature flag enabled and correct org settings, should return True
319+
with with_feature("organizations:gen-ai-features"):
320+
result = _can_use_prevent_ai_features(self.organization)
321+
assert result is True
322+
323+
def test_can_use_prevent_ai_features_with_gen_ai_flag_but_hide_ai(self) -> None:
324+
"""Test that _can_use_prevent_ai_features returns False when hide_ai_features is True"""
325+
from sentry.testutils.helpers.features import with_feature
326+
327+
# Enable PR review but enable hide_ai_features
328+
OrganizationOption.objects.set_value(
329+
self.organization, "sentry:enable_pr_review_test_generation", True
330+
)
331+
OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", True)
332+
333+
# Even with feature flag enabled, should return False due to hide_ai_features
334+
with with_feature("organizations:gen-ai-features"):
335+
result = _can_use_prevent_ai_features(self.organization)
336+
assert result is False
337+
338+
def test_can_use_prevent_ai_features_with_gen_ai_flag_but_no_pr_review(self) -> None:
339+
"""Test that _can_use_prevent_ai_features returns False when PR review is disabled"""
340+
from sentry.testutils.helpers.features import with_feature
341+
342+
# Disable PR review but disable hide_ai_features
343+
OrganizationOption.objects.set_value(
344+
self.organization, "sentry:enable_pr_review_test_generation", False
345+
)
346+
OrganizationOption.objects.set_value(self.organization, "sentry:hide_ai_features", False)
347+
348+
# Even with feature flag enabled, should return False due to PR review being disabled
349+
with with_feature("organizations:gen-ai-features"):
350+
result = _can_use_prevent_ai_features(self.organization)
351+
assert result is False
352+
292353
def test_get_attributes_for_span(self) -> None:
293354
project = self.create_project(organization=self.organization)
294355

@@ -505,6 +566,7 @@ def test_get_github_enterprise_integration_config_invalid_encrypt_key(
505566

506567
def test_get_sentry_organization_ids_repository_found(self) -> None:
507568
"""Test when repository exists and is active"""
569+
from sentry.testutils.helpers.features import with_feature
508570

509571
# Create a project
510572
project = self.create_project(organization=self.organization)
@@ -546,7 +608,8 @@ def test_get_sentry_organization_ids_repository_found(self) -> None:
546608
OrganizationOption.objects.set_value(
547609
self.organization, "sentry:enable_pr_review_test_generation", True
548610
)
549-
result = get_sentry_organization_ids(external_id="1234567890")
611+
with with_feature("organizations:gen-ai-features"):
612+
result = get_sentry_organization_ids(external_id="1234567890")
550613
assert result == {
551614
"org_ids": [self.organization.id],
552615
"org_slugs": [self.organization.slug],
@@ -659,19 +722,23 @@ def test_get_sentry_organization_ids_multiple_repos_same_name_different_provider
659722
)
660723
OrganizationOption.objects.set_value(org2, "sentry:enable_pr_review_test_generation", True)
661724

725+
from sentry.testutils.helpers.features import with_feature
726+
662727
# Search for GitHub provider
663-
result = get_sentry_organization_ids(external_id="1234567890")
728+
with with_feature("organizations:gen-ai-features"):
729+
result = get_sentry_organization_ids(external_id="1234567890")
664730

665731
assert result == {
666732
"org_ids": [self.organization.id],
667733
"org_slugs": [self.organization.slug],
668734
}
669735

670736
# Search for GitLab provider
671-
result = get_sentry_organization_ids(
672-
provider="integrations:gitlab",
673-
external_id="1234567890",
674-
)
737+
with with_feature("organizations:gen-ai-features"):
738+
result = get_sentry_organization_ids(
739+
provider="integrations:gitlab",
740+
external_id="1234567890",
741+
)
675742

676743
assert result == {"org_ids": [org2.id], "org_slugs": [org2.slug]}
677744

@@ -775,7 +842,10 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None:
775842
)
776843

777844
# Search for GitHub provider
778-
result = get_sentry_organization_ids(external_id="1234567890")
845+
from sentry.testutils.helpers.features import with_feature
846+
847+
with with_feature("organizations:gen-ai-features"):
848+
result = get_sentry_organization_ids(external_id="1234567890")
779849

780850
assert set(result["org_ids"]) == {self.organization.id, org2.id}
781851
assert set(result["org_slugs"]) == {self.organization.slug, org2.slug}

0 commit comments

Comments
 (0)