Skip to content

Commit 02f768c

Browse files
removed import
1 parent 050df12 commit 02f768c

File tree

5 files changed

+162
-21
lines changed

5 files changed

+162
-21
lines changed

src/sentry/seer/autofix/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,24 @@ def get_project_seer_preferences(project_id: int):
173173
raise SeerApiError(response.data.decode("utf-8"), response.status)
174174

175175

176+
def has_project_connected_repos(organization_id: int, project_id: int) -> bool:
177+
"""
178+
Check if a project has connected repositories in Seer.
179+
Results are cached for 60 minutes to minimize API calls.
180+
"""
181+
cache_key = f"seer-project-has-repos:{organization_id}:{project_id}"
182+
cached_value = cache.get(cache_key)
183+
184+
if cached_value is not None:
185+
return cached_value
186+
187+
project_preferences = get_project_seer_preferences(project_id)
188+
has_repos = bool(project_preferences.code_mapping_repos)
189+
190+
cache.set(cache_key, has_repos, timeout=60 * 60) # Cache for 1 hour
191+
return has_repos
192+
193+
176194
def bulk_get_project_preferences(organization_id: int, project_ids: list[int]) -> dict[str, dict]:
177195
"""Bulk fetch Seer project preferences. Returns dict mapping project ID (string) to preference dict."""
178196
path = "/v1/project-preference/bulk"

src/sentry/seer/endpoints/group_autofix_setup_check.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.conf import settings
88
from rest_framework.response import Response
99

10-
from sentry import features, quotas
10+
from sentry import quotas
1111
from sentry.api.api_owners import ApiOwner
1212
from sentry.api.api_publish_status import ApiPublishStatus
1313
from sentry.api.base import region_silo_endpoint
@@ -135,15 +135,6 @@ def get(self, request: Request, group: Group) -> Response:
135135
organization=org, project=group.project
136136
)
137137

138-
# Customers on new pricing need to have github configured to use Autofix.
139-
# Check if project has code mappings configured (feature flagged)
140-
if integration_check is None and features.has(
141-
"organizations:triage-signals-v0-org", org
142-
):
143-
repos_from_mappings = get_autofix_repos_from_project_code_mappings(group.project)
144-
if not repos_from_mappings:
145-
integration_check = "code_mappings_missing"
146-
147138
write_integration_check = None
148139
if request.query_params.get("check_write_access", False):
149140
repos = get_repos_and_access(group.project, group.id)

src/sentry/tasks/post_process.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,7 +1606,6 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
16061606
get_issue_summary_lock_key,
16071607
)
16081608
from sentry.seer.autofix.utils import (
1609-
get_autofix_repos_from_project_code_mappings,
16101609
is_issue_eligible_for_seer_automation,
16111610
is_seer_scanner_rate_limited,
16121611
)
@@ -1684,15 +1683,17 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
16841683
if not is_issue_eligible_for_seer_automation(group):
16851684
return
16861685

1687-
# Check if project has connected repositories - requirement for new pricing
1688-
if not get_autofix_repos_from_project_code_mappings(group.project):
1689-
return
1690-
16911686
# Atomically set cache to prevent duplicate dispatches (returns False if key exists)
16921687
automation_dispatch_cache_key = f"seer-automation-dispatched:{group.id}"
16931688
if not cache.add(automation_dispatch_cache_key, True, timeout=300):
16941689
return # Another process already dispatched automation
16951690

1691+
# Check if project has connected repositories - requirement for new pricing
1692+
from sentry.seer.autofix.utils import has_project_connected_repos
1693+
1694+
if not has_project_connected_repos(group.organization.id, group.project.id):
1695+
return
1696+
16961697
# Check if summary exists in cache
16971698
cache_key = get_issue_summary_cache_key(group.id)
16981699
if cache.get(cache_key) is not None:

tests/sentry/seer/autofix/test_autofix_utils.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CodingAgentStatus,
1212
get_autofix_prompt,
1313
get_coding_agent_prompt,
14+
has_project_connected_repos,
1415
is_issue_eligible_for_seer_automation,
1516
is_seer_seat_based_tier_enabled,
1617
)
@@ -403,3 +404,84 @@ def test_returns_cached_value(self):
403404
# Even without feature flags enabled, should return cached True
404405
result = is_seer_seat_based_tier_enabled(self.organization)
405406
assert result is True
407+
408+
409+
class TestHasProjectConnectedRepos(TestCase):
410+
"""Test the has_project_connected_repos function."""
411+
412+
def setUp(self):
413+
super().setUp()
414+
self.organization = self.create_organization()
415+
self.project = self.create_project(organization=self.organization)
416+
417+
@patch("sentry.seer.autofix.utils.cache")
418+
@patch("sentry.seer.autofix.utils.get_project_seer_preferences")
419+
def test_returns_true_when_repos_exist(self, mock_get_preferences, mock_cache):
420+
"""Test returns True when project has connected repositories."""
421+
mock_cache.get.return_value = None
422+
mock_preferences = Mock()
423+
mock_preferences.code_mapping_repos = [
424+
{"provider": "github", "owner": "test", "name": "repo"}
425+
]
426+
mock_get_preferences.return_value = mock_preferences
427+
428+
result = has_project_connected_repos(self.organization.id, self.project.id)
429+
430+
assert result is True
431+
mock_cache.set.assert_called_once_with(
432+
f"seer-project-has-repos:{self.organization.id}:{self.project.id}",
433+
True,
434+
timeout=60 * 60,
435+
)
436+
437+
@patch("sentry.seer.autofix.utils.cache")
438+
@patch("sentry.seer.autofix.utils.get_project_seer_preferences")
439+
def test_returns_false_when_no_repos(self, mock_get_preferences, mock_cache):
440+
"""Test returns False when project has no connected repositories."""
441+
mock_cache.get.return_value = None
442+
mock_preferences = Mock()
443+
mock_preferences.code_mapping_repos = []
444+
mock_get_preferences.return_value = mock_preferences
445+
446+
result = has_project_connected_repos(self.organization.id, self.project.id)
447+
448+
assert result is False
449+
mock_cache.set.assert_called_once_with(
450+
f"seer-project-has-repos:{self.organization.id}:{self.project.id}",
451+
False,
452+
timeout=60 * 60,
453+
)
454+
455+
@patch("sentry.seer.autofix.utils.cache")
456+
@patch("sentry.seer.autofix.utils.get_project_seer_preferences")
457+
def test_returns_cached_value_true(self, mock_get_preferences, mock_cache):
458+
"""Test returns cached True value without calling API."""
459+
mock_cache.get.return_value = True
460+
461+
result = has_project_connected_repos(self.organization.id, self.project.id)
462+
463+
assert result is True
464+
mock_get_preferences.assert_not_called()
465+
mock_cache.set.assert_not_called()
466+
467+
@patch("sentry.seer.autofix.utils.cache")
468+
@patch("sentry.seer.autofix.utils.get_project_seer_preferences")
469+
def test_returns_cached_value_false(self, mock_get_preferences, mock_cache):
470+
"""Test returns cached False value without calling API."""
471+
mock_cache.get.return_value = False
472+
473+
result = has_project_connected_repos(self.organization.id, self.project.id)
474+
475+
assert result is False
476+
mock_get_preferences.assert_not_called()
477+
mock_cache.set.assert_not_called()
478+
479+
@patch("sentry.seer.autofix.utils.cache")
480+
@patch("sentry.seer.autofix.utils.get_project_seer_preferences")
481+
def test_raises_on_api_error(self, mock_get_preferences, mock_cache):
482+
"""Test raises SeerApiError when API call fails."""
483+
mock_cache.get.return_value = None
484+
mock_get_preferences.side_effect = SeerApiError("API Error", 500)
485+
486+
with pytest.raises(SeerApiError):
487+
has_project_connected_repos(self.organization.id, self.project.id)

tests/sentry/tasks/test_post_process.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,15 +3109,15 @@ def test_triage_signals_event_count_less_than_10_with_cache(
31093109
return_value=True,
31103110
)
31113111
@patch(
3112-
"sentry.seer.autofix.utils.get_autofix_repos_from_project_code_mappings",
3113-
return_value=[{"name": "test-repo"}],
3112+
"sentry.seer.autofix.utils.has_project_connected_repos",
3113+
return_value=True,
31143114
)
31153115
@patch("sentry.tasks.autofix.run_automation_only_task.delay")
31163116
@with_feature(
31173117
{"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True}
31183118
)
31193119
def test_triage_signals_event_count_gte_10_with_cache(
3120-
self, mock_run_automation, mock_get_repos, mock_get_seer_org_acknowledgement
3120+
self, mock_run_automation, mock_has_repos, mock_get_seer_org_acknowledgement
31213121
):
31223122
"""Test that with event count >= 10 and cached summary exists, we run automation directly."""
31233123
self.project.update_option("sentry:seer_scanner_automation", True)
@@ -3162,8 +3162,8 @@ def mock_buffer_get(model, columns, filters):
31623162
return_value=True,
31633163
)
31643164
@patch(
3165-
"sentry.seer.autofix.utils.get_autofix_repos_from_project_code_mappings",
3166-
return_value=[{"name": "test-repo"}],
3165+
"sentry.seer.autofix.utils.has_project_connected_repos",
3166+
return_value=True,
31673167
)
31683168
@patch("sentry.tasks.autofix.generate_summary_and_run_automation.delay")
31693169
@with_feature(
@@ -3172,7 +3172,7 @@ def mock_buffer_get(model, columns, filters):
31723172
def test_triage_signals_event_count_gte_10_no_cache(
31733173
self,
31743174
mock_generate_summary_and_run_automation,
3175-
mock_get_repos,
3175+
mock_has_repos,
31763176
mock_get_seer_org_acknowledgement,
31773177
):
31783178
"""Test that with event count >= 10 and no cached summary, we generate summary + run automation."""
@@ -3207,6 +3207,55 @@ def mock_buffer_get(model, columns, filters):
32073207
# Should call generate_summary_and_run_automation to generate summary + run automation
32083208
mock_generate_summary_and_run_automation.assert_called_once_with(group.id)
32093209

3210+
@patch(
3211+
"sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner",
3212+
return_value=True,
3213+
)
3214+
@patch(
3215+
"sentry.seer.autofix.utils.has_project_connected_repos",
3216+
return_value=False,
3217+
)
3218+
@patch("sentry.tasks.autofix.generate_summary_and_run_automation.delay")
3219+
@with_feature(
3220+
{"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True}
3221+
)
3222+
def test_triage_signals_event_count_gte_10_skips_without_connected_repos(
3223+
self,
3224+
mock_generate_summary_and_run_automation,
3225+
mock_has_repos,
3226+
mock_get_seer_org_acknowledgement,
3227+
):
3228+
"""Test that with event count >= 10 but no connected repos, we skip automation."""
3229+
self.project.update_option("sentry:seer_scanner_automation", True)
3230+
self.project.update_option("sentry:autofix_automation_tuning", "always")
3231+
event = self.create_event(
3232+
data={"message": "testing"},
3233+
project_id=self.project.id,
3234+
)
3235+
3236+
# Update group times_seen to simulate >= 10 events
3237+
group = event.group
3238+
group.times_seen = 1
3239+
group.save()
3240+
event.group.times_seen = 1
3241+
3242+
# Mock buffer backend to return pending increments
3243+
from sentry import buffer
3244+
3245+
def mock_buffer_get(model, columns, filters):
3246+
return {"times_seen": 9}
3247+
3248+
with patch.object(buffer.backend, "get", side_effect=mock_buffer_get):
3249+
self.call_post_process_group(
3250+
is_new=False,
3251+
is_regression=False,
3252+
is_new_group_environment=False,
3253+
event=event,
3254+
)
3255+
3256+
# Should not call automation since no connected repos
3257+
mock_generate_summary_and_run_automation.assert_not_called()
3258+
32103259
@patch(
32113260
"sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner",
32123261
return_value=True,

0 commit comments

Comments
 (0)