diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 9b72503751df90..8b4ba4753495f2 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -78,6 +78,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:chonk-ui-feedback", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enables Codecov UI manager.add("organizations:codecov-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable Prevent AI code review to run per commit + manager.add("organizations:code-review-run-per-commit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enables Prevent Test Analytics manager.add("organizations:prevent-test-analytics", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the improved command menu (Cmd+K) diff --git a/src/sentry/overwatch/endpoints/overwatch_rpc.py b/src/sentry/overwatch/endpoints/overwatch_rpc.py index 7e781ee0d15c1a..b4bdbd52807f4b 100644 --- a/src/sentry/overwatch/endpoints/overwatch_rpc.py +++ b/src/sentry/overwatch/endpoints/overwatch_rpc.py @@ -25,7 +25,7 @@ from sentry.models.organization import Organization from sentry.models.repository import Repository from sentry.prevent.models import PreventAIConfiguration -from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT +from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT, PREVENT_AI_CONFIG_DEFAULT_V1 from sentry.silo.base import SiloMode logger = logging.getLogger(__name__) @@ -154,7 +154,13 @@ def get(self, request: Request) -> Response: integration_id=github_org_integrations[0].integration_id, ).first() - response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_DEFAULT) + organization = Organization.objects.filter(id=sentry_org_id).first() + + default_config = PREVENT_AI_CONFIG_DEFAULT + if features.has("organizations:code-review-run-per-commit", organization): + default_config = PREVENT_AI_CONFIG_DEFAULT_V1 + + response_data: dict[str, Any] = deepcopy(default_config) if config: response_data["organization"] = config.data diff --git a/src/sentry/prevent/endpoints/pr_review_github_config.py b/src/sentry/prevent/endpoints/pr_review_github_config.py index 7e74c60ae3a2dc..676684cb9e5e0f 100644 --- a/src/sentry/prevent/endpoints/pr_review_github_config.py +++ b/src/sentry/prevent/endpoints/pr_review_github_config.py @@ -5,7 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import audit_log +from sentry import audit_log, features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint @@ -15,7 +15,11 @@ from sentry.integrations.types import IntegrationProviderSlug from sentry.models.organization import Organization from sentry.prevent.models import PreventAIConfiguration -from sentry.prevent.types.config import ORG_CONFIG_SCHEMA, PREVENT_AI_CONFIG_DEFAULT +from sentry.prevent.types.config import ( + ORG_CONFIG_SCHEMA, + PREVENT_AI_CONFIG_DEFAULT, + PREVENT_AI_CONFIG_DEFAULT_V1, +) class PreventAIConfigPermission(OrganizationPermission): @@ -59,7 +63,11 @@ def get( integration_id=github_org_integrations[0].integration_id, ).first() - response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_DEFAULT) + default_config = PREVENT_AI_CONFIG_DEFAULT + if features.has("organizations:code-review-run-per-commit", organization): + default_config = PREVENT_AI_CONFIG_DEFAULT_V1 + + response_data: dict[str, Any] = deepcopy(default_config) if config: response_data["organization"] = config.data diff --git a/src/sentry/prevent/types/config.py b/src/sentry/prevent/types/config.py index 0416e896839f0a..55928e84b077e9 100644 --- a/src/sentry/prevent/types/config.py +++ b/src/sentry/prevent/types/config.py @@ -125,3 +125,40 @@ }, "organization": {}, } + +PREVENT_AI_CONFIG_DEFAULT_V1 = { + "schema_version": "v1", + "default_org_config": { + "org_defaults": { + "bug_prediction": { + "enabled": True, + "sensitivity": "medium", + "triggers": { + "on_command_phrase": True, + "on_ready_for_review": True, + # v1 default enables on_new_commit + "on_new_commit": True, + }, + }, + "test_generation": { + "enabled": True, + "triggers": { + "on_command_phrase": True, + "on_ready_for_review": False, + "on_new_commit": False, + }, + }, + "vanilla": { + "enabled": True, + "sensitivity": "medium", + "triggers": { + "on_command_phrase": True, + "on_ready_for_review": False, + "on_new_commit": False, + }, + }, + }, + "repo_overrides": {}, + }, + "organization": {}, +} diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index 4bce11e60a5d85..fb52204e7738db 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -7,7 +7,7 @@ from sentry.constants import ObjectStatus from sentry.prevent.models import PreventAIConfiguration -from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT +from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT, PREVENT_AI_CONFIG_DEFAULT_V1 from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase from sentry.testutils.silo import assume_test_silo_mode @@ -157,6 +157,52 @@ def test_returns_default_when_no_config(self): assert resp.status_code == 200 assert resp.data == PREVENT_AI_CONFIG_DEFAULT assert resp.data["organization"] == {} + # Default config has on_new_commit disabled for bug_prediction + assert ( + resp.data["default_org_config"]["org_defaults"]["bug_prediction"]["triggers"][ + "on_new_commit" + ] + is False + ) + + @patch( + "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", + ["test-secret"], + ) + def test_returns_v1_default_when_feature_flag_enabled(self): + """Test that V1 default config is returned when code-review-run-per-commit flag is enabled.""" + org = self.create_organization() + git_org_name = "test-github-org" + + with assume_test_silo_mode(SiloMode.CONTROL): + self.create_integration( + organization=org, + provider="github", + name=git_org_name, + external_id=f"github:{git_org_name}", + status=ObjectStatus.ACTIVE, + ) + + url = reverse("sentry-api-0-prevent-pr-review-configs-resolved") + params = { + "sentryOrgId": str(org.id), + "gitOrgName": git_org_name, + "provider": "github", + } + auth = self._auth_header_for_get(url, params, "test-secret") + + with self.feature({"organizations:code-review-run-per-commit": org}): + resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) + assert resp.status_code == 200 + assert resp.data == PREVENT_AI_CONFIG_DEFAULT_V1 + # V1 config has on_new_commit enabled for bug_prediction + assert ( + resp.data["default_org_config"]["org_defaults"]["bug_prediction"]["triggers"][ + "on_new_commit" + ] + is True + ) + assert resp.data["organization"] == {} @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", diff --git a/tests/sentry/prevent/endpoints/test_pr_review_config.py b/tests/sentry/prevent/endpoints/test_pr_review_config.py index 1cda279ae7b13c..e0728a85ee887e 100644 --- a/tests/sentry/prevent/endpoints/test_pr_review_config.py +++ b/tests/sentry/prevent/endpoints/test_pr_review_config.py @@ -4,7 +4,7 @@ from sentry.constants import ObjectStatus from sentry.prevent.models import PreventAIConfiguration -from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT +from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT, PREVENT_AI_CONFIG_DEFAULT_V1 from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase from sentry.testutils.silo import assume_test_silo_mode, region_silo_test @@ -55,6 +55,28 @@ def test_get_returns_default_when_no_config(self): assert resp.status_code == 200 assert resp.data == PREVENT_AI_CONFIG_DEFAULT assert resp.data["organization"] == {} + # Default config has on_new_commit disabled for bug_prediction + assert ( + resp.data["default_org_config"]["org_defaults"]["bug_prediction"]["triggers"][ + "on_new_commit" + ] + is False + ) + + def test_get_returns_v1_default_when_feature_flag_enabled(self): + """Test GET endpoint returns V1 default config when code-review-run-per-commit flag is enabled.""" + with self.feature("organizations:code-review-run-per-commit"): + resp = self.client.get(self.url) + assert resp.status_code == 200 + assert resp.data == PREVENT_AI_CONFIG_DEFAULT_V1 + # V1 config has on_new_commit enabled for bug_prediction + assert ( + resp.data["default_org_config"]["org_defaults"]["bug_prediction"]["triggers"][ + "on_new_commit" + ] + is True + ) + assert resp.data["organization"] == {} def test_get_returns_config_when_exists(self): """Test GET endpoint returns the saved configuration when it exists."""