Skip to content

Commit 66e863e

Browse files
committed
incorporate pr comments
1 parent 17b7e2a commit 66e863e

File tree

16 files changed

+627
-118
lines changed

16 files changed

+627
-118
lines changed

fixtures/github.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3548,6 +3548,7 @@
35483548
# Simplified example of a check_run rerequested action event
35493549
# Note: installation.id must match the external_id used in create_github_integration (default: "12345")
35503550
# Note: repository.id must match a Repository created for the organization (use create_repo in tests)
3551+
# Note: check_run events are linked to PRs via check_run.pull_requests - we use user.id as fallback for billing
35513552
CHECK_RUN_REREQUESTED_ACTION_EVENT_EXAMPLE = b"""{
35523553
"action": "rerequested",
35533554
"installation": {"id": 12345},
@@ -3558,7 +3559,12 @@
35583559
},
35593560
"check_run": {
35603561
"external_id": "4663713",
3561-
"html_url": "https://github.com/test/repo/runs/4"
3562+
"html_url": "https://github.com/test/repo/runs/4",
3563+
"pull_requests": []
3564+
},
3565+
"user": {
3566+
"id": 12345678,
3567+
"login": "test-user"
35623568
}
35633569
}"""
35643570

@@ -3569,5 +3575,9 @@
35693575
"id": 35129377,
35703576
"full_name": "getsentry/sentry",
35713577
"html_url": "https://github.com/getsentry/sentry"
3578+
},
3579+
"user": {
3580+
"id": 12345678,
3581+
"login": "test-user"
35723582
}
35733583
}"""

src/sentry/integrations/github/webhook.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __call__(
8686
event: Mapping[str, Any],
8787
organization: Organization,
8888
repo: Repository,
89+
integration: RpcIntegration | None = None,
8990
**kwargs: Any,
9091
) -> None: ...
9192

src/sentry/models/repositorysettings.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass
34
from enum import StrEnum
45

56
from django.contrib.postgres.fields.array import ArrayField
@@ -19,6 +20,14 @@ def as_choices(cls) -> tuple[tuple[str, str], ...]:
1920
return tuple((trigger.value, trigger.value) for trigger in cls)
2021

2122

23+
@dataclass
24+
class CodeReviewSettings:
25+
"""Settings for code review functionality on a repository."""
26+
27+
enabled: bool
28+
triggers: list[CodeReviewTrigger]
29+
30+
2231
@region_silo_model
2332
class RepositorySettings(Model):
2433
"""
@@ -41,3 +50,8 @@ class Meta:
4150
db_table = "sentry_repositorysettings"
4251

4352
__repr__ = sane_repr("repository_id", "enabled_code_review")
53+
54+
def get_code_review_settings(self) -> CodeReviewSettings:
55+
"""Return code review settings for this repository."""
56+
triggers = [CodeReviewTrigger(t) for t in self.code_review_triggers]
57+
return CodeReviewSettings(enabled=self.enabled_code_review, triggers=triggers)

src/sentry/overwatch/endpoints/overwatch_rpc.py

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,20 @@
1111
from rest_framework.request import Request
1212
from rest_framework.response import Response
1313

14-
from sentry import features, quotas
14+
from sentry import features
1515
from sentry.api.api_owners import ApiOwner
1616
from sentry.api.api_publish_status import ApiPublishStatus
1717
from sentry.api.authentication import AuthenticationSiloLimit, StandardAuthentication
1818
from sentry.api.base import Endpoint, region_silo_endpoint
19-
from sentry.constants import DEFAULT_CODE_REVIEW_TRIGGERS, DataCategory, ObjectStatus
19+
from sentry.constants import DEFAULT_CODE_REVIEW_TRIGGERS, ObjectStatus
2020
from sentry.integrations.services.integration import integration_service
2121
from sentry.models.organization import Organization
22-
from sentry.models.organizationcontributors import OrganizationContributors
2322
from sentry.models.repository import Repository
2423
from sentry.models.repositorysettings import RepositorySettings
2524
from sentry.prevent.models import PreventAIConfiguration
2625
from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT, PREVENT_AI_CONFIG_DEFAULT_V1
26+
from sentry.seer.code_review.billing import passes_code_review_billing_check
2727
from sentry.silo.base import SiloMode
28-
from sentry.utils import metrics
2928
from sentry.utils.seer import can_use_prevent_ai_features
3029

3130
logger = logging.getLogger(__name__)
@@ -309,32 +308,11 @@ def _is_eligible_for_code_review(
309308
if not code_review_enabled:
310309
return False
311310

312-
# Check if contributor exists, and if there's either a seat or quota available.
313-
# NOTE: We explicitly check billing as the source of truth because if the contributor exists,
314-
# then that means that they've opened a PR before, and either have a seat already OR it's their
315-
# "Free action."
316-
try:
317-
contributor = OrganizationContributors.objects.get(
318-
organization_id=organization.id,
319-
integration_id=integration_id,
320-
external_identifier=external_identifier,
321-
)
322-
323-
if not quotas.backend.check_seer_quota(
324-
org_id=organization.id,
325-
data_category=DataCategory.SEER_USER,
326-
seat_object=contributor,
327-
):
328-
return False
329-
330-
except OrganizationContributors.DoesNotExist:
331-
metrics.incr(
332-
"overwatch.code_review.contributor_not_found",
333-
tags={"organization_id": organization.id, "repository_id": repository_id},
334-
)
335-
return False
336-
337-
return True
311+
return passes_code_review_billing_check(
312+
organization_id=organization.id,
313+
integration_id=integration_id,
314+
external_identifier=external_identifier,
315+
)
338316

339317

340318
@region_silo_endpoint
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
from sentry import quotas
4+
from sentry.constants import DataCategory
5+
from sentry.models.organizationcontributors import OrganizationContributors
6+
from sentry.utils import metrics
7+
8+
9+
def passes_code_review_billing_check(
10+
organization_id: int,
11+
integration_id: int,
12+
external_identifier: str,
13+
) -> bool:
14+
"""
15+
Check if contributor exists, and if there's either a seat or quota available.
16+
NOTE: We explicitly check billing as the source of truth because if the contributor exists,
17+
then that means that they've opened a PR before, and either have a seat already OR it's their
18+
"Free action."
19+
20+
Returns False if the billing check does not pass for code review feature.
21+
"""
22+
try:
23+
contributor = OrganizationContributors.objects.get(
24+
organization_id=organization_id,
25+
integration_id=integration_id,
26+
external_identifier=external_identifier,
27+
)
28+
except OrganizationContributors.DoesNotExist:
29+
metrics.incr(
30+
"overwatch.code_review.contributor_not_found",
31+
tags={"organization_id": organization_id},
32+
)
33+
return False
34+
35+
return quotas.backend.check_seer_quota(
36+
org_id=organization_id,
37+
data_category=DataCategory.SEER_USER,
38+
seat_object=contributor,
39+
)

src/sentry/seer/code_review/preflight.py

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@
33
from collections.abc import Callable
44
from dataclasses import dataclass
55

6-
from sentry import features, quotas
7-
from sentry.constants import (
8-
ENABLE_PR_REVIEW_TEST_GENERATION_DEFAULT,
9-
HIDE_AI_FEATURES_DEFAULT,
10-
DataCategory,
11-
)
6+
from sentry import features
7+
from sentry.constants import ENABLE_PR_REVIEW_TEST_GENERATION_DEFAULT, HIDE_AI_FEATURES_DEFAULT
128
from sentry.models.organization import Organization
13-
from sentry.models.organizationcontributors import OrganizationContributors
149
from sentry.models.repository import Repository
10+
from sentry.models.repositorysettings import CodeReviewSettings, RepositorySettings
1511

16-
from .settings import CodeReviewSettings, get_code_review_settings
12+
from .billing import passes_code_review_billing_check
1713

1814
DenialReason = str | None
1915

@@ -31,14 +27,15 @@ def __init__(
3127
organization: Organization,
3228
repo: Repository,
3329
integration_id: int | None = None,
34-
external_identifier: str | None = None,
30+
pr_author_external_id: str | None = None,
3531
):
3632
self.organization = organization
3733
self.repo = repo
3834
self.integration_id = integration_id
39-
self.external_identifier = external_identifier
40-
self._repo_settings: CodeReviewSettings | None = None
41-
self._repo_settings_loaded = False
35+
self.pr_author_external_id = pr_author_external_id
36+
37+
repo_settings = RepositorySettings.objects.filter(repository=repo).first()
38+
self._repo_settings = repo_settings.get_code_review_settings() if repo_settings else None
4239

4340
def check(self) -> CodeReviewPreflightResult:
4441
checks: list[Callable[[], DenialReason]] = [
@@ -53,13 +50,7 @@ def check(self) -> CodeReviewPreflightResult:
5350
if denial_reason is not None:
5451
return CodeReviewPreflightResult(allowed=False, denial_reason=denial_reason)
5552

56-
return CodeReviewPreflightResult(allowed=True, settings=self._get_repo_settings())
57-
58-
def _get_repo_settings(self) -> CodeReviewSettings | None:
59-
if not self._repo_settings_loaded:
60-
self._repo_settings = get_code_review_settings(self.repo)
61-
self._repo_settings_loaded = True
62-
return self._repo_settings
53+
return CodeReviewPreflightResult(allowed=True, settings=self._repo_settings)
6354

6455
# -------------------------------------------------------------------------
6556
# Checks - each returns denial reason (str) or None if valid
@@ -90,8 +81,7 @@ def _check_org_feature_enablement(self) -> DenialReason:
9081

9182
def _check_repo_feature_enablement(self) -> DenialReason:
9283
if self._is_seat_based_seer_plan_org():
93-
settings = self._get_repo_settings()
94-
if settings is None or not settings.enabled:
84+
if self._repo_settings is None or not self._repo_settings.enabled:
9585
return "repo_code_review_disabled"
9686
return None
9787
elif self._is_code_review_beta_org():
@@ -101,28 +91,15 @@ def _check_repo_feature_enablement(self) -> DenialReason:
10191
return "repo_code_review_disabled"
10292

10393
def _check_billing(self) -> DenialReason:
104-
# Check if contributor exists, and if there's either a seat or quota available.
105-
# NOTE: We explicitly check billing as the source of truth because if the contributor exists,
106-
# then that means that they've opened a PR before, and either have a seat already OR it's their
107-
# "Free action."
108-
if self.integration_id is None or self.external_identifier is None:
94+
if self.integration_id is None or self.pr_author_external_id is None:
10995
return "billing_missing_contributor_info"
11096

111-
try:
112-
contributor = OrganizationContributors.objects.get(
113-
organization_id=self.organization.id,
114-
integration_id=self.integration_id,
115-
external_identifier=self.external_identifier,
116-
)
117-
except OrganizationContributors.DoesNotExist:
118-
return "billing_contributor_not_found"
119-
120-
has_quota = quotas.backend.check_seer_quota(
121-
org_id=self.organization.id,
122-
data_category=DataCategory.SEER_USER,
123-
seat_object=contributor,
97+
billing_ok = passes_code_review_billing_check(
98+
organization_id=self.organization.id,
99+
integration_id=self.integration_id,
100+
external_identifier=self.pr_author_external_id,
124101
)
125-
if not has_quota:
102+
if not billing_ok:
126103
return "billing_quota_exceeded"
127104

128105
return None

src/sentry/seer/code_review/settings.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/sentry/seer/code_review/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,23 @@ def transform_webhook_to_codegen_request(
123123
"request_type": request_type,
124124
"organization_id": organization_id,
125125
}
126+
127+
128+
def get_pr_author_id(event: Mapping[str, Any]) -> str | None:
129+
"""
130+
Extract the PR author's GitHub user ID from the webhook payload.
131+
The user information can be found in different locations depending on the webhook type.
132+
"""
133+
# Check issue.user.id (for issue comments on PRs)
134+
if (user_id := event.get("issue", {}).get("user", {}).get("id")) is not None:
135+
return str(user_id)
136+
137+
# Check pull_request.user.id (for pull request events)
138+
if (user_id := event.get("pull_request", {}).get("user", {}).get("id")) is not None:
139+
return str(user_id)
140+
141+
# Check user.id (fallback for direct user events)
142+
if (user_id := event.get("user", {}).get("id")) is not None:
143+
return str(user_id)
144+
145+
return None

src/sentry/seer/code_review/webhooks/check_run.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from sentry.models.repository import Repository
2323
from sentry.utils import metrics
2424

25-
from ..preflight import CodeReviewPreflightService
2625
from ..utils import SeerEndpoint, make_seer_request
2726

2827
logger = logging.getLogger(__name__)
@@ -31,7 +30,6 @@
3130
class ErrorStatus(enum.StrEnum):
3231
MISSING_ORGANIZATION = "missing_organization"
3332
MISSING_ACTION = "missing_action"
34-
CODE_REVIEW_NOT_ENABLED = "code_review_not_enabled"
3533
INVALID_PAYLOAD = "invalid_payload"
3634

3735

@@ -118,14 +116,6 @@ def handle_check_run_event(
118116
if action != GitHubCheckRunAction.REREQUESTED:
119117
return
120118

121-
preflight = CodeReviewPreflightService(organization, repo).check()
122-
if not preflight.allowed:
123-
metrics.incr(
124-
f"{Metrics.ERROR.value}",
125-
tags={**tags, "error_status": ErrorStatus.CODE_REVIEW_NOT_ENABLED.value},
126-
)
127-
return
128-
129119
try:
130120
validated_event = _validate_github_check_run_event(event)
131121
except (ValidationError, ValueError):

0 commit comments

Comments
 (0)