Skip to content

Commit 1626fa6

Browse files
authored
chore(seer): Use seer quotas endpoints (#92132)
Redeploying reverted change now that the billing endpoints are ready
1 parent 337e3ab commit 1626fa6

File tree

11 files changed

+102
-21
lines changed

11 files changed

+102
-21
lines changed

src/sentry/api/endpoints/group_ai_summary.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry.api.api_publish_status import ApiPublishStatus
1111
from sentry.api.base import region_silo_endpoint
1212
from sentry.api.bases.group import GroupAiEndpoint
13+
from sentry.autofix.utils import SeerAutomationSource
1314
from sentry.models.group import Group
1415
from sentry.seer.issue_summary import get_issue_summary
1516
from sentry.types.ratelimit import RateLimit, RateLimitCategory
@@ -37,7 +38,10 @@ def post(self, request: Request, group: Group) -> Response:
3738
force_event_id = data.get("event_id", None)
3839

3940
summary_data, status_code = get_issue_summary(
40-
group=group, user=request.user, force_event_id=force_event_id, source="issue_details"
41+
group=group,
42+
user=request.user,
43+
force_event_id=force_event_id,
44+
source=SeerAutomationSource.ISSUE_DETAILS,
4145
)
4246

4347
return Response(summary_data, status=status_code)

src/sentry/api/endpoints/group_autofix_setup_check.py

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

10+
from sentry import quotas
1011
from sentry.api.api_owners import ApiOwner
1112
from sentry.api.api_publish_status import ApiPublishStatus
1213
from sentry.api.base import region_silo_endpoint
1314
from sentry.api.bases.group import GroupAiEndpoint
1415
from sentry.autofix.utils import get_autofix_repos_from_project_code_mappings
15-
from sentry.constants import ObjectStatus
16+
from sentry.constants import DataCategory, ObjectStatus
1617
from sentry.integrations.services.integration import integration_service
1718
from sentry.models.group import Group
1819
from sentry.models.organization import Organization
@@ -126,6 +127,11 @@ def get(self, request: Request, group: Group) -> Response:
126127
if not user_acknowledgement: # If the user has acknowledged, the org must have too.
127128
org_acknowledgement = get_seer_org_acknowledgement(org_id=org.id)
128129

130+
# TODO return BOTH trial status and autofix quota
131+
has_autofix_quota: bool = quotas.backend.has_available_reserved_budget(
132+
org_id=org.id, data_category=DataCategory.SEER_AUTOFIX
133+
)
134+
129135
return Response(
130136
{
131137
"integration": {
@@ -137,5 +143,8 @@ def get(self, request: Request, group: Group) -> Response:
137143
"orgHasAcknowledged": org_acknowledgement,
138144
"userHasAcknowledged": user_acknowledgement,
139145
},
146+
"billing": {
147+
"hasAutofixQuota": has_autofix_quota,
148+
},
140149
}
141150
)

src/sentry/autofix/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,9 @@ def get_autofix_state_from_pr_id(provider: str, pr_id: int) -> AutofixState | No
146146
return None
147147

148148
return AutofixState.validate(result.get("state", None))
149+
150+
151+
class SeerAutomationSource(enum.Enum):
152+
ISSUE_DETAILS = "issue_details"
153+
ALERT = "alert"
154+
POST_PROCESS = "post_process"

src/sentry/integrations/utils/issue_summary_for_alerts.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sentry_sdk
66

77
from sentry import features, options
8+
from sentry.autofix.utils import SeerAutomationSource
89
from sentry.issues.grouptype import GroupCategory
910
from sentry.models.group import Group
1011
from sentry.seer.issue_summary import get_issue_summary
@@ -32,7 +33,9 @@ def fetch_issue_summary(group: Group) -> dict[str, Any] | None:
3233
try:
3334
with sentry_sdk.start_span(op="ai_summary.fetch_issue_summary_for_alert"):
3435
with concurrent.futures.ThreadPoolExecutor() as executor:
35-
future = executor.submit(get_issue_summary, group, source="alert")
36+
future = executor.submit(
37+
get_issue_summary, group, source=SeerAutomationSource.ALERT
38+
)
3639
summary_result, status_code = future.result(timeout=timeout)
3740

3841
if status_code == 200:

src/sentry/seer/autofix.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from django.utils import timezone
1212
from rest_framework.response import Response
1313

14-
from sentry import eventstore, features
14+
from sentry import eventstore, features, quotas
1515
from sentry.api.serializers import EventSerializer, serialize
1616
from sentry.autofix.utils import get_autofix_repos_from_project_code_mappings
17-
from sentry.constants import ObjectStatus
17+
from sentry.constants import DataCategory, ObjectStatus
1818
from sentry.eventstore.models import Event, GroupEvent
1919
from sentry.models.group import Group
2020
from sentry.models.project import Project
@@ -859,6 +859,14 @@ def trigger_autofix(
859859
403,
860860
)
861861

862+
# check billing quota for autofix
863+
has_budget: bool = quotas.backend.has_available_reserved_budget(
864+
org_id=group.organization.id,
865+
data_category=DataCategory.SEER_AUTOFIX,
866+
)
867+
if not has_budget:
868+
return _respond_with_error("No budget for Seer Autofix.", 402)
869+
862870
if event_id is None:
863871
event: Event | GroupEvent | None = group.get_recommended_event_for_environments()
864872
if not event:
@@ -928,6 +936,11 @@ def trigger_autofix(
928936

929937
group.update(seer_autofix_last_triggered=timezone.now())
930938

939+
# log billing event for seer autofix
940+
quotas.backend.record_seer_run(
941+
group.organization.id, group.project.id, DataCategory.SEER_AUTOFIX
942+
)
943+
931944
return Response(
932945
{
933946
"run_id": run_id,

src/sentry/seer/issue_summary.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
from django.conf import settings
1111
from django.contrib.auth.models import AnonymousUser
1212

13-
from sentry import eventstore, features
13+
from sentry import eventstore, features, quotas
1414
from sentry.api.serializers import EventSerializer, serialize
1515
from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case
16-
from sentry.autofix.utils import get_autofix_state
17-
from sentry.constants import ObjectStatus
16+
from sentry.autofix.utils import SeerAutomationSource, get_autofix_state
17+
from sentry.constants import DataCategory, ObjectStatus
1818
from sentry.eventstore.models import Event, GroupEvent
1919
from sentry.locks import locks
2020
from sentry.models.group import Group
@@ -254,7 +254,7 @@ def _run_automation(
254254
group: Group,
255255
user: User | RpcUser | AnonymousUser,
256256
event: GroupEvent,
257-
source: str,
257+
source: SeerAutomationSource,
258258
):
259259
if features.has(
260260
"organizations:trigger-autofix-on-issue-summary", group.organization, actor=user
@@ -282,8 +282,9 @@ def _run_automation(
282282
not autofix_state
283283
): # Only trigger autofix if we don't have an autofix on this issue already.
284284
auto_run_source_map = {
285-
"issue_details": "issue_summary_fixability",
286-
"alert": "issue_summary_on_alert_fixability",
285+
SeerAutomationSource.ISSUE_DETAILS: "issue_summary_fixability",
286+
SeerAutomationSource.ALERT: "issue_summary_on_alert_fixability",
287+
SeerAutomationSource.POST_PROCESS: "issue_summary_on_post_process_fixability",
287288
}
288289
_trigger_autofix_task.delay(
289290
group_id=group.id,
@@ -297,7 +298,7 @@ def _generate_summary(
297298
group: Group,
298299
user: User | RpcUser | AnonymousUser,
299300
force_event_id: str | None,
300-
source: str,
301+
source: SeerAutomationSource,
301302
cache_key: str,
302303
) -> tuple[dict[str, Any], int]:
303304
"""Core logic to generate and cache the issue summary."""
@@ -335,11 +336,29 @@ def _generate_summary(
335336
return summary_dict, 200
336337

337338

339+
def _log_seer_scanner_billing_event(group: Group, source: SeerAutomationSource):
340+
if source == SeerAutomationSource.ISSUE_DETAILS:
341+
return
342+
343+
quotas.backend.record_seer_run(
344+
group.organization.id, group.project.id, DataCategory.SEER_SCANNER
345+
)
346+
347+
348+
def _has_seer_scanner_budget(group: Group, source: SeerAutomationSource) -> bool:
349+
if source == SeerAutomationSource.ISSUE_DETAILS:
350+
return True
351+
352+
return quotas.backend.has_available_reserved_budget(
353+
org_id=group.organization.id, data_category=DataCategory.SEER_SCANNER
354+
)
355+
356+
338357
def get_issue_summary(
339358
group: Group,
340359
user: User | RpcUser | AnonymousUser | None = None,
341360
force_event_id: str | None = None,
342-
source: str = "issue_details",
361+
source: SeerAutomationSource = SeerAutomationSource.ISSUE_DETAILS,
343362
) -> tuple[dict[str, Any], int]:
344363
"""
345364
Generate an AI summary for an issue.
@@ -361,6 +380,9 @@ def get_issue_summary(
361380
if not get_seer_org_acknowledgement(group.organization.id):
362381
return {"detail": "AI Autofix has not been acknowledged by the organization."}, 403
363382

383+
if not _has_seer_scanner_budget(group, source):
384+
return {"detail": "No budget for Seer Scanner."}, 402
385+
364386
cache_key = f"ai-group-summary-v2:{group.id}"
365387
lock_key = f"ai-group-summary-v2-lock:{group.id}"
366388
lock_duration = 10 # How long the lock is held if acquired (seconds)
@@ -371,6 +393,7 @@ def get_issue_summary(
371393
summary_dict, status_code = _generate_summary(
372394
group, user, force_event_id, source, cache_key
373395
)
396+
_log_seer_scanner_billing_event(group, source)
374397
return convert_dict_key_case(summary_dict, snake_to_camel_case), status_code
375398

376399
# 1. Check cache first
@@ -392,6 +415,7 @@ def get_issue_summary(
392415
summary_dict, status_code = _generate_summary(
393416
group, user, force_event_id, source, cache_key
394417
)
418+
_log_seer_scanner_billing_event(group, source)
395419
return convert_dict_key_case(summary_dict, snake_to_camel_case), status_code
396420

397421
except UnableToAcquireLock:

src/sentry/tasks/autofix.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from datetime import datetime, timedelta
33

4-
from sentry.autofix.utils import AutofixStatus, get_autofix_state
4+
from sentry.autofix.utils import AutofixStatus, SeerAutomationSource, get_autofix_state
55
from sentry.models.group import Group
66
from sentry.tasks.base import instrumented_task
77
from sentry.taskworker.config import TaskworkerConfig
@@ -51,4 +51,4 @@ def start_seer_automation(group_id: int):
5151
from sentry.seer.issue_summary import get_issue_summary
5252

5353
group = Group.objects.get(id=group_id)
54-
get_issue_summary(group=group, source="post_process")
54+
get_issue_summary(group=group, source=SeerAutomationSource.POST_PROCESS)

tests/sentry/api/endpoints/test_group_ai_summary.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest.mock import ANY, patch
22

3+
from sentry.autofix.utils import SeerAutomationSource
34
from sentry.testutils.cases import APITestCase, SnubaTestCase
45
from sentry.testutils.helpers.features import apply_feature_flag_on_cls
56
from sentry.testutils.skips import requires_snuba
@@ -28,7 +29,10 @@ def test_endpoint_calls_get_issue_summary(self, mock_get_issue_summary):
2829
assert response.status_code == 200
2930
assert response.data == mock_summary_data
3031
mock_get_issue_summary.assert_called_once_with(
31-
group=self.group, user=ANY, force_event_id="test_event_id", source="issue_details"
32+
group=self.group,
33+
user=ANY,
34+
force_event_id="test_event_id",
35+
source=SeerAutomationSource.ISSUE_DETAILS,
3236
)
3337

3438
@patch("sentry.api.endpoints.group_ai_summary.get_issue_summary")
@@ -41,7 +45,10 @@ def test_endpoint_without_event_id(self, mock_get_issue_summary):
4145
assert response.status_code == 200
4246
assert response.data == mock_summary_data
4347
mock_get_issue_summary.assert_called_once_with(
44-
group=self.group, user=ANY, force_event_id=None, source="issue_details"
48+
group=self.group,
49+
user=ANY,
50+
force_event_id=None,
51+
source=SeerAutomationSource.ISSUE_DETAILS,
4552
)
4653

4754
@patch("sentry.api.endpoints.group_ai_summary.get_issue_summary")
@@ -54,5 +61,8 @@ def test_endpoint_with_error_response(self, mock_get_issue_summary):
5461
assert response.status_code == 400
5562
assert response.data == error_data
5663
mock_get_issue_summary.assert_called_once_with(
57-
group=self.group, user=ANY, force_event_id=None, source="issue_details"
64+
group=self.group,
65+
user=ANY,
66+
force_event_id=None,
67+
source=SeerAutomationSource.ISSUE_DETAILS,
5868
)

tests/sentry/api/endpoints/test_group_autofix_setup_check.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ def test_successful_setup(self):
5555
"orgHasAcknowledged": False,
5656
"userHasAcknowledged": False,
5757
},
58+
"billing": {
59+
"hasAutofixQuota": True,
60+
},
5861
}
5962

6063
def test_current_user_acknowledged_setup(self):
@@ -154,6 +157,9 @@ def test_successful_with_write_access(self, mock_get_repos_and_access):
154157
"orgHasAcknowledged": False,
155158
"userHasAcknowledged": False,
156159
},
160+
"billing": {
161+
"hasAutofixQuota": True,
162+
},
157163
}
158164

159165

tests/sentry/integrations/slack/test_message_builder.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.urls import reverse
99
from urllib3.response import HTTPResponse
1010

11+
from sentry.autofix.utils import SeerAutomationSource
1112
from sentry.eventstore.models import Event
1213
from sentry.grouping.grouptype import ErrorGroupType
1314
from sentry.incidents.logic import CRITICAL_TRIGGER_LABEL
@@ -1008,7 +1009,7 @@ def test_build_group_block_with_ai_summary_with_feature_flag(
10081009

10091010
blocks = SlackIssuesMessageBuilder(group).build()
10101011

1011-
mock_get_summary.assert_called_once_with(group, source="alert")
1012+
mock_get_summary.assert_called_once_with(group, source=SeerAutomationSource.ALERT)
10121013

10131014
# Verify that the original title is \\ present
10141015
title_text = blocks["blocks"][0]["elements"][0]["elements"][-1]["text"]

tests/sentry/seer/test_issue_summary.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from unittest.mock import ANY, Mock, call, patch
55

66
from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case
7+
from sentry.autofix.utils import SeerAutomationSource
78
from sentry.locks import locks
89
from sentry.seer.issue_summary import (
910
_get_event,
@@ -548,7 +549,9 @@ def test_run_automation_saves_fixability_score(
548549
self.group.refresh_from_db()
549550
assert self.group.seer_fixability_score is None
550551

551-
_run_automation(self.group, mock_user, mock_event, source="issue_details")
552+
_run_automation(
553+
self.group, mock_user, mock_event, source=SeerAutomationSource.ISSUE_DETAILS
554+
)
552555

553556
mock_generate_fixability_score.assert_called_once_with(self.group.id)
554557

@@ -613,7 +616,9 @@ def test_is_issue_fixable_triggers_autofix(
613616

614617
with self.subTest(option=option_value, score=score, should_trigger=should_trigger):
615618
self.group.project.update_option("sentry:autofix_automation_tuning", option_value)
616-
_run_automation(self.group, mock_user, mock_event, source="issue_details")
619+
_run_automation(
620+
self.group, mock_user, mock_event, source=SeerAutomationSource.ISSUE_DETAILS
621+
)
617622

618623
mock_generate_fixability_score.assert_called_once_with(self.group.id)
619624
self.group.refresh_from_db()

0 commit comments

Comments
 (0)