From d1e24794df851ed5e93d298c564ae18b76d04d41 Mon Sep 17 00:00:00 2001 From: Kush Dubey Date: Mon, 10 Nov 2025 00:42:10 -0800 Subject: [PATCH 1/3] fix(bug-prediction): Require code mappings in initial org ids fetch --- src/sentry/seer/endpoints/seer_rpc.py | 28 ++- tests/sentry/seer/endpoints/test_seer_rpc.py | 187 ++++++++++++++++--- 2 files changed, 180 insertions(+), 35 deletions(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index eb3a6eff83dbdc..b891dcca85f6ed 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Any, TypedDict +from typing import Any, Literal, TypedDict import sentry_sdk from cryptography.fernet import Fernet @@ -57,6 +57,7 @@ from sentry.hybridcloud.rpc.service import RpcAuthenticationSetupException, RpcResolutionException from sentry.hybridcloud.rpc.sig import SerializableFunctionValueException from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegration +from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.integrations.services.integration import integration_service from sentry.integrations.types import IntegrationProviderSlug from sentry.models.organization import Organization, OrganizationStatus @@ -279,21 +280,34 @@ def _can_use_prevent_ai_features(org: Organization) -> bool: def get_sentry_organization_ids( - *, full_repo_name: str, external_id: str, provider: str = "integrations:github" -) -> dict: + *, external_id: str, provider: str = "integrations:github", **kwargs +) -> dict[Literal["org_ids"], list[int]]: """ Get the Sentry organization ID for a given Repository. Args: - full_repo_name: The full name of the repository (e.g. "getsentry/sentry") external_id: The id of the repo in the provider's system provider: The provider of the repository (e.g. "integrations:github") """ # It's possible that multiple orgs will be returned for a given repo. - organization_ids = Repository.objects.filter( - name=full_repo_name, provider=provider, status=ObjectStatus.ACTIVE, external_id=external_id - ).values_list("organization_id", flat=True) + repositories = Repository.objects.filter( + provider=provider, + external_id=external_id, + status=ObjectStatus.ACTIVE, + ) + repo_ids = repositories.values_list("id", flat=True) + + # Filter to only repositories that have code mappings. + repo_ids_with_config = ( + RepositoryProjectPathConfig.objects.filter(repository_id__in=repo_ids) + .values_list("repository_id", flat=True) + .distinct() + ) + + organization_ids = repositories.filter(id__in=repo_ids_with_config).values_list( + "organization_id", flat=True + ) organizations = Organization.objects.filter(id__in=organization_ids) # We then filter out all orgs that didn't give us consent to use AI features. orgs_with_consent = [org for org in organizations if _can_use_prevent_ai_features(org)] diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index b17bdef18ae80d..2e04ba0a3864b8 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -13,6 +13,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.models.integration import Integration +from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.models.options.organization_option import OrganizationOption from sentry.models.repository import Repository from sentry.seer.endpoints.seer_rpc import ( @@ -505,8 +506,20 @@ 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""" + # Create a project + project = self.create_project(organization=self.organization) + + # Create an integration + integration = self.create_integration( + organization=self.organization, + provider="github", + external_id="github:1", + ) + org_integration = integration.organizationintegration_set.first() + assert org_integration is not None + # Create a repository - Repository.objects.create( + repo = Repository.objects.create( name="getsentry/sentry", organization_id=self.organization.id, provider="integrations:github", @@ -514,26 +527,31 @@ def test_get_sentry_organization_ids_repository_found(self) -> None: status=ObjectStatus.ACTIVE, ) - # By default the organization has pr_review turned off - result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", external_id="1234567890" + # Create a RepositoryProjectPathConfig + RepositoryProjectPathConfig.objects.create( + repository=repo, + project=project, + organization_integration_id=org_integration.id, + integration_id=org_integration.integration_id, + organization_id=self.organization.id, + stack_root="/", + source_root="/", ) + + # By default the organization has pr_review turned off + result = get_sentry_organization_ids(external_id="1234567890") assert result == {"org_ids": []} # Turn on pr_review OrganizationOption.objects.set_value( self.organization, "sentry:enable_pr_review_test_generation", True ) - result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", external_id="1234567890" - ) + result = get_sentry_organization_ids(external_id="1234567890") assert result == {"org_ids": [self.organization.id]} def test_get_sentry_organization_ids_repository_not_found(self) -> None: """Test when repository does not exist""" - result = get_sentry_organization_ids( - full_repo_name="nonexistent/repo", external_id="1234567890" - ) + result = get_sentry_organization_ids(external_id="1234567890") assert result == {"org_ids": []} @@ -549,9 +567,7 @@ def test_get_sentry_organization_ids_repository_inactive(self) -> None: status=ObjectStatus.DISABLED, ) - result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", external_id="1234567890" - ) + result = get_sentry_organization_ids(external_id="1234567890") # Should not find the repository because it's not active assert result == {"org_ids": []} @@ -569,9 +585,7 @@ def test_get_sentry_organization_ids_different_provider(self) -> None: ) # Search with default provider (integrations:github) - result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", external_id="1234567890" - ) + result = get_sentry_organization_ids(external_id="1234567890") # Should not find the repository because provider doesn't match assert result == {"org_ids": []} @@ -580,36 +594,75 @@ def test_get_sentry_organization_ids_multiple_repos_same_name_different_provider """Test when multiple repositories exist with same name but different providers""" org2 = self.create_organization(owner=self.user) + # Create projects + project1 = self.create_project(organization=self.organization) + project2 = self.create_project(organization=org2) + + # Create integrations + integration1 = self.create_integration( + organization=self.organization, + provider="github", + external_id="github:1", + ) + org_integration1 = integration1.organizationintegration_set.first() + assert org_integration1 is not None + + integration2 = self.create_integration( + organization=org2, + provider="gitlab", + external_id="gitlab:1", + ) + org_integration2 = integration2.organizationintegration_set.first() + assert org_integration2 is not None + # Create repositories with same name but different providers - Repository.objects.create( + repo1 = Repository.objects.create( name="getsentry/sentry", organization_id=self.organization.id, provider="integrations:github", external_id="1234567890", status=ObjectStatus.ACTIVE, ) - Repository.objects.create( + repo2 = Repository.objects.create( name="getsentry/sentry", organization_id=org2.id, provider="integrations:gitlab", external_id="1234567890", status=ObjectStatus.ACTIVE, ) + + # Create RepositoryProjectPathConfigs + RepositoryProjectPathConfig.objects.create( + repository=repo1, + project=project1, + organization_integration_id=org_integration1.id, + integration_id=org_integration1.integration_id, + organization_id=self.organization.id, + stack_root="/", + source_root="/", + ) + RepositoryProjectPathConfig.objects.create( + repository=repo2, + project=project2, + organization_integration_id=org_integration2.id, + integration_id=org_integration2.integration_id, + organization_id=org2.id, + stack_root="/", + source_root="/", + ) + OrganizationOption.objects.set_value( self.organization, "sentry:enable_pr_review_test_generation", True ) OrganizationOption.objects.set_value(org2, "sentry:enable_pr_review_test_generation", True) # Search for GitHub provider - result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", external_id="1234567890" - ) + result = get_sentry_organization_ids(external_id="1234567890") assert result == {"org_ids": [self.organization.id]} # Search for GitLab provider result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", provider="integrations:gitlab", external_id="1234567890", ) @@ -629,8 +682,38 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: OrganizationOption.objects.set_value(org2, "sentry:enable_pr_review_test_generation", True) OrganizationOption.objects.set_value(org3, "sentry:enable_pr_review_test_generation", True) + # Create projects + project1 = self.create_project(organization=self.organization) + project2 = self.create_project(organization=org2) + project3 = self.create_project(organization=org3) + + # Create integrations + integration1 = self.create_integration( + organization=self.organization, + provider="github", + external_id="github:1", + ) + org_integration1 = integration1.organizationintegration_set.first() + assert org_integration1 is not None + + integration2 = self.create_integration( + organization=org2, + provider="github", + external_id="github:2", + ) + org_integration2 = integration2.organizationintegration_set.first() + assert org_integration2 is not None + + integration3 = self.create_integration( + organization=org3, + provider="github", + external_id="github:3", + ) + org_integration3 = integration3.organizationintegration_set.first() + assert org_integration3 is not None + # repo in org 1 - Repository.objects.create( + repo1 = Repository.objects.create( name="getsentry/sentry", organization_id=self.organization.id, provider="integrations:github", @@ -639,7 +722,7 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: ) # repo in org 2 - Repository.objects.create( + repo2 = Repository.objects.create( name="getsentry/sentry", organization_id=org2.id, provider="integrations:github", @@ -648,7 +731,7 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: ) # repo in org 3 - Repository.objects.create( + repo3 = Repository.objects.create( name="getsentry/sentry", organization_id=org3.id, provider="integrations:github", @@ -656,12 +739,60 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: status=ObjectStatus.ACTIVE, ) + # Create RepositoryProjectPathConfigs + RepositoryProjectPathConfig.objects.create( + repository=repo1, + project=project1, + organization_integration_id=org_integration1.id, + integration_id=org_integration1.integration_id, + organization_id=self.organization.id, + stack_root="/", + source_root="/", + ) + RepositoryProjectPathConfig.objects.create( + repository=repo2, + project=project2, + organization_integration_id=org_integration2.id, + integration_id=org_integration2.integration_id, + organization_id=org2.id, + stack_root="/", + source_root="/", + ) + RepositoryProjectPathConfig.objects.create( + repository=repo3, + project=project3, + organization_integration_id=org_integration3.id, + integration_id=org_integration3.integration_id, + organization_id=org3.id, + stack_root="/", + source_root="/", + ) + # Search for GitHub provider - result = get_sentry_organization_ids( - full_repo_name="getsentry/sentry", external_id="1234567890" + result = get_sentry_organization_ids(external_id="1234567890") + + assert set(result["org_ids"]) == {self.organization.id, org2.id} + + def test_get_sentry_organization_ids_no_repo_project_path_config(self) -> None: + """Test when repository exists but has no RepositoryProjectPathConfig""" + # Create a repository without any RepositoryProjectPathConfig + Repository.objects.create( + name="getsentry/sentry", + organization_id=self.organization.id, + provider="integrations:github", + external_id="1234567890", + status=ObjectStatus.ACTIVE, ) - assert result == {"org_ids": [self.organization.id, org2.id]} + # Turn on pr_review + OrganizationOption.objects.set_value( + self.organization, "sentry:enable_pr_review_test_generation", True + ) + + # Should not find the organization because there's no RepositoryProjectPathConfig + result = get_sentry_organization_ids(external_id="1234567890") + + assert result == {"org_ids": []} def test_send_seer_webhook_invalid_event_name(self) -> None: """Test that send_seer_webhook returns error for invalid event names""" From 02efcb7e4450596c7bcac654fb22938b3d55fd4a Mon Sep 17 00:00:00 2001 From: Kush Dubey Date: Mon, 10 Nov 2025 09:41:33 -0800 Subject: [PATCH 2/3] include org slugs --- src/sentry/seer/endpoints/seer_rpc.py | 5 ++++- tests/sentry/seer/endpoints/test_seer_rpc.py | 23 +++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index b891dcca85f6ed..2389ece1a5fde2 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -312,7 +312,10 @@ def get_sentry_organization_ids( # We then filter out all orgs that didn't give us consent to use AI features. orgs_with_consent = [org for org in organizations if _can_use_prevent_ai_features(org)] - return {"org_ids": [organization.id for organization in orgs_with_consent]} + return { + "org_ids": [organization.id for organization in orgs_with_consent], + "org_slugs": [organization.slug for organization in orgs_with_consent], + } def get_organization_autofix_consent(*, org_id: int) -> dict: diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 2e04ba0a3864b8..23a364933bbed1 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -540,20 +540,23 @@ def test_get_sentry_organization_ids_repository_found(self) -> None: # By default the organization has pr_review turned off result = get_sentry_organization_ids(external_id="1234567890") - assert result == {"org_ids": []} + assert result == {"org_ids": [], "org_slugs": []} # Turn on pr_review OrganizationOption.objects.set_value( self.organization, "sentry:enable_pr_review_test_generation", True ) result = get_sentry_organization_ids(external_id="1234567890") - assert result == {"org_ids": [self.organization.id]} + assert result == { + "org_ids": [self.organization.id], + "org_slugs": [self.organization.slug], + } def test_get_sentry_organization_ids_repository_not_found(self) -> None: """Test when repository does not exist""" result = get_sentry_organization_ids(external_id="1234567890") - assert result == {"org_ids": []} + assert result == {"org_ids": [], "org_slugs": []} def test_get_sentry_organization_ids_repository_inactive(self) -> None: """Test when repository exists but is not active""" @@ -570,7 +573,7 @@ def test_get_sentry_organization_ids_repository_inactive(self) -> None: result = get_sentry_organization_ids(external_id="1234567890") # Should not find the repository because it's not active - assert result == {"org_ids": []} + assert result == {"org_ids": [], "org_slugs": []} def test_get_sentry_organization_ids_different_provider(self) -> None: """Test when repository exists but with different provider""" @@ -588,7 +591,7 @@ def test_get_sentry_organization_ids_different_provider(self) -> None: result = get_sentry_organization_ids(external_id="1234567890") # Should not find the repository because provider doesn't match - assert result == {"org_ids": []} + assert result == {"org_ids": [], "org_slugs": []} def test_get_sentry_organization_ids_multiple_repos_same_name_different_providers(self) -> None: """Test when multiple repositories exist with same name but different providers""" @@ -659,7 +662,10 @@ def test_get_sentry_organization_ids_multiple_repos_same_name_different_provider # Search for GitHub provider result = get_sentry_organization_ids(external_id="1234567890") - assert result == {"org_ids": [self.organization.id]} + assert result == { + "org_ids": [self.organization.id], + "org_slugs": [self.organization.slug], + } # Search for GitLab provider result = get_sentry_organization_ids( @@ -667,7 +673,7 @@ def test_get_sentry_organization_ids_multiple_repos_same_name_different_provider external_id="1234567890", ) - assert result == {"org_ids": [org2.id]} + assert result == {"org_ids": [org2.id], "org_slugs": [org2.slug]} def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: """Test when multiple repositories exist with same name but different providers and provider is provided""" @@ -772,6 +778,7 @@ def test_get_sentry_organization_ids_multiple_orgs_same_repo(self) -> None: 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} def test_get_sentry_organization_ids_no_repo_project_path_config(self) -> None: """Test when repository exists but has no RepositoryProjectPathConfig""" @@ -792,7 +799,7 @@ def test_get_sentry_organization_ids_no_repo_project_path_config(self) -> None: # Should not find the organization because there's no RepositoryProjectPathConfig result = get_sentry_organization_ids(external_id="1234567890") - assert result == {"org_ids": []} + assert result == {"org_ids": [], "org_slugs": []} def test_send_seer_webhook_invalid_event_name(self) -> None: """Test that send_seer_webhook returns error for invalid event names""" From 29a7c3eeba6faad7297ec80be515778a7f981fa3 Mon Sep 17 00:00:00 2001 From: Kush Dubey Date: Mon, 10 Nov 2025 11:11:39 -0800 Subject: [PATCH 3/3] return type --- src/sentry/seer/endpoints/seer_rpc.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 2389ece1a5fde2..9ce57354663bfe 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Any, Literal, TypedDict +from typing import Any, TypedDict import sentry_sdk from cryptography.fernet import Fernet @@ -279,9 +279,14 @@ def _can_use_prevent_ai_features(org: Organization) -> bool: return not hide_ai_features and pr_review_test_generation_enabled +class SentryOrganizaionIdsAndSlugs(TypedDict): + org_ids: list[int] + org_slugs: list[str] + + def get_sentry_organization_ids( *, external_id: str, provider: str = "integrations:github", **kwargs -) -> dict[Literal["org_ids"], list[int]]: +) -> SentryOrganizaionIdsAndSlugs: """ Get the Sentry organization ID for a given Repository.