From 6be26065441dfde24313177156d786bb98f092ac Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 14 Sep 2026 10:42:46 -0400 Subject: [PATCH 1/2] feat(pr-iter): record the CI result after an iteration Adds head_shas to the feedback-batch analytics event and an event for a concluded check suite. Together they let a CI result join to the batch that pushed the commit. The scan for the repositories an iteration changed moves to autofix_agent, so the push outcome and the analytics row read it from one place. Ref CW-2013. Claude-Session: https://claude.ai/code/session_01W2xkbxFqSJ2F5uUzrc5DXB --- .../analytics/events/pr_iteration_events.py | 20 +++ src/sentry/seer/autofix/autofix_agent.py | 7 + src/sentry/seer/autofix/on_completion_hook.py | 3 +- src/sentry/seer/autofix/pr_iteration/emit.py | 33 ++++- .../pr_iteration/listeners/check_suite.py | 39 +++++ .../autofix/pr_iteration/test_check_suite.py | 86 +++++++++++ .../seer/autofix/pr_iteration/test_emit.py | 134 +++++++++++++++++- 7 files changed, 316 insertions(+), 6 deletions(-) diff --git a/src/sentry/analytics/events/pr_iteration_events.py b/src/sentry/analytics/events/pr_iteration_events.py index effd2c263d9c..2c330358c297 100644 --- a/src/sentry/analytics/events/pr_iteration_events.py +++ b/src/sentry/analytics/events/pr_iteration_events.py @@ -53,6 +53,9 @@ class AiAutofixPrIterationFeedbackBatchCompletedEvent(analytics.Event): # Review bots behind the feedback the drain consumed, sorted and deduped. feedback_bot_logins: list[str] = field(default_factory=list) + # Commit SHAs this iteration pushed. Empty unless the outcome is already_pushed. + head_shas: list[str] = field(default_factory=list) + @analytics.eventclass("ai.autofix.pr_iteration.feedback_batch.blocked") class AiAutofixPrIterationFeedbackBatchBlockedEvent(analytics.Event): @@ -83,6 +86,23 @@ class AiAutofixPrIterationFeedbackBatchBlockedEvent(analytics.Event): outcome: str +@analytics.eventclass("ai.autofix.pr_iteration.check_suite_concluded") +class AiAutofixPrIterationCheckSuiteConcludedEvent(analytics.Event): + """One CI check suite that concluded on a PR with an Autofix run. + + Join to a feedback batch on ``head_sha``. + """ + + organization_id: int + run_id: int + head_sha: str + conclusion: str + app_name: str + check_suite_id: int + updated_at: str | None = None + + analytics.register(AiAutofixPrIterationMissingPermissionsEvent) analytics.register(AiAutofixPrIterationFeedbackBatchCompletedEvent) analytics.register(AiAutofixPrIterationFeedbackBatchBlockedEvent) +analytics.register(AiAutofixPrIterationCheckSuiteConcludedEvent) diff --git a/src/sentry/seer/autofix/autofix_agent.py b/src/sentry/seer/autofix/autofix_agent.py index 376d8d31fc50..d8fecec2207d 100644 --- a/src/sentry/seer/autofix/autofix_agent.py +++ b/src/sentry/seer/autofix/autofix_agent.py @@ -340,6 +340,13 @@ def get_iterations(state: SeerRunState) -> list[Iteration]: return iterations +def iteration_repos(iteration: Iteration) -> set[str]: + """The repositories this iteration changed.""" + return { + patch.repo_name for block in iteration.blocks for patch in (block.merged_file_patches or []) + } + + def get_latest_iteration_index(state: SeerRunState) -> int: try: iterations = get_iterations(state) diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index cebec77eb950..fa50262ccbfc 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -30,6 +30,7 @@ STEP_CONFIGS, get_iterations, get_latest_iteration_index, + iteration_repos, should_open_autofix_pr_as_draft, trigger_autofix_agent, trigger_coding_agent_handoff, @@ -1137,7 +1138,7 @@ def _latest_iteration_touched_files( if not iterations: return True - return any(block.merged_file_patches for block in iterations[-1].blocks) + return bool(iteration_repos(iterations[-1])) @classmethod def _pr_iteration_push_outcome( diff --git a/src/sentry/seer/autofix/pr_iteration/emit.py b/src/sentry/seer/autofix/pr_iteration/emit.py index a0da751827cf..4b0514d2e58b 100644 --- a/src/sentry/seer/autofix/pr_iteration/emit.py +++ b/src/sentry/seer/autofix/pr_iteration/emit.py @@ -30,7 +30,11 @@ ) from sentry.models.group import Group from sentry.seer.agent.client_models import SeerRunState -from sentry.seer.autofix.autofix_agent import get_latest_iteration_index +from sentry.seer.autofix.autofix_agent import ( + get_iterations, + get_latest_iteration_index, + iteration_repos, +) from sentry.seer.autofix.pr_iteration.current_iteration import triggered_iteration_id from sentry.seer.autofix.pr_iteration.details_store import ( claim_iteration, @@ -284,6 +288,24 @@ def discard_pr_iteration_details( log_ctx.error("autofix.pr_iteration.details.discard_failed") +def _pushed_head_shas(run_state: SeerRunState) -> list[str]: + """The commit SHAs the latest iteration pushed, one for each repository.""" + try: + iterations = get_iterations(run_state) + except Exception: + return [] + + if not iterations: + return [] + + shas = { + pr_state.commit_sha + for repo in iteration_repos(iterations[-1]) + if (pr_state := run_state.repo_pr_states.get(repo)) and pr_state.commit_sha + } + return sorted(shas) + + def _build_event( log_ctx: PrIterationLogContext, iteration: SeerRunPrIteration, @@ -291,6 +313,7 @@ def _build_event( *, iteration_index: int, outcome: str, + head_shas: list[str] | None = None, ) -> EventT | None: """An event filled from an iteration's row. None when that row is incomplete. @@ -305,6 +328,8 @@ class decides how much of that is in scope: a blocked event takes the four # event reports what the drain wrote instead. if "duration_ms" in known: payload["duration_ms"] = int((timezone.now() - iteration.date_added).total_seconds() * 1000) + if head_shas is not None and "head_shas" in known: + payload["head_shas"] = head_shas try: return event_cls( iteration_id=iteration.id, @@ -415,12 +440,18 @@ def complete_pr_iteration_details( log_ctx.info("autofix.pr_iteration.details.skipped", reason="already_emitted") return + head_shas = ( + _pushed_head_shas(run_state) + if outcome == PrIterationOutcome.ALREADY_PUSHED.value + else [] + ) event = _build_event( log_ctx, iteration, AiAutofixPrIterationFeedbackBatchCompletedEvent, iteration_index=get_latest_iteration_index(run_state), outcome=outcome, + head_shas=head_shas, ) if event is None or not remove_iteration(iteration): return diff --git a/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py b/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py index 7197a82e10ae..db3a7eff0a10 100644 --- a/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py +++ b/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py @@ -4,6 +4,10 @@ import sentry_sdk from pydantic import ValidationError +from sentry import analytics +from sentry.analytics.events.pr_iteration_events import ( + AiAutofixPrIterationCheckSuiteConcludedEvent, +) from sentry.scm.private.event_stream import scm_event_stream from sentry.scm.types import CheckSuiteEvent from sentry.seer.autofix.constants import AutofixReferrer @@ -13,6 +17,7 @@ GREEN_CONCLUSIONS, READY_FOR_REVIEW_EXTRA, REVIEW_REQUESTS_EXTRA, + GithubCheckSuiteEvent, ResolvedGreenCheckSuite, confirm_green_check_suite, green_review_side_effects_enabled, @@ -122,6 +127,31 @@ def _retrigger_deferred_iteration( ) +def _record_check_suite_concluded( + log_ctx: PrIterationLogContext, + *, + event: GithubCheckSuiteEvent, + organization_id: int, + run_id: int, +) -> None: + """Record one concluded suite so Hex can join it to the batch that pushed the commit.""" + try: + suite = event.check_suite + analytics.record( + AiAutofixPrIterationCheckSuiteConcludedEvent( + organization_id=organization_id, + run_id=run_id, + head_sha=suite.head_sha, + conclusion=suite.conclusion or "", + app_name=suite.app.name, + check_suite_id=suite.id, + updated_at=suite.updated_at, + ) + ) + except Exception: + log_ctx.error("autofix.pr_iteration.check_suite.analytics_failed") + + @scm_event_stream.listen_for(event_type="check_suite") def pr_iteration_from_check_suite_listener(check_suite_event: CheckSuiteEvent): if check_suite_event.action != "completed": @@ -153,6 +183,12 @@ def pr_iteration_from_check_suite_listener(check_suite_event: CheckSuiteEvent): group_id=resolved.autofix_run.group_id, create=False, ) + _record_check_suite_concluded( + log_ctx, + event=resolved.event, + organization_id=resolved.organization.id, + run_id=run_state.run_id, + ) # Peek the queue for parked check-suite feedback on this head, then # ``should_defer_pr_iteration`` (GitHub sweep) only if something is # waiting. Isolated so a failure cannot swallow undraft / review-request @@ -227,6 +263,9 @@ def pr_iteration_from_check_suite_listener(check_suite_event: CheckSuiteEvent): organization_id=organization_id, group_id=autofix_run.group_id, ) + _record_check_suite_concluded( + log_ctx, event=source.event, organization_id=organization_id, run_id=agent_state.run_id + ) # Report failures here rather than only in the SCM event stream so they # are searchable under the PR-iteration identity. Swallow so diff --git a/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py b/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py index 2a8cef84a42f..3e1c95031032 100644 --- a/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py +++ b/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py @@ -4,6 +4,9 @@ import orjson from scm.helpers import iter_all_pages +from sentry.analytics.events.pr_iteration_events import ( + AiAutofixPrIterationCheckSuiteConcludedEvent, +) from sentry.scm.types import CheckSuiteEvent from sentry.seer.agent.client_models import MemoryBlock, Message, RepoPRState, SeerRunState from sentry.seer.autofix.constants import AutofixReferrer @@ -46,6 +49,10 @@ ) from sentry.seer.autofix.pr_iteration.queue import QueuedAutofixFeedback from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.analytics import ( + assert_any_analytics_event, + assert_not_analytics_event, +) from sentry.testutils.helpers.options import override_options CHECK_PATH = "sentry.seer.autofix.pr_iteration.listeners.check_suite" @@ -479,6 +486,85 @@ def test_enqueues_and_triggers_for_matched_run( mock_trigger_consume.assert_called_once() mock_assign.assert_not_called() + @patch("sentry.analytics.record") + @patch(f"{CHECK_PATH}.assign_user_for_exhausted_cap") + @patch(TRIGGER_CONSUME_PATH) + @patch(f"{CHECK_PATH}.try_enqueue_autofix_feedback", return_value=True) + @patch(f"{CHECK_SUITES_PATH}.get_agent_state_from_pr_id") + @patch(f"{CHECK_SUITES_PATH}.resolve_check_suite_repositories") + def test_a_failing_suite_records_its_conclusion( + self, + mock_resolve: MagicMock, + mock_get_state: MagicMock, + _mock_enqueue: MagicMock, + _mock_trigger_consume: MagicMock, + _mock_assign: MagicMock, + mock_record: MagicMock, + ) -> None: + mock_resolve.return_value = [MagicMock(organization_id=self.organization.id, id=2)] + mock_get_state.return_value = self._agent_state() + raw = self._raw(pull_requests=[own_repo_pr(555)]) + raw["check_suite"]["conclusion"] = "failure" + + pr_iteration_from_check_suite_listener(self._event(raw)) + + assert_any_analytics_event( + mock_record, + AiAutofixPrIterationCheckSuiteConcludedEvent( + organization_id=self.organization.id, + run_id=67890, + head_sha="abc", + conclusion="failure", + app_name="CI", + check_suite_id=1, + updated_at="2024-01-01T00:00:00Z", + ), + ) + + @patch("sentry.analytics.record") + @patch(f"{CHECK_SUITES_PATH}.get_agent_state_from_pr_id") + def test_an_unhandled_suite_records_nothing( + self, _mock_get_state: MagicMock, mock_record: MagicMock + ) -> None: + pr_iteration_from_check_suite_listener(self._event(self._raw(), action="requested")) + pr_iteration_from_check_suite_listener(self._event(self._raw(), conclusion="cancelled")) + + assert_not_analytics_event(mock_record, AiAutofixPrIterationCheckSuiteConcludedEvent) + + @patch("sentry.analytics.record") + @patch(f"{CHECK_PATH}.peek_queued_autofix_feedback", return_value=[]) + @patch(f"{CHECK_PATH}.green_review_side_effects_enabled", return_value=False) + @patch(f"{CHECK_PATH}.resolve_green_check_suite") + def test_a_green_suite_records_its_conclusion( + self, + mock_resolve: MagicMock, + _mock_enabled: MagicMock, + _mock_peek: MagicMock, + mock_record: MagicMock, + ) -> None: + raw = self._raw() + raw["check_suite"]["conclusion"] = "success" + resolved = MagicMock() + resolved.event = GithubCheckSuiteEvent(**raw) + resolved.organization.id = self.organization.id + resolved.autofix_run.run_state = self._agent_state() + mock_resolve.return_value = resolved + + pr_iteration_from_check_suite_listener(self._event(raw, conclusion="success")) + + assert_any_analytics_event( + mock_record, + AiAutofixPrIterationCheckSuiteConcludedEvent( + organization_id=self.organization.id, + run_id=67890, + head_sha="abc", + conclusion="success", + app_name="CI", + check_suite_id=1, + updated_at="2024-01-01T00:00:00Z", + ), + ) + @patch(f"{CHECK_SUITES_PATH}.sentry_sdk.capture_exception") @patch(TRIGGER_CONSUME_PATH) @patch(f"{CHECK_PATH}.try_enqueue_autofix_feedback", return_value=True) diff --git a/tests/sentry/seer/autofix/pr_iteration/test_emit.py b/tests/sentry/seer/autofix/pr_iteration/test_emit.py index 7ae7aa796db1..15cf96399fd3 100644 --- a/tests/sentry/seer/autofix/pr_iteration/test_emit.py +++ b/tests/sentry/seer/autofix/pr_iteration/test_emit.py @@ -7,7 +7,14 @@ AiAutofixPrIterationFeedbackBatchBlockedEvent, AiAutofixPrIterationFeedbackBatchCompletedEvent, ) -from sentry.seer.agent.client_models import MemoryBlock, Message, SeerRunState +from sentry.seer.agent.client_models import ( + AgentFilePatch, + FilePatch, + MemoryBlock, + Message, + RepoPRState, + SeerRunState, +) from sentry.seer.autofix.pr_iteration.details_store import ( open_iterations, remove_iterations_before, @@ -34,18 +41,53 @@ RUN_ID = 4242 -def _run_state(*, blocks: list[MemoryBlock] | None = None) -> SeerRunState: +def _run_state( + *, + blocks: list[MemoryBlock] | None = None, + commit_shas: dict[str, str] | None = None, +) -> SeerRunState: return SeerRunState( run_id=RUN_ID, blocks=blocks or [], status="completed", updated_at="2024-01-01T00:00:00Z", + repo_pr_states={ + repo: RepoPRState(repo_name=repo, commit_sha=sha) + for repo, sha in (commit_shas or {}).items() + }, + ) + + +def _patch(repo_name: str) -> AgentFilePatch: + return AgentFilePatch( + repo_name=repo_name, + patch=FilePatch(path="src/foo.py", type="M", added=1, removed=0), ) -def _iteration_block(iteration_id: int) -> MemoryBlock: +def _edit_block( + block_id: str, *, repos: list[str], pr_commit_shas: dict[str, str] | None = None +) -> MemoryBlock: + """A follow-on block in the iteration that edited files in ``repos``.""" + return MemoryBlock( + id=block_id, + pr_commit_shas=pr_commit_shas, + merged_file_patches=[_patch(repo) for repo in repos], + message=Message(role="assistant", content="edit"), + timestamp="2024-01-01T00:00:00Z", + ) + + +def _iteration_block( + iteration_id: int, + *, + repos: list[str] | None = None, + pr_commit_shas: dict[str, str] | None = None, +) -> MemoryBlock: return MemoryBlock( id="block-0", + pr_commit_shas=pr_commit_shas, + merged_file_patches=[_patch(repo) for repo in repos or []], message=Message( role="assistant", content="iteration", @@ -108,10 +150,16 @@ def _complete( iteration_id: int, *, outcome: str = PrIterationOutcome.ALREADY_PUSHED.value, + repos: list[str] | None = None, + commit_shas: dict[str, str] | None = None, + extra_blocks: list[MemoryBlock] | None = None, ) -> None: complete_pr_iteration_details( log_ctx=self.log_ctx, - run_state=_run_state(blocks=[_iteration_block(iteration_id)]), + run_state=_run_state( + blocks=[_iteration_block(iteration_id, repos=repos), *(extra_blocks or [])], + commit_shas=commit_shas, + ), organization_id=self.organization.id, outcome=outcome, ) @@ -140,6 +188,82 @@ def test_the_trigger_writes_what_the_drain_saw(self) -> None: assert row.data["automated_feedback_count"] == 1 assert row.data["feedback_bot_logins"] == ["coderabbitai[bot]"] + def test_a_pushed_iteration_records_the_commit_it_pushed(self) -> None: + self._open() + iteration_id = self._trigger() + assert iteration_id is not None + + with patch("sentry.analytics.record") as mock_record: + self._complete( + iteration_id, + repos=["owner/repo"], + commit_shas={"owner/repo": "sha-new"}, + ) + + assert mock_record.call_args.args[0].head_shas == ["sha-new"] + + def test_the_pushed_commit_wins_over_an_earlier_blocks_commit(self) -> None: + """A block records the PR head at the time it was created, so it can be stale.""" + self._open() + iteration_id = self._trigger() + assert iteration_id is not None + stale = _edit_block( + "block-1", repos=["owner/repo"], pr_commit_shas={"owner/repo": "sha-old"} + ) + pushed = _edit_block("block-2", repos=["owner/repo"]) + + with patch("sentry.analytics.record") as mock_record: + self._complete( + iteration_id, + commit_shas={"owner/repo": "sha-new"}, + extra_blocks=[stale, pushed], + ) + + assert mock_record.call_args.args[0].head_shas == ["sha-new"] + + def test_a_multi_repo_iteration_records_every_commit_it_pushed(self) -> None: + self._open() + iteration_id = self._trigger() + assert iteration_id is not None + + with patch("sentry.analytics.record") as mock_record: + self._complete( + iteration_id, + repos=["owner/one", "owner/two"], + commit_shas={"owner/one": "sha-b", "owner/two": "sha-a"}, + ) + + assert mock_record.call_args.args[0].head_shas == ["sha-a", "sha-b"] + + def test_a_repo_the_iteration_did_not_touch_is_left_out(self) -> None: + self._open() + iteration_id = self._trigger() + assert iteration_id is not None + + with patch("sentry.analytics.record") as mock_record: + self._complete( + iteration_id, + repos=["owner/one"], + commit_shas={"owner/one": "sha-a", "owner/untouched": "sha-z"}, + ) + + assert mock_record.call_args.args[0].head_shas == ["sha-a"] + + def test_an_iteration_that_pushed_nothing_records_no_commit(self) -> None: + self._open() + iteration_id = self._trigger() + assert iteration_id is not None + + with patch("sentry.analytics.record") as mock_record: + self._complete( + iteration_id, + outcome=PrIterationOutcome.NO_CODE_CHANGES.value, + repos=["owner/repo"], + commit_shas={"owner/repo": "sha-new"}, + ) + + assert mock_record.call_args.args[0].head_shas == [] + @freeze_time("2024-01-01 00:00:00") def test_the_iteration_it_opened_is_emitted_when_it_completes(self) -> None: self._open() @@ -165,6 +289,7 @@ def test_the_iteration_it_opened_is_emitted_when_it_completes(self) -> None: dropped_count=1, automated_feedback_count=1, feedback_bot_logins=["coderabbitai[bot]"], + head_shas=[], outcome="already_pushed", ), ) @@ -308,6 +433,7 @@ def test_an_iteration_that_produced_nothing_records_that_outcome(self) -> None: dropped_count=1, automated_feedback_count=1, feedback_bot_logins=["coderabbitai[bot]"], + head_shas=[], outcome="no_code_changes", ), ) From a8d3c2444244bb8f611dc7cdeee92fdf00d289fd Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 14 Sep 2026 11:43:06 -0400 Subject: [PATCH 2/2] ref(pr-iter): join CI results through ci_head_results Drops the check-suite event. ``head_shas`` joins to the per-head CI results already emitted on ``scm.pr.closed``, so the CI outcome of an iteration is a query rather than a second event stream. Claude-Session: https://claude.ai/code/session_01W2xkbxFqSJ2F5uUzrc5DXB --- .../analytics/events/pr_iteration_events.py | 17 ---- .../pr_iteration/listeners/check_suite.py | 39 --------- .../autofix/pr_iteration/test_check_suite.py | 86 ------------------- 3 files changed, 142 deletions(-) diff --git a/src/sentry/analytics/events/pr_iteration_events.py b/src/sentry/analytics/events/pr_iteration_events.py index 2c330358c297..43db189d9291 100644 --- a/src/sentry/analytics/events/pr_iteration_events.py +++ b/src/sentry/analytics/events/pr_iteration_events.py @@ -86,23 +86,6 @@ class AiAutofixPrIterationFeedbackBatchBlockedEvent(analytics.Event): outcome: str -@analytics.eventclass("ai.autofix.pr_iteration.check_suite_concluded") -class AiAutofixPrIterationCheckSuiteConcludedEvent(analytics.Event): - """One CI check suite that concluded on a PR with an Autofix run. - - Join to a feedback batch on ``head_sha``. - """ - - organization_id: int - run_id: int - head_sha: str - conclusion: str - app_name: str - check_suite_id: int - updated_at: str | None = None - - analytics.register(AiAutofixPrIterationMissingPermissionsEvent) analytics.register(AiAutofixPrIterationFeedbackBatchCompletedEvent) analytics.register(AiAutofixPrIterationFeedbackBatchBlockedEvent) -analytics.register(AiAutofixPrIterationCheckSuiteConcludedEvent) diff --git a/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py b/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py index db3a7eff0a10..7197a82e10ae 100644 --- a/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py +++ b/src/sentry/seer/autofix/pr_iteration/listeners/check_suite.py @@ -4,10 +4,6 @@ import sentry_sdk from pydantic import ValidationError -from sentry import analytics -from sentry.analytics.events.pr_iteration_events import ( - AiAutofixPrIterationCheckSuiteConcludedEvent, -) from sentry.scm.private.event_stream import scm_event_stream from sentry.scm.types import CheckSuiteEvent from sentry.seer.autofix.constants import AutofixReferrer @@ -17,7 +13,6 @@ GREEN_CONCLUSIONS, READY_FOR_REVIEW_EXTRA, REVIEW_REQUESTS_EXTRA, - GithubCheckSuiteEvent, ResolvedGreenCheckSuite, confirm_green_check_suite, green_review_side_effects_enabled, @@ -127,31 +122,6 @@ def _retrigger_deferred_iteration( ) -def _record_check_suite_concluded( - log_ctx: PrIterationLogContext, - *, - event: GithubCheckSuiteEvent, - organization_id: int, - run_id: int, -) -> None: - """Record one concluded suite so Hex can join it to the batch that pushed the commit.""" - try: - suite = event.check_suite - analytics.record( - AiAutofixPrIterationCheckSuiteConcludedEvent( - organization_id=organization_id, - run_id=run_id, - head_sha=suite.head_sha, - conclusion=suite.conclusion or "", - app_name=suite.app.name, - check_suite_id=suite.id, - updated_at=suite.updated_at, - ) - ) - except Exception: - log_ctx.error("autofix.pr_iteration.check_suite.analytics_failed") - - @scm_event_stream.listen_for(event_type="check_suite") def pr_iteration_from_check_suite_listener(check_suite_event: CheckSuiteEvent): if check_suite_event.action != "completed": @@ -183,12 +153,6 @@ def pr_iteration_from_check_suite_listener(check_suite_event: CheckSuiteEvent): group_id=resolved.autofix_run.group_id, create=False, ) - _record_check_suite_concluded( - log_ctx, - event=resolved.event, - organization_id=resolved.organization.id, - run_id=run_state.run_id, - ) # Peek the queue for parked check-suite feedback on this head, then # ``should_defer_pr_iteration`` (GitHub sweep) only if something is # waiting. Isolated so a failure cannot swallow undraft / review-request @@ -263,9 +227,6 @@ def pr_iteration_from_check_suite_listener(check_suite_event: CheckSuiteEvent): organization_id=organization_id, group_id=autofix_run.group_id, ) - _record_check_suite_concluded( - log_ctx, event=source.event, organization_id=organization_id, run_id=agent_state.run_id - ) # Report failures here rather than only in the SCM event stream so they # are searchable under the PR-iteration identity. Swallow so diff --git a/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py b/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py index 3e1c95031032..2a8cef84a42f 100644 --- a/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py +++ b/tests/sentry/seer/autofix/pr_iteration/test_check_suite.py @@ -4,9 +4,6 @@ import orjson from scm.helpers import iter_all_pages -from sentry.analytics.events.pr_iteration_events import ( - AiAutofixPrIterationCheckSuiteConcludedEvent, -) from sentry.scm.types import CheckSuiteEvent from sentry.seer.agent.client_models import MemoryBlock, Message, RepoPRState, SeerRunState from sentry.seer.autofix.constants import AutofixReferrer @@ -49,10 +46,6 @@ ) from sentry.seer.autofix.pr_iteration.queue import QueuedAutofixFeedback from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.analytics import ( - assert_any_analytics_event, - assert_not_analytics_event, -) from sentry.testutils.helpers.options import override_options CHECK_PATH = "sentry.seer.autofix.pr_iteration.listeners.check_suite" @@ -486,85 +479,6 @@ def test_enqueues_and_triggers_for_matched_run( mock_trigger_consume.assert_called_once() mock_assign.assert_not_called() - @patch("sentry.analytics.record") - @patch(f"{CHECK_PATH}.assign_user_for_exhausted_cap") - @patch(TRIGGER_CONSUME_PATH) - @patch(f"{CHECK_PATH}.try_enqueue_autofix_feedback", return_value=True) - @patch(f"{CHECK_SUITES_PATH}.get_agent_state_from_pr_id") - @patch(f"{CHECK_SUITES_PATH}.resolve_check_suite_repositories") - def test_a_failing_suite_records_its_conclusion( - self, - mock_resolve: MagicMock, - mock_get_state: MagicMock, - _mock_enqueue: MagicMock, - _mock_trigger_consume: MagicMock, - _mock_assign: MagicMock, - mock_record: MagicMock, - ) -> None: - mock_resolve.return_value = [MagicMock(organization_id=self.organization.id, id=2)] - mock_get_state.return_value = self._agent_state() - raw = self._raw(pull_requests=[own_repo_pr(555)]) - raw["check_suite"]["conclusion"] = "failure" - - pr_iteration_from_check_suite_listener(self._event(raw)) - - assert_any_analytics_event( - mock_record, - AiAutofixPrIterationCheckSuiteConcludedEvent( - organization_id=self.organization.id, - run_id=67890, - head_sha="abc", - conclusion="failure", - app_name="CI", - check_suite_id=1, - updated_at="2024-01-01T00:00:00Z", - ), - ) - - @patch("sentry.analytics.record") - @patch(f"{CHECK_SUITES_PATH}.get_agent_state_from_pr_id") - def test_an_unhandled_suite_records_nothing( - self, _mock_get_state: MagicMock, mock_record: MagicMock - ) -> None: - pr_iteration_from_check_suite_listener(self._event(self._raw(), action="requested")) - pr_iteration_from_check_suite_listener(self._event(self._raw(), conclusion="cancelled")) - - assert_not_analytics_event(mock_record, AiAutofixPrIterationCheckSuiteConcludedEvent) - - @patch("sentry.analytics.record") - @patch(f"{CHECK_PATH}.peek_queued_autofix_feedback", return_value=[]) - @patch(f"{CHECK_PATH}.green_review_side_effects_enabled", return_value=False) - @patch(f"{CHECK_PATH}.resolve_green_check_suite") - def test_a_green_suite_records_its_conclusion( - self, - mock_resolve: MagicMock, - _mock_enabled: MagicMock, - _mock_peek: MagicMock, - mock_record: MagicMock, - ) -> None: - raw = self._raw() - raw["check_suite"]["conclusion"] = "success" - resolved = MagicMock() - resolved.event = GithubCheckSuiteEvent(**raw) - resolved.organization.id = self.organization.id - resolved.autofix_run.run_state = self._agent_state() - mock_resolve.return_value = resolved - - pr_iteration_from_check_suite_listener(self._event(raw, conclusion="success")) - - assert_any_analytics_event( - mock_record, - AiAutofixPrIterationCheckSuiteConcludedEvent( - organization_id=self.organization.id, - run_id=67890, - head_sha="abc", - conclusion="success", - app_name="CI", - check_suite_id=1, - updated_at="2024-01-01T00:00:00Z", - ), - ) - @patch(f"{CHECK_SUITES_PATH}.sentry_sdk.capture_exception") @patch(TRIGGER_CONSUME_PATH) @patch(f"{CHECK_PATH}.try_enqueue_autofix_feedback", return_value=True)