diff --git a/src/sentry/tasks/auto_ongoing_issues.py b/src/sentry/tasks/auto_ongoing_issues.py index c79a7868bd6470..ff56e0fb5af5cb 100644 --- a/src/sentry/tasks/auto_ongoing_issues.py +++ b/src/sentry/tasks/auto_ongoing_issues.py @@ -2,11 +2,11 @@ from datetime import datetime, timedelta, timezone import sentry_sdk -from django.db.models import Max +from django.db.models import Max, OuterRef, Subquery from sentry.issues.ongoing import TRANSITION_AFTER_DAYS, bulk_transition_group_to_ongoing from sentry.models.group import Group, GroupStatus -from sentry.models.grouphistory import GroupHistoryStatus +from sentry.models.grouphistory import GroupHistory, GroupHistoryStatus from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import issues_tasks @@ -163,14 +163,28 @@ def get_total_count(results): nonlocal total_count total_count += len(results) + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) + + # Use a subquery to get the most recent REGRESSED history date for each group. + # This ensures we only transition groups whose MOST RECENT regressed history + # is older than the threshold, not just any regressed history. + latest_regressed_subquery = ( + GroupHistory.objects.filter(group_id=OuterRef("id"), status=GroupHistoryStatus.REGRESSED) + .values("group_id") + .annotate(max_date=Max("date_added")) + .values("max_date")[:1] + ) + base_queryset = ( Group.objects.filter( status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.REGRESSED, - grouphistory__status=GroupHistoryStatus.REGRESSED, ) - .annotate(recent_regressed_history=Max("grouphistory__date_added")) - .filter(recent_regressed_history__lte=datetime.fromtimestamp(date_added_lte, timezone.utc)) + .annotate(recent_regressed_history=Subquery(latest_regressed_subquery)) + .filter( + recent_regressed_history__lte=date_threshold, + recent_regressed_history__isnull=False, + ) ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): @@ -244,14 +258,30 @@ def get_total_count(results): nonlocal total_count total_count += len(results) + from django.db.models import Max, OuterRef, Subquery + + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) + + # Use a subquery to get the most recent ESCALATING history date for each group. + # This ensures we only transition groups whose MOST RECENT escalating history + # is older than the threshold, not just any escalating history. + latest_escalating_subquery = ( + GroupHistory.objects.filter(group_id=OuterRef("id"), status=GroupHistoryStatus.ESCALATING) + .values("group_id") + .annotate(max_date=Max("date_added")) + .values("max_date")[:1] + ) + base_queryset = ( Group.objects.filter( status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.ESCALATING, - grouphistory__status=GroupHistoryStatus.ESCALATING, ) - .annotate(recent_escalating_history=Max("grouphistory__date_added")) - .filter(recent_escalating_history__lte=datetime.fromtimestamp(date_added_lte, timezone.utc)) + .annotate(recent_escalating_history=Subquery(latest_escalating_subquery)) + .filter( + recent_escalating_history__lte=date_threshold, + recent_escalating_history__isnull=False, + ) ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): diff --git a/tests/sentry/tasks/test_auto_ongoing_issues.py b/tests/sentry/tasks/test_auto_ongoing_issues.py index 63212e7bcca25e..f18db99669885c 100644 --- a/tests/sentry/tasks/test_auto_ongoing_issues.py +++ b/tests/sentry/tasks/test_auto_ongoing_issues.py @@ -345,6 +345,73 @@ def test_not_all_groups_get_updated(self, mock_metrics_incr) -> None: tags={"count": 0}, ) + @freeze_time("2023-07-12 18:40:00Z") + def test_only_checks_most_recent_regressed_history(self) -> None: + """ + Test that only the MOST RECENT regressed history is checked against the threshold, + not just any regressed history. + + Scenario: + - Group regressed 14 days ago (older than 7-day threshold) + - Group resolved 10 days ago + - Group regressed again 2 days ago (newer than 7-day threshold) + + Expected: Group should NOT be transitioned because most recent regression is only 2 days old + """ + now = datetime.now(tz=timezone.utc) + project = self.create_project() + group = self.create_group( + project=project, + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.REGRESSED, + first_seen=now - timedelta(days=14), + ) + + # Create OLD regressed history (14 days ago) - this is OLDER than threshold + old_regressed_history = record_group_history( + group, GroupHistoryStatus.REGRESSED, actor=None, release=None + ) + old_regressed_history.date_added = now - timedelta(days=14) + old_regressed_history.save(update_fields=["date_added"]) + + # Create resolved history in between (10 days ago) + resolved_history = record_group_history( + group, GroupHistoryStatus.RESOLVED, actor=None, release=None + ) + resolved_history.date_added = now - timedelta(days=10) + resolved_history.save(update_fields=["date_added"]) + + # Create NEW regressed history (2 days ago) - this is NEWER than threshold + # This is the MOST RECENT regressed history + new_regressed_history = record_group_history( + group, GroupHistoryStatus.REGRESSED, actor=None, release=None + ) + new_regressed_history.date_added = now - timedelta(days=2) + new_regressed_history.save(update_fields=["date_added"]) + + # Also create a recent group inbox entry + group_inbox = add_group_to_inbox(group, GroupInboxReason.REGRESSION) + group_inbox.date_added = now - timedelta(days=2) + group_inbox.save(update_fields=["date_added"]) + + with self.tasks(): + schedule_auto_transition_to_ongoing() + + # Group should NOT be transitioned because most recent regression is only 2 days old + group.refresh_from_db() + assert group.status == GroupStatus.UNRESOLVED + assert group.substatus == GroupSubStatus.REGRESSED # Should still be REGRESSED + + # Should NOT have created an auto-ongoing activity + assert not Activity.objects.filter( + group=group, type=ActivityType.AUTO_SET_ONGOING.value + ).exists() + + # Should NOT have created an ONGOING history entry + assert not GroupHistory.objects.filter( + group=group, status=GroupHistoryStatus.ONGOING + ).exists() + class ScheduleAutoEscalatingOngoingIssuesTest(TestCase): @freeze_time("2023-07-12 18:40:00Z")