Repository: getsentry/sentry

Issue:
Get hourly event count from Snuba to compare with forecast
## Objective:

The forecasting algorithm generates an hourly threshold that we should compare against. Currently we fetch the entire day's event count from Snuba. This should be the hourly event count


Discussion:

Resolving patch:
diff --git a/src/sentry/issues/escalating.py b/src/sentry/issues/escalating.py
--- a/src/sentry/issues/escalating.py
+++ b/src/sentry/issues/escalating.py
@@ -41,7 +41,7 @@
 BUCKETS_PER_GROUP = 7 * 24
 ONE_WEEK_DURATION = 7
 IS_ESCALATING_REFERRER = "sentry.issues.escalating.is_escalating"
-GROUP_DAILY_COUNT_TTL = 60
+GROUP_HOURLY_COUNT_TTL = 60
 
 GroupsCountResponse = TypedDict(
     "GroupsCountResponse",
@@ -204,15 +204,14 @@ def _extract_project_and_group_ids(groups: Sequence[Group]) -> Dict[int, List[in
     return group_ids_by_project
 
 
-def get_group_daily_count(organization_id: int, project_id: int, group_id: int) -> int:
-    """Return the number of events a group has had today"""
-    key = f"daily-group-count:{project_id}:{group_id}"
-    daily_count = cache.get(key)
+def get_group_hourly_count(organization_id: int, project_id: int, group_id: int) -> int:
+    """Return the number of events a group has had today in the last hour"""
+    key = f"hourly-group-count:{project_id}:{group_id}"
+    hourly_count = cache.get(key)
 
-    if daily_count is None:
-        today = datetime.now().date()
-        midnight = datetime.combine(today, datetime.min.time())
+    if hourly_count is None:
         now = datetime.now()
+        current_hour = now.replace(minute=0, second=0, microsecond=0)
         query = Query(
             match=Entity(EntityKey.Events.value),
             select=[
@@ -221,7 +220,7 @@ def get_group_daily_count(organization_id: int, project_id: int, group_id: int)
             where=[
                 Condition(Column("project_id"), Op.EQ, project_id),
                 Condition(Column("group_id"), Op.EQ, group_id),
-                Condition(Column("timestamp"), Op.GTE, midnight),
+                Condition(Column("timestamp"), Op.GTE, current_hour),
                 Condition(Column("timestamp"), Op.LT, now),
             ],
         )
@@ -231,21 +230,21 @@ def get_group_daily_count(organization_id: int, project_id: int, group_id: int)
             query=query,
             tenant_ids={"referrer": IS_ESCALATING_REFERRER, "organization_id": organization_id},
         )
-        daily_count = int(
+        hourly_count = int(
             raw_snql_query(request, referrer=IS_ESCALATING_REFERRER)["data"][0]["count()"]
         )
-        cache.set(key, daily_count, GROUP_DAILY_COUNT_TTL)
-    return int(daily_count)
+        cache.set(key, hourly_count, GROUP_HOURLY_COUNT_TTL)
+    return int(hourly_count)
 
 
 def is_escalating(group: Group) -> bool:
     """Return boolean depending on if the group is escalating or not"""
-    group_daily_count = get_group_daily_count(
+    group_hourly_count = get_group_hourly_count(
         group.project.organization.id, group.project.id, group.id
     )
     forecast_today = EscalatingGroupForecast.fetch_todays_forecast(group.project.id, group.id)
     # Check if current event occurance is greater than forecast for today's date
-    if group_daily_count > forecast_today:
+    if group_hourly_count > forecast_today:
         group.substatus = GroupSubStatus.ESCALATING
         group.status = GroupStatus.UNRESOLVED
         add_group_to_inbox(group, GroupInboxReason.ESCALATING)


Test patch:
diff --git a/tests/sentry/issues/test_escalating.py b/tests/sentry/issues/test_escalating.py
--- a/tests/sentry/issues/test_escalating.py
+++ b/tests/sentry/issues/test_escalating.py
@@ -9,7 +9,7 @@
 from sentry.issues.escalating import (
     GroupsCountResponse,
     _start_and_end_dates,
-    get_group_daily_count,
+    get_group_hourly_count,
     is_escalating,
     query_groups_past_counts,
 )
@@ -31,6 +31,7 @@ def _create_events_for_group(
         project_id: Optional[int] = None,
         count: int = 1,
         hours_ago: int = 0,
+        min_ago: int = 0,
         group: str = "foo-1",
     ) -> Event:
         """Creates one or many events for a group.
@@ -44,7 +45,7 @@ def _create_events_for_group(
 
         last_event = None
         for _ in range(count):
-            data["timestamp"] = (datetime_reset_zero - timedelta(hours=hours_ago)).timestamp()  # type: ignore[assignment]
+            data["timestamp"] = (datetime_reset_zero - timedelta(hours=hours_ago, minutes=min_ago)).timestamp()  # type: ignore[assignment]
             data["event_id"] = uuid4().hex
             # assert_no_errors is necessary because of SDK and server time differences due to freeze gun
             last_event = self.store_event(data=data, project_id=proj_id, assert_no_errors=False)
@@ -231,14 +232,20 @@ def test_not_escalating_issue(self) -> None:
             assert group.status == GroupStatus.IGNORED
             assert not GroupInbox.objects.filter(group=group).exists()
 
-    @freeze_time(TIME_YESTERDAY)
-    def test_daily_count_query(self) -> None:
-        """Test the daily count query only aggregates events from today"""
+    @freeze_time(TIME_YESTERDAY.replace(minute=12, second=40, microsecond=0))
+    def test_hourly_count_query(self) -> None:
+        """Test the hourly count query only aggregates events from today"""
         # TIME_YESTERDAY is at 6 in the morning
-        self._create_events_for_group(count=2, hours_ago=7)  # Yesterday
-        event = self._create_events_for_group(count=1)  # Today
-        group = event.group
-        self.archive_until_escalating(event.group)
+        event_1 = self._create_events_for_group(count=2, hours_ago=7)  # Yesterday
+        self.archive_until_escalating(event_1.group)
+
+        # Create event 2 min ago
+        event_2 = self._create_events_for_group(count=1, min_ago=2)
 
-        # Events are aggregated in the daily count query by date rather than the last 24hrs
-        assert get_group_daily_count(group.project.organization.id, group.project.id, group.id) == 1
+        group_2 = event_2.group
+        self.archive_until_escalating(event_2.group)
+
+        assert (
+            get_group_hourly_count(group_2.project.organization.id, group_2.project.id, group_2.id)
+            == 1
+        )