Skip to content

Commit d558c4c

Browse files
JerrySentryisabellaenriquez
authored andcommitted
feat(prevent): Update overwatch endpoint to retrieve config (#102419)
- Update the overwatch rpc endpoint `/prevent/pr-review/configs/resolved` to fetch stored configs
1 parent 024c502 commit d558c4c

File tree

5 files changed

+292
-21
lines changed

5 files changed

+292
-21
lines changed

src/sentry/overwatch/endpoints/overwatch_rpc.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import hashlib
22
import hmac
33
import logging
4+
from copy import deepcopy
45
from typing import Any
56

67
import sentry_sdk
@@ -19,8 +20,11 @@
1920
HIDE_AI_FEATURES_DEFAULT,
2021
ObjectStatus,
2122
)
23+
from sentry.integrations.services.integration import integration_service
2224
from sentry.models.organization import Organization
2325
from sentry.models.repository import Repository
26+
from sentry.prevent.models import PreventAIConfiguration
27+
from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT
2428
from sentry.silo.base import SiloMode
2529

2630
logger = logging.getLogger(__name__)
@@ -98,7 +102,7 @@ class PreventPrReviewResolvedConfigsEndpoint(Endpoint):
98102
"""
99103
Returns the resolved config for a Sentry organization.
100104
101-
GET /prevent/pr-review/configs/resolved?sentryOrgId={orgId}
105+
GET /prevent/pr-review/configs/resolved?sentryOrgId={orgId}&gitOrgName={gitOrgName}&provider={provider}
102106
"""
103107

104108
publish_status = {
@@ -115,9 +119,42 @@ def get(self, request: Request) -> Response:
115119
):
116120
raise PermissionDenied
117121

118-
# TODO: Fetch config from PreventAIConfiguration model
122+
sentry_org_id_str = request.GET.get("sentryOrgId")
123+
if not sentry_org_id_str:
124+
raise ParseError("Missing required query parameter: sentryOrgId")
125+
try:
126+
sentry_org_id = int(sentry_org_id_str)
127+
if sentry_org_id <= 0:
128+
raise ParseError("sentryOrgId must be a positive integer")
129+
except ValueError:
130+
raise ParseError("sentryOrgId must be a valid integer")
131+
132+
git_org_name = request.GET.get("gitOrgName")
133+
if not git_org_name:
134+
raise ParseError("Missing required query parameter: gitOrgName")
135+
provider = request.GET.get("provider")
136+
if not provider:
137+
raise ParseError("Missing required query parameter: provider")
138+
139+
github_org_integrations = integration_service.get_organization_integrations(
140+
organization_id=sentry_org_id,
141+
providers=[provider],
142+
status=ObjectStatus.ACTIVE,
143+
name=git_org_name,
144+
)
145+
if not github_org_integrations:
146+
return Response({"detail": "GitHub integration not found"}, status=404)
147+
148+
config = PreventAIConfiguration.objects.filter(
149+
organization_id=sentry_org_id,
150+
integration_id=github_org_integrations[0].integration_id,
151+
).first()
152+
153+
response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_DEFAULT)
154+
if config:
155+
response_data["organization"][git_org_name] = config.data
119156

120-
return Response(data={})
157+
return Response(data=response_data)
121158

122159

123160
@region_silo_endpoint

src/sentry/prevent/endpoints/pr_review_github_config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from sentry.integrations.types import IntegrationProviderSlug
1616
from sentry.models.organization import Organization
1717
from sentry.prevent.models import PreventAIConfiguration
18-
from sentry.prevent.types.config import ORG_CONFIG_SCHEMA, PREVENT_AI_CONFIG_GITHUB_DEFAULT
18+
from sentry.prevent.types.config import ORG_CONFIG_SCHEMA, PREVENT_AI_CONFIG_DEFAULT
1919

2020

2121
class PreventAIConfigPermission(OrganizationPermission):
@@ -59,9 +59,9 @@ def get(
5959
integration_id=github_org_integrations[0].integration_id,
6060
).first()
6161

62-
response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT)
62+
response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_DEFAULT)
6363
if config:
64-
response_data["github_organization"][git_organization_name] = config.data
64+
response_data["organization"][git_organization_name] = config.data
6565

6666
return Response(response_data, status=200)
6767

@@ -102,7 +102,7 @@ def put(
102102
},
103103
)
104104

105-
response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_GITHUB_DEFAULT)
106-
response_data["github_organization"][git_organization_name] = request.data
105+
response_data: dict[str, Any] = deepcopy(PREVENT_AI_CONFIG_DEFAULT)
106+
response_data["organization"][git_organization_name] = request.data
107107

108108
return Response(response_data, status=200)

src/sentry/prevent/types/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"additionalProperties": False,
9090
}
9191

92-
PREVENT_AI_CONFIG_GITHUB_DEFAULT = {
92+
PREVENT_AI_CONFIG_DEFAULT = {
9393
"schema_version": "v1",
9494
"default_org_config": {
9595
"org_defaults": {
@@ -119,5 +119,5 @@
119119
},
120120
"repo_overrides": {},
121121
},
122-
"github_organization": {},
122+
"organization": {},
123123
}

tests/sentry/overwatch/endpoints/test_overwatch_rpc.py

Lines changed: 237 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
import hashlib
22
import hmac
3+
from copy import deepcopy
34
from unittest.mock import patch
45

56
from django.urls import reverse
67

78
from sentry.constants import ObjectStatus
9+
from sentry.prevent.models import PreventAIConfiguration
10+
from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT
11+
from sentry.silo.base import SiloMode
812
from sentry.testutils.cases import APITestCase
13+
from sentry.testutils.silo import assume_test_silo_mode
14+
15+
VALID_ORG_CONFIG = {
16+
"schema_version": "v1",
17+
"org_defaults": {
18+
"bug_prediction": {
19+
"enabled": True,
20+
"sensitivity": "medium",
21+
"triggers": {"on_command_phrase": True, "on_ready_for_review": True},
22+
},
23+
"test_generation": {
24+
"enabled": False,
25+
"triggers": {"on_command_phrase": True, "on_ready_for_review": False},
26+
},
27+
"vanilla": {
28+
"enabled": False,
29+
"sensitivity": "medium",
30+
"triggers": {"on_command_phrase": True, "on_ready_for_review": False},
31+
},
32+
},
33+
"repo_overrides": {},
34+
}
935

1036

1137
class TestPreventPrReviewResolvedConfigsEndpoint(APITestCase):
@@ -29,14 +55,222 @@ def test_requires_auth(self):
2955
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
3056
["test-secret"],
3157
)
32-
def test_success_returns_default_config(self):
58+
def test_missing_sentry_org_id_returns_400(self):
59+
"""Test that missing sentryOrgId parameter returns 400."""
60+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
61+
auth = self._auth_header_for_get(url, {}, "test-secret")
62+
resp = self.client.get(url, HTTP_AUTHORIZATION=auth)
63+
assert resp.status_code == 400
64+
assert "sentryOrgId" in resp.data["detail"]
65+
66+
@patch(
67+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
68+
["test-secret"],
69+
)
70+
def test_invalid_sentry_org_id_returns_400(self):
71+
"""Test that invalid sentryOrgId (non-integer) returns 400."""
72+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
73+
params = {"sentryOrgId": "not-a-number", "gitOrgName": "test-org", "provider": "github"}
74+
auth = self._auth_header_for_get(url, params, "test-secret")
75+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
76+
assert resp.status_code == 400
77+
assert "must be a valid integer" in resp.data["detail"]
78+
79+
@patch(
80+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
81+
["test-secret"],
82+
)
83+
def test_negative_sentry_org_id_returns_400(self):
84+
"""Test that negative sentryOrgId returns 400."""
85+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
86+
params = {"sentryOrgId": "-123", "gitOrgName": "test-org", "provider": "github"}
87+
auth = self._auth_header_for_get(url, params, "test-secret")
88+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
89+
assert resp.status_code == 400
90+
assert "must be a positive integer" in resp.data["detail"]
91+
92+
@patch(
93+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
94+
["test-secret"],
95+
)
96+
def test_zero_sentry_org_id_returns_400(self):
97+
"""Test that zero sentryOrgId returns 400."""
98+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
99+
params = {"sentryOrgId": "0", "gitOrgName": "test-org", "provider": "github"}
100+
auth = self._auth_header_for_get(url, params, "test-secret")
101+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
102+
assert resp.status_code == 400
103+
assert "must be a positive integer" in resp.data["detail"]
104+
105+
@patch(
106+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
107+
["test-secret"],
108+
)
109+
def test_missing_git_org_name_returns_400(self):
110+
"""Test that missing gitOrgName parameter returns 400."""
111+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
112+
params = {"sentryOrgId": "123"}
113+
auth = self._auth_header_for_get(url, params, "test-secret")
114+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
115+
assert resp.status_code == 400
116+
assert "gitOrgName" in resp.data["detail"]
117+
118+
@patch(
119+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
120+
["test-secret"],
121+
)
122+
def test_missing_provider_returns_400(self):
123+
"""Test that missing provider parameter returns 400."""
124+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
125+
params = {"sentryOrgId": "123", "gitOrgName": "test-org"}
126+
auth = self._auth_header_for_get(url, params, "test-secret")
127+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
128+
assert resp.status_code == 400
129+
assert "provider" in resp.data["detail"]
130+
131+
@patch(
132+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
133+
["test-secret"],
134+
)
135+
def test_returns_default_when_no_config(self):
136+
"""Test that default config is returned when no configuration exists."""
137+
org = self.create_organization()
138+
git_org_name = "test-github-org"
139+
140+
with assume_test_silo_mode(SiloMode.CONTROL):
141+
self.create_integration(
142+
organization=org,
143+
provider="github",
144+
name=git_org_name,
145+
external_id=f"github:{git_org_name}",
146+
status=ObjectStatus.ACTIVE,
147+
)
148+
149+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
150+
params = {
151+
"sentryOrgId": str(org.id),
152+
"gitOrgName": git_org_name,
153+
"provider": "github",
154+
}
155+
auth = self._auth_header_for_get(url, params, "test-secret")
156+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
157+
assert resp.status_code == 200
158+
assert resp.data == PREVENT_AI_CONFIG_DEFAULT
159+
assert resp.data["organization"] == {}
160+
161+
@patch(
162+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
163+
["test-secret"],
164+
)
165+
def test_returns_config_when_exists(self):
166+
"""Test that saved configuration is returned when it exists."""
33167
org = self.create_organization()
168+
git_org_name = "test-github-org"
169+
170+
with assume_test_silo_mode(SiloMode.CONTROL):
171+
integration = self.create_integration(
172+
organization=org,
173+
provider="github",
174+
name=git_org_name,
175+
external_id=f"github:{git_org_name}",
176+
status=ObjectStatus.ACTIVE,
177+
)
178+
179+
PreventAIConfiguration.objects.create(
180+
organization_id=org.id,
181+
integration_id=integration.id,
182+
data=VALID_ORG_CONFIG,
183+
)
184+
34185
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
35-
params = {"sentryOrgId": str(org.id)}
186+
params = {
187+
"sentryOrgId": str(org.id),
188+
"gitOrgName": git_org_name,
189+
"provider": "github",
190+
}
36191
auth = self._auth_header_for_get(url, params, "test-secret")
37192
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
38193
assert resp.status_code == 200
39-
assert resp.data == {}
194+
assert resp.data["organization"][git_org_name] == VALID_ORG_CONFIG
195+
196+
@patch(
197+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
198+
["test-secret"],
199+
)
200+
def test_returns_404_when_integration_not_found(self):
201+
"""Test that 404 is returned when GitHub integration doesn't exist."""
202+
org = self.create_organization()
203+
204+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
205+
params = {
206+
"sentryOrgId": str(org.id),
207+
"gitOrgName": "nonexistent-org",
208+
"provider": "github",
209+
}
210+
auth = self._auth_header_for_get(url, params, "test-secret")
211+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
212+
assert resp.status_code == 404
213+
assert resp.data["detail"] == "GitHub integration not found"
214+
215+
@patch(
216+
"sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET",
217+
["test-secret"],
218+
)
219+
def test_config_with_repo_overrides(self):
220+
"""Test that configuration with repo overrides is properly retrieved."""
221+
org = self.create_organization()
222+
git_org_name = "test-github-org"
223+
224+
with assume_test_silo_mode(SiloMode.CONTROL):
225+
integration = self.create_integration(
226+
organization=org,
227+
provider="github",
228+
name=git_org_name,
229+
external_id=f"github:{git_org_name}",
230+
status=ObjectStatus.ACTIVE,
231+
)
232+
233+
config_with_overrides = deepcopy(VALID_ORG_CONFIG)
234+
config_with_overrides["repo_overrides"] = {
235+
"my-repo": {
236+
"bug_prediction": {
237+
"enabled": True,
238+
"sensitivity": "high",
239+
"triggers": {"on_command_phrase": True, "on_ready_for_review": False},
240+
},
241+
"test_generation": {
242+
"enabled": True,
243+
"triggers": {"on_command_phrase": True, "on_ready_for_review": True},
244+
},
245+
"vanilla": {
246+
"enabled": False,
247+
"sensitivity": "low",
248+
"triggers": {"on_command_phrase": False, "on_ready_for_review": False},
249+
},
250+
}
251+
}
252+
253+
PreventAIConfiguration.objects.create(
254+
organization_id=org.id,
255+
integration_id=integration.id,
256+
data=config_with_overrides,
257+
)
258+
259+
url = reverse("sentry-api-0-prevent-pr-review-configs-resolved")
260+
params = {
261+
"sentryOrgId": str(org.id),
262+
"gitOrgName": git_org_name,
263+
"provider": "github",
264+
}
265+
auth = self._auth_header_for_get(url, params, "test-secret")
266+
resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth)
267+
assert resp.status_code == 200
268+
assert (
269+
resp.data["organization"][git_org_name]["repo_overrides"]["my-repo"]["bug_prediction"][
270+
"sensitivity"
271+
]
272+
== "high"
273+
)
40274

41275

42276
class TestPreventPrReviewSentryOrgEndpoint(APITestCase):

0 commit comments

Comments
 (0)