Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/sentry/integrations/github/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@

from rest_framework.response import Response

from sentry import options
from sentry import features, options
from sentry.constants import ENABLE_PR_REVIEW_TEST_GENERATION_DEFAULT, HIDE_AI_FEATURES_DEFAULT
from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig
from sentry.models.organization import Organization
from sentry.models.repository import Repository
from sentry.seer.seer_setup import get_seer_org_acknowledgement
from sentry.utils import jwt

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -78,3 +83,34 @@ def parse_github_blob_url(repo_url: str, source_url: str) -> tuple[str, str]:

branch, _, remainder = after_blob.partition("/")
return branch, remainder.lstrip("/")


def has_seer_and_ai_features_enabled_for_repo(organization: Organization, repo: Repository) -> bool:
"""
Check if Seer is enabled and AI features are configured for a repository.

Returns:
True if Seer and AI features are enabled for the repository else False
"""

seer_enabled = get_seer_org_acknowledgement(organization)
if not seer_enabled:
return False

repo_enabled = RepositoryProjectPathConfig.objects.filter(
repository=repo,
organization_id=organization.id,
).exists()
if not repo_enabled:
return False

gen_ai_features = features.has(
"organizations:gen-ai-features", organization
) and not organization.get_option("sentry:hide_ai_features", HIDE_AI_FEATURES_DEFAULT)

ai_code_review_enabled = organization.get_option(
Copy link
Member

@suejung-sentry suejung-sentry Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can skip this toggle check for the value of the sentry:enable_pr_review_test_generation organization option for the purposes of billing here.

We're going to keep the toggle around only for legacy seer usage based plan people so they have a way to turn on/off code review until they purchase the seat based plan. They will get code review for free if they turn this toggle on. (slack convo).

The value of the option is no longer relevant for any orgs outside of above

"sentry:enable_pr_review_test_generation",
ENABLE_PR_REVIEW_TEST_GENERATION_DEFAULT,
)

return gen_ai_features or ai_code_review_enabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: hide_ai_features option not respected for ai_code_review path

The has_seer_and_ai_features_enabled_for_repo function doesn't check hide_ai_features when evaluating the ai_code_review_enabled path. When an organization sets both sentry:hide_ai_features=True and sentry:enable_pr_review_test_generation=True, the function returns True even though AI features are supposed to be hidden. This contradicts similar logic in _can_use_prevent_ai_features which returns not hide_ai_features and pr_review_test_generation_enabled. The or logic should likely be and with hide_ai_features check, or ai_code_review_enabled needs to include the hide_ai_features check.

Fix in Cursor Fix in Web

12 changes: 12 additions & 0 deletions src/sentry/integrations/github/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from .repository import GitHubRepositoryProvider
from .tasks.codecov_account_unlink import codecov_account_unlink
from .types import IssueEvenntWebhookActionType
from .utils import has_seer_and_ai_features_enabled_for_repo

logger = logging.getLogger("sentry.webhooks")

Expand Down Expand Up @@ -767,6 +768,17 @@ def _handle(
)

if created:
# Track AI contributor if eligible
if has_seer_and_ai_features_enabled_for_repo(organization, repo):
metrics.incr(
"github.webhook.organization_contributor.should_create",
sample_rate=1.0,
tags={
"organization_id": organization.id,
"repository_id": repo.id,
},
)

metrics.incr(
"github.webhook.pull_request.created",
sample_rate=1.0,
Expand Down
83 changes: 82 additions & 1 deletion tests/sentry/integrations/github/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,87 @@
from unittest.mock import patch

import pytest

from sentry.integrations.github.utils import parse_github_blob_url
from sentry.integrations.github.utils import (
has_seer_and_ai_features_enabled_for_repo,
parse_github_blob_url,
)
from sentry.testutils.cases import TestCase


class HasSeerAndAiFeaturesEnabledForRepoTest(TestCase):
def setUp(self):
super().setUp()
self.integration = self.create_integration(
organization=self.organization,
provider="github",
external_id="github:1",
)
self.repo = self.create_repo(
project=self.project,
provider="integrations:github",
integration_id=self.integration.id,
)

@patch(
"sentry.integrations.github.utils.get_seer_org_acknowledgement",
return_value=False,
)
def test_returns_false_when_seer_not_acknowledged(self, mock_seer_ack):
result = has_seer_and_ai_features_enabled_for_repo(self.organization, self.repo)
assert result is False

@patch(
"sentry.integrations.github.utils.get_seer_org_acknowledgement",
return_value=True,
)
def test_returns_false_when_no_code_mappings(self, mock_seer_ack):
result = has_seer_and_ai_features_enabled_for_repo(self.organization, self.repo)
assert result is False

@patch(
"sentry.integrations.github.utils.get_seer_org_acknowledgement",
return_value=True,
)
def test_returns_false_when_ai_features_disabled(self, mock_seer_ack):
self.create_code_mapping(project=self.project, repo=self.repo)

result = has_seer_and_ai_features_enabled_for_repo(self.organization, self.repo)
assert result is False

@patch(
"sentry.integrations.github.utils.get_seer_org_acknowledgement",
return_value=True,
)
def test_returns_true_when_gen_ai_features_enabled(self, mock_seer_ack):
self.create_code_mapping(project=self.project, repo=self.repo)

with self.feature("organizations:gen-ai-features"):
result = has_seer_and_ai_features_enabled_for_repo(self.organization, self.repo)
assert result is True

@patch(
"sentry.integrations.github.utils.get_seer_org_acknowledgement",
return_value=True,
)
def test_returns_false_when_gen_ai_features_enabled_but_hidden(self, mock_seer_ack):
self.create_code_mapping(project=self.project, repo=self.repo)
self.organization.update_option("sentry:hide_ai_features", True)

with self.feature("organizations:gen-ai-features"):
result = has_seer_and_ai_features_enabled_for_repo(self.organization, self.repo)
assert result is False

@patch(
"sentry.integrations.github.utils.get_seer_org_acknowledgement",
return_value=True,
)
def test_returns_true_when_ai_code_review_enabled(self, mock_seer_ack):
self.create_code_mapping(project=self.project, repo=self.repo)
self.organization.update_option("sentry:enable_pr_review_test_generation", True)

result = has_seer_and_ai_features_enabled_for_repo(self.organization, self.repo)
assert result is True


@pytest.mark.parametrize(
Expand Down
Loading