Skip to content

Commit 9084a23

Browse files
authored
chore(seer): Track rate limited outcomes (#94358)
Emits a rate limited outcome when Seer scanner or auto-Autofix are rate limited. Refactors the rate limiting utils a bit to encapsulate the logging and outcome logic in one place.
1 parent cc85440 commit 9084a23

File tree

7 files changed

+83
-77
lines changed

7 files changed

+83
-77
lines changed

src/sentry/api/endpoints/group_autofix_setup_check.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ def get(self, request: Request, group: Group) -> Response:
138138
if not user_acknowledgement: # If the user has acknowledged, the org must have too.
139139
org_acknowledgement = get_seer_org_acknowledgement(org_id=org.id)
140140

141-
# TODO return BOTH trial status and autofix quota
142141
has_autofix_quota: bool = quotas.backend.has_available_reserved_budget(
143142
org_id=org.id, data_category=DataCategory.SEER_AUTOFIX
144143
)

src/sentry/autofix/utils.py

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import datetime
21
import enum
2+
import logging
3+
from datetime import UTC, datetime
34
from typing import TypedDict
45

56
import orjson
@@ -8,13 +9,17 @@
89
from pydantic import BaseModel
910

1011
from sentry import features, options, ratelimits
12+
from sentry.constants import DataCategory
1113
from sentry.issues.auto_source_code_config.code_mapping import get_sorted_code_mapping_configs
1214
from sentry.models.organization import Organization
1315
from sentry.models.project import Project
1416
from sentry.models.repository import Repository
1517
from sentry.seer.seer_utils import AutofixAutomationTuningSettings
1618
from sentry.seer.signed_seer_api import sign_with_seer_secret
1719
from sentry.utils import json
20+
from sentry.utils.outcomes import Outcome, track_outcome
21+
22+
logger = logging.getLogger(__name__)
1823

1924

2025
class AutofixIssue(TypedDict):
@@ -51,7 +56,7 @@ class CodebaseState(BaseModel):
5156
class AutofixState(BaseModel):
5257
run_id: int
5358
request: AutofixRequest
54-
updated_at: datetime.datetime
59+
updated_at: datetime
5560
status: AutofixStatus
5661
actor_ids: list[str] | None = None
5762
codebases: dict[str, CodebaseState] = {}
@@ -162,20 +167,20 @@ class SeerAutomationSource(enum.Enum):
162167
POST_PROCESS = "post_process"
163168

164169

165-
def is_seer_scanner_rate_limited(
166-
project: Project, organization: Organization
167-
) -> tuple[bool, int, int]:
170+
def is_seer_scanner_rate_limited(project: Project, organization: Organization) -> bool:
168171
"""
169172
Check if Seer Scanner automation is rate limited for a given project and organization.
173+
Calling this method increments the counter used to enforce the rate limit, and tracks rate limited outcomes.
174+
175+
Args:
176+
project: The project to check.
177+
organization: The organization to check.
170178
171179
Returns:
172-
tuple[bool, int, int]:
173-
- is_rate_limited: Whether the seer scanner is rate limited.
174-
- current: The current number of seer scanner runs.
175-
- limit: The limit for seer scanner runs.
180+
bool: Whether the seer scanner is rate limited.
176181
"""
177182
if features.has("organizations:unlimited-auto-triggered-autofix-runs", organization):
178-
return False, 0, 0
183+
return False
179184

180185
limit = options.get("seer.max_num_scanner_autotriggered_per_minute", 50)
181186
is_rate_limited, current, _ = ratelimits.backend.is_limited_with_value(
@@ -184,7 +189,26 @@ def is_seer_scanner_rate_limited(
184189
limit=limit,
185190
window=60, # 1 minute
186191
)
187-
return is_rate_limited, current, limit
192+
if is_rate_limited:
193+
logger.info(
194+
"Seer scanner auto-trigger rate limit hit",
195+
extra={
196+
"org_slug": organization.slug,
197+
"project_slug": project.slug,
198+
"scanner_run_count": current,
199+
"scanner_run_limit": limit,
200+
},
201+
)
202+
track_outcome(
203+
org_id=organization.id,
204+
project_id=project.id,
205+
key_id=None,
206+
outcome=Outcome.RATE_LIMITED,
207+
reason="rate_limited",
208+
timestamp=datetime.now(UTC),
209+
category=DataCategory.SEER_SCANNER,
210+
)
211+
return is_rate_limited
188212

189213

190214
AUTOFIX_AUTOTRIGGED_RATE_LIMIT_OPTION_MULTIPLIERS = {
@@ -200,18 +224,20 @@ def is_seer_scanner_rate_limited(
200224

201225
def is_seer_autotriggered_autofix_rate_limited(
202226
project: Project, organization: Organization
203-
) -> tuple[bool, int, int]:
227+
) -> bool:
204228
"""
205229
Check if Seer Autofix automation is rate limited for a given project and organization.
230+
Calling this method increments the counter used to enforce the rate limit, and tracks rate limited outcomes.
231+
232+
Args:
233+
project: The project to check.
234+
organization: The organization to check.
206235
207236
Returns:
208-
tuple[bool, int, int]:
209-
- is_rate_limited: Whether Autofix is rate limited.
210-
- current: The current number of Autofix runs.
211-
- limit: The limit for Autofix runs.
237+
bool: Whether Autofix is rate limited.
212238
"""
213239
if features.has("organizations:unlimited-auto-triggered-autofix-runs", organization):
214-
return False, 0, 0
240+
return False
215241

216242
limit = options.get("seer.max_num_autofix_autotriggered_per_hour", 20)
217243

@@ -228,4 +254,23 @@ def is_seer_autotriggered_autofix_rate_limited(
228254
limit=limit,
229255
window=60 * 60, # 1 hour
230256
)
231-
return is_rate_limited, current, limit
257+
if is_rate_limited:
258+
logger.info(
259+
"Autofix auto-trigger rate limit hit",
260+
extra={
261+
"auto_run_count": current,
262+
"auto_run_limit": limit,
263+
"org_slug": organization.slug,
264+
"project_slug": project.slug,
265+
},
266+
)
267+
track_outcome(
268+
org_id=organization.id,
269+
project_id=project.id,
270+
key_id=None,
271+
outcome=Outcome.RATE_LIMITED,
272+
reason="rate_limited",
273+
timestamp=datetime.now(UTC),
274+
category=DataCategory.SEER_AUTOFIX,
275+
)
276+
return is_rate_limited

src/sentry/integrations/utils/issue_summary_for_alerts.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,7 @@ def fetch_issue_summary(group: Group) -> dict[str, Any] | None:
3131
if not get_seer_org_acknowledgement(org_id=group.organization.id):
3232
return None
3333

34-
is_rate_limited, current, limit = is_seer_scanner_rate_limited(project, group.organization)
35-
if is_rate_limited:
36-
logger.warning(
37-
"Seer scanner auto-trigger rate limit hit",
38-
extra={
39-
"org_slug": group.organization.slug,
40-
"project_slug": project.slug,
41-
"group_id": group.id,
42-
"scanner_run_count": current,
43-
"scanner_run_limit": limit,
44-
},
45-
)
34+
if is_seer_scanner_rate_limited(project, group.organization):
4635
return None
4736

4837
from sentry import quotas

src/sentry/seer/issue_summary.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -318,20 +318,8 @@ def _run_automation(
318318
if autofix_state:
319319
return # already have an autofix on this issue
320320

321-
is_rate_limited, current, limit = is_seer_autotriggered_autofix_rate_limited(
322-
group.project, group.organization
323-
)
321+
is_rate_limited = is_seer_autotriggered_autofix_rate_limited(group.project, group.organization)
324322
if is_rate_limited:
325-
logger.warning(
326-
"Autofix auto-trigger rate limit hit",
327-
extra={
328-
"group_id": group.id,
329-
"auto_run_count": current,
330-
"auto_run_limit": limit,
331-
"org_slug": group.organization.slug,
332-
"project_slug": group.project.slug,
333-
},
334-
)
335323
return
336324

337325
_trigger_autofix_task.delay(

src/sentry/tasks/post_process.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,18 +1617,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
16171617

16181618
from sentry.autofix.utils import is_seer_scanner_rate_limited
16191619

1620-
is_rate_limited, current, limit = is_seer_scanner_rate_limited(project, group.organization)
1621-
if is_rate_limited:
1622-
logger.warning(
1623-
"Seer scanner auto-trigger rate limit hit",
1624-
extra={
1625-
"org_slug": group.organization.slug,
1626-
"project_slug": project.slug,
1627-
"group_id": group.id,
1628-
"scanner_run_count": current,
1629-
"scanner_run_limit": limit,
1630-
},
1631-
)
1620+
if is_seer_scanner_rate_limited(project, group.organization):
16321621
return
16331622

16341623
start_seer_automation.delay(group.id)

tests/sentry/autofix/test_utils.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -191,48 +191,47 @@ def test_get_autofix_state_http_error(self, mock_post):
191191

192192
class TestAutomationRateLimiting(TestCase):
193193
@with_feature("organizations:unlimited-auto-triggered-autofix-runs")
194-
def test_scanner_rate_limited_with_unlimited_flag(self):
194+
@patch("sentry.autofix.utils.track_outcome")
195+
def test_scanner_rate_limited_with_unlimited_flag(self, mock_track_outcome):
195196
"""Test scanner rate limiting bypassed with unlimited feature flag"""
196197
project = self.create_project()
197198
organization = project.organization
198199

199-
is_rate_limited, current, limit = is_seer_scanner_rate_limited(project, organization)
200+
is_rate_limited = is_seer_scanner_rate_limited(project, organization)
200201

201202
assert is_rate_limited is False
202-
assert current == 0
203-
assert limit == 0
203+
mock_track_outcome.assert_not_called()
204204

205205
@patch("sentry.autofix.utils.ratelimits.backend.is_limited_with_value")
206-
def test_scanner_rate_limited_logic(self, mock_is_limited):
206+
@patch("sentry.autofix.utils.track_outcome")
207+
def test_scanner_rate_limited_logic(self, mock_track_outcome, mock_is_limited):
207208
"""Test scanner rate limiting logic"""
208209
project = self.create_project()
209210
organization = project.organization
210211

211212
mock_is_limited.return_value = (True, 45, None)
212213

213214
with self.options({"seer.max_num_scanner_autotriggered_per_minute": 50}):
214-
is_rate_limited, current, limit = is_seer_scanner_rate_limited(project, organization)
215+
is_rate_limited = is_seer_scanner_rate_limited(project, organization)
215216

216217
assert is_rate_limited is True
217-
assert current == 45
218-
assert limit == 50
218+
mock_track_outcome.assert_called_once()
219219

220220
@with_feature("organizations:unlimited-auto-triggered-autofix-runs")
221-
def test_autofix_rate_limited_with_unlimited_flag(self):
221+
@patch("sentry.autofix.utils.track_outcome")
222+
def test_autofix_rate_limited_with_unlimited_flag(self, mock_track_outcome):
222223
"""Test autofix rate limiting bypassed with unlimited feature flag"""
223224
project = self.create_project()
224225
organization = project.organization
225226

226-
is_rate_limited, current, limit = is_seer_autotriggered_autofix_rate_limited(
227-
project, organization
228-
)
227+
is_rate_limited = is_seer_autotriggered_autofix_rate_limited(project, organization)
229228

230229
assert is_rate_limited is False
231-
assert current == 0
232-
assert limit == 0
230+
mock_track_outcome.assert_not_called()
233231

234232
@patch("sentry.autofix.utils.ratelimits.backend.is_limited_with_value")
235-
def test_autofix_rate_limited_logic(self, mock_is_limited):
233+
@patch("sentry.autofix.utils.track_outcome")
234+
def test_autofix_rate_limited_logic(self, mock_track_outcome, mock_is_limited):
236235
"""Test autofix rate limiting logic"""
237236
project = self.create_project()
238237
organization = project.organization
@@ -242,13 +241,10 @@ def test_autofix_rate_limited_logic(self, mock_is_limited):
242241
mock_is_limited.return_value = (True, 19, None)
243242

244243
with self.options({"seer.max_num_autofix_autotriggered_per_hour": 20}):
245-
is_rate_limited, current, limit = is_seer_autotriggered_autofix_rate_limited(
246-
project, organization
247-
)
244+
is_rate_limited = is_seer_autotriggered_autofix_rate_limited(project, organization)
248245

249246
assert is_rate_limited is True
250-
assert current == 19
251-
assert limit == 20
247+
mock_track_outcome.assert_called_once()
252248

253249
@patch("sentry.autofix.utils.ratelimits.backend.is_limited_with_value")
254250
def test_autofix_rate_limit_multiplication_logic(self, mock_is_limited):

tests/sentry/tasks/test_post_process.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2698,7 +2698,7 @@ def test_rate_limit_only_checked_after_all_other_checks_pass(
26982698
"""Test that rate limit check only happens after all other checks pass"""
26992699
mock_get_seer_org_acknowledgement.return_value = True
27002700
mock_has_budget.return_value = True
2701-
mock_is_rate_limited.return_value = (False, 0, 100) # Not rate limited
2701+
mock_is_rate_limited.return_value = False
27022702

27032703
self.project.update_option("sentry:seer_scanner_automation", True)
27042704
event = self.create_event(

0 commit comments

Comments
 (0)