Repository: getsentry/sentry

Issue:
Swap inbox reason for substatus in weekly email
In https://github.com/getsentry/sentry/pull/48047 i switched the weekly emails to count the number of inbox issues. 

This was wrong, we want to use the issue substatus and use those counts instead.


Discussion:

Resolving patch:
diff --git a/src/sentry/tasks/weekly_reports.py b/src/sentry/tasks/weekly_reports.py
--- a/src/sentry/tasks/weekly_reports.py
+++ b/src/sentry/tasks/weekly_reports.py
@@ -2,7 +2,7 @@
 import logging
 from datetime import timedelta
 from functools import partial, reduce
-from typing import MutableMapping
+from typing import MutableMapping, Tuple
 
 import sentry_sdk
 from django.db.models import Count
@@ -26,9 +26,8 @@
     Group,
     GroupHistory,
     GroupHistoryStatus,
-    GroupInbox,
-    GroupInboxReason,
     GroupStatus,
+    GroupSubStatus,
     Organization,
     OrganizationMember,
     OrganizationStatus,
@@ -82,11 +81,11 @@ class ProjectContext:
     new_issue_count = 0
 
     # For organizations:issue-states
-    new_inbox_count = 0
-    ongoing_inbox_count = 0
-    escalating_inbox_count = 0
-    regression_inbox_count = 0
-    total_inbox_count = 0
+    new_substatus_count = 0
+    ongoing_substatus_count = 0
+    escalating_substatus_count = 0
+    regression_substatus_count = 0
+    total_substatus_count = 0
 
     def __init__(self, project):
         self.project = project
@@ -176,8 +175,10 @@ def prepare_organization_report(
         project_event_counts_for_organization(ctx)
 
     if has_issue_states:
-        with sentry_sdk.start_span(op="weekly_reports.organization_project_issue_inbox_summaries"):
-            organization_project_issue_inbox_summaries(ctx)
+        with sentry_sdk.start_span(
+            op="weekly_reports.organization_project_issue_substatus_summaries"
+        ):
+            organization_project_issue_substatus_summaries(ctx)
     else:
         with sentry_sdk.start_span(op="weekly_reports.organization_project_issue_summaries"):
             organization_project_issue_summaries(ctx)
@@ -342,30 +343,27 @@ def organization_project_issue_summaries(ctx):
         )
 
 
-def organization_project_issue_inbox_summaries(ctx: OrganizationReportContext):
-    inbox_counts = (
-        GroupInbox.objects.filter(
-            organization_id=ctx.organization.id,
-            reason__in=[
-                GroupInboxReason.NEW.value,
-                GroupInboxReason.ESCALATING.value,
-                GroupInboxReason.ONGOING.value,
-                GroupInboxReason.REGRESSION.value,
-            ],
+def organization_project_issue_substatus_summaries(ctx: OrganizationReportContext):
+    substatus_counts = (
+        Group.objects.filter(
+            project__organization_id=ctx.organization.id,
+            first_seen__gte=ctx.start,
+            first_seen__lt=ctx.end,
+            status=GroupStatus.UNRESOLVED,
         )
-        .values("project_id", "reason")
-        .annotate(total=Count("reason"))
+        .values("project_id", "substatus")
+        .annotate(total=Count("substatus"))
     )
-    for count in inbox_counts:
-        if count["reason"] == GroupInboxReason.NEW.value:
-            ctx.projects[count["project_id"]].new_inbox_count = count["total"]
-        if count["reason"] == GroupInboxReason.ESCALATING.value:
-            ctx.projects[count["project_id"]].escalating_inbox_count = count["total"]
-        if count["reason"] == GroupInboxReason.ONGOING.value:
-            ctx.projects[count["project_id"]].ongoing_inbox_count = count["total"]
-        if count["reason"] == GroupInboxReason.REGRESSION.value:
-            ctx.projects[count["project_id"]].regression_inbox_count = count["total"]
-        ctx.projects[count["project_id"]].total_inbox_count += count["total"]
+    for item in substatus_counts:
+        if item["substatus"] == GroupSubStatus.NEW:
+            ctx.projects[item["project_id"]].new_substatus_count = item["total"]
+        if item["substatus"] == GroupSubStatus.ESCALATING:
+            ctx.projects[item["project_id"]].escalating_substatus_count = item["total"]
+        if item["substatus"] == GroupSubStatus.ONGOING:
+            ctx.projects[item["project_id"]].ongoing_substatus_count = item["total"]
+        if item["substatus"] == GroupSubStatus.REGRESSED:
+            ctx.projects[item["project_id"]].regression_substatus_count = item["total"]
+        ctx.projects[item["project_id"]].total_substatus_count += item["total"]
 
 
 # Project passes
@@ -412,26 +410,8 @@ def fetch_key_error_groups(ctx):
     for group in Group.objects.filter(id__in=all_key_error_group_ids).all():
         group_id_to_group[group.id] = group
 
-    group_id_to_group_inbox = {}
     group_id_to_group_history = {}
-    if features.has("organizations:issue-states", ctx.organization):
-        group_inbox = (
-            GroupInbox.objects.filter(
-                group_id__in=all_key_error_group_ids,
-                organization_id=ctx.organization.id,
-                reason__in=[
-                    GroupInboxReason.NEW.value,
-                    GroupInboxReason.ESCALATING.value,
-                    GroupInboxReason.ONGOING.value,
-                    GroupInboxReason.REGRESSION.value,
-                ],
-            )
-            .order_by("group_id", "-date_added")
-            .distinct("group_id")
-            .all()
-        )
-        group_id_to_group_inbox = {g.group_id: g for g in group_inbox}
-    else:
+    if not features.has("organizations:issue-states", ctx.organization):
         group_history = (
             GroupHistory.objects.filter(
                 group_id__in=all_key_error_group_ids, organization_id=ctx.organization.id
@@ -452,7 +432,6 @@ def fetch_key_error_groups(ctx):
                     (
                         group_id_to_group.get(group_id),
                         group_id_to_group_history.get(group_id, None),
-                        group_id_to_group_inbox.get(group_id, None),
                         count,
                     )
                     for group_id, count in project_ctx.key_errors
@@ -683,18 +662,21 @@ def deliver_reports(ctx, dry_run=False, target_user=None, email_override=None):
     GroupHistoryStatus.REVIEWED: "#FAD473",
     GroupHistoryStatus.NEW: "#FAD473",
 }
-group_inbox_to_color = {
-    GroupInboxReason.NEW: "rgba(245, 176, 0, 0.08)",
-    GroupInboxReason.REGRESSION: "rgba(108, 95, 199, 0.08)",
-    GroupInboxReason.ESCALATING: "rgba(245, 84, 89, 0.09)",
-    GroupInboxReason.ONGOING: "rgba(219, 214, 225, 1)",
-}
-group_inbox_to_color_border = {
-    GroupInboxReason.NEW: "rgba(245, 176, 0, 0.55)",
-    GroupInboxReason.REGRESSION: "rgba(108, 95, 199, 0.5)",
-    GroupInboxReason.ESCALATING: "rgba(245, 84, 89, 0.5)",
-    GroupInboxReason.ONGOING: "rgba(219, 214, 225, 1)",
-}
+
+
+def get_group_status_badge(group: Group) -> Tuple[str, str, str]:
+    """
+    Returns a tuple of (text, background_color, border_color)
+    Should be similar to GroupStatusBadge.tsx in the frontend
+    """
+    if group.status == GroupStatus.UNRESOLVED:
+        if group.substatus == GroupSubStatus.NEW:
+            return ("New", "rgba(245, 176, 0, 0.08)", "rgba(245, 176, 0, 0.55)")
+        if group.substatus == GroupSubStatus.REGRESSED:
+            return ("Regressed", "rgba(108, 95, 199, 0.08)", "rgba(108, 95, 199, 0.5)")
+        if group.substatus == GroupSubStatus.ESCALATING:
+            return ("Escalating", "rgba(245, 84, 89, 0.09)", "rgba(245, 84, 89, 0.5)")
+    return ("Ongoing", "rgba(219, 214, 225, 1)", "rgba(219, 214, 225, 1)")
 
 
 def render_template_context(ctx, user):
@@ -714,6 +696,8 @@ def render_template_context(ctx, user):
         # If user is None, or if the user is not a member of the organization, we assume that the email was directed to a user who joined all teams.
         user_projects = ctx.projects.values()
 
+    has_issue_states = features.has("organizations:issue-states", ctx.organization)
+
     # Render the first section of the email where we had the table showing the
     # number of accepted/dropped errors/transactions for each project.
     def trends():
@@ -861,7 +845,7 @@ def all_key_errors():
                             "project_id": project_ctx.project.id,
                         },
                     )
-                for group, group_history, group_inbox, count in project_ctx.key_errors:
+                for group, group_history, count in project_ctx.key_errors:
                     if ctx.organization.slug == "sentry":
                         logger.info(
                             "render_template_context.all_key_errors.found_error",
@@ -872,9 +856,10 @@ def all_key_errors():
                             },
                         )
 
-                    group_inbox_reason = (
-                        GroupInboxReason(group_inbox.reason) if group_inbox else None
+                    (substatus, substatus_color, substatus_border_color,) = (
+                        get_group_status_badge(group) if has_issue_states else (None, None, None)
                     )
+
                     yield {
                         "count": count,
                         "group": group,
@@ -884,15 +869,9 @@ def all_key_errors():
                         "status_color": group_status_to_color[group_history.status]
                         if group_history
                         else group_status_to_color[GroupHistoryStatus.NEW],
-                        "inbox_reason": group_inbox_reason.name.capitalize()
-                        if group_inbox_reason
-                        else None,
-                        "inbox_color": group_inbox_to_color[group_inbox_reason]
-                        if group_inbox_reason
-                        else group_inbox_to_color[GroupInboxReason.NEW],
-                        "inbox_color_border": group_inbox_to_color_border[group_inbox_reason]
-                        if group_inbox_reason
-                        else group_inbox_to_color_border[GroupInboxReason.NEW],
+                        "group_substatus": substatus,
+                        "group_substatus_color": substatus_color,
+                        "group_substatus_border_color": substatus_border_color,
                     }
 
         return heapq.nlargest(3, all_key_errors(), lambda d: d["count"])
@@ -939,31 +918,31 @@ def issue_summary():
         existing_issue_count = 0
         reopened_issue_count = 0
         new_issue_count = 0
-        new_inbox_count = 0
-        escalating_inbox_count = 0
-        ongoing_inbox_count = 0
-        regression_inbox_count = 0
-        total_inbox_count = 0
+        new_substatus_count = 0
+        escalating_substatus_count = 0
+        ongoing_substatus_count = 0
+        regression_substatus_count = 0
+        total_substatus_count = 0
         for project_ctx in user_projects:
             all_issue_count += project_ctx.all_issue_count
             existing_issue_count += project_ctx.existing_issue_count
             reopened_issue_count += project_ctx.reopened_issue_count
             new_issue_count += project_ctx.new_issue_count
-            new_inbox_count += project_ctx.new_inbox_count
-            escalating_inbox_count += project_ctx.escalating_inbox_count
-            ongoing_inbox_count += project_ctx.ongoing_inbox_count
-            regression_inbox_count += project_ctx.regression_inbox_count
-            total_inbox_count += project_ctx.total_inbox_count
+            new_substatus_count += project_ctx.new_substatus_count
+            escalating_substatus_count += project_ctx.escalating_substatus_count
+            ongoing_substatus_count += project_ctx.ongoing_substatus_count
+            regression_substatus_count += project_ctx.regression_substatus_count
+            total_substatus_count += project_ctx.total_substatus_count
         return {
             "all_issue_count": all_issue_count,
             "existing_issue_count": existing_issue_count,
             "reopened_issue_count": reopened_issue_count,
             "new_issue_count": new_issue_count,
-            "new_inbox_count": new_inbox_count,
-            "escalating_inbox_count": escalating_inbox_count,
-            "ongoing_inbox_count": ongoing_inbox_count,
-            "regression_inbox_count": regression_inbox_count,
-            "total_inbox_count": total_inbox_count,
+            "new_substatus_count": new_substatus_count,
+            "escalating_substatus_count": escalating_substatus_count,
+            "ongoing_substatus_count": ongoing_substatus_count,
+            "regression_substatus_count": regression_substatus_count,
+            "total_substatus_count": total_substatus_count,
         }
 
     return {
diff --git a/src/sentry/web/frontend/debug/debug_weekly_report.py b/src/sentry/web/frontend/debug/debug_weekly_report.py
--- a/src/sentry/web/frontend/debug/debug_weekly_report.py
+++ b/src/sentry/web/frontend/debug/debug_weekly_report.py
@@ -8,8 +8,6 @@
 from sentry.models import Group, Organization, Project
 from sentry.tasks.weekly_reports import (
     ONE_DAY,
-    GroupInbox,
-    GroupInboxReason,
     OrganizationReportContext,
     ProjectContext,
     render_template_context,
@@ -19,7 +17,7 @@
 
 from .mail import MailPreviewView
 
-DEBUG_ESCALATING_ISSUES = False
+DEBUG_ISSUE_STATES = True
 
 
 def get_random(request):
@@ -80,24 +78,21 @@ def get_context(self, request):
             project_context.dropped_transaction_count = int(
                 random.weibullvariate(5, 1) * random.paretovariate(0.2)
             )
-            group_inbox = (
-                GroupInbox(reason=GroupInboxReason.ESCALATING) if DEBUG_ESCALATING_ISSUES else None
-            )
             project_context.key_errors = [
-                (g, None, group_inbox, random.randint(0, 1000)) for g in Group.objects.all()[:3]
+                (g, None, random.randint(0, 1000)) for g in Group.objects.all()[:3]
             ]
 
-            if DEBUG_ESCALATING_ISSUES:
+            if DEBUG_ISSUE_STATES:
                 # For organizations:issue-states
-                project_context.new_inbox_count = random.randint(5, 200)
-                project_context.escalating_inbox_count = random.randint(5, 200)
-                project_context.regression_inbox_count = random.randint(5, 200)
-                project_context.ongoing_inbox_count = random.randint(20, 3000)
-                project_context.total_inbox_count = (
-                    project_context.new_inbox_count
-                    + project_context.escalating_inbox_count
-                    + project_context.regression_inbox_count
-                    + project_context.ongoing_inbox_count
+                project_context.new_substatus_count = random.randint(5, 200)
+                project_context.escalating_substatus_count = random.randint(5, 200)
+                project_context.regression_substatus_count = random.randint(5, 200)
+                project_context.ongoing_substatus_count = random.randint(20, 3000)
+                project_context.total_substatus_count = (
+                    project_context.new_substatus_count
+                    + project_context.escalating_substatus_count
+                    + project_context.regression_substatus_count
+                    + project_context.ongoing_substatus_count
                 )
             else:
                 # Removed after organizations:issue-states GA


Test patch:
diff --git a/tests/sentry/tasks/test_weekly_reports.py b/tests/sentry/tasks/test_weekly_reports.py
--- a/tests/sentry/tasks/test_weekly_reports.py
+++ b/tests/sentry/tasks/test_weekly_reports.py
@@ -11,19 +11,12 @@
 
 from sentry.constants import DataCategory
 from sentry.db.postgres.roles import in_test_psql_role_override
-from sentry.models import (
-    GroupInboxReason,
-    GroupStatus,
-    OrganizationMember,
-    Project,
-    UserOption,
-    add_group_to_inbox,
-)
+from sentry.models import GroupStatus, GroupSubStatus, OrganizationMember, Project, UserOption
 from sentry.tasks.weekly_reports import (
     ONE_DAY,
     OrganizationReportContext,
     deliver_reports,
-    organization_project_issue_inbox_summaries,
+    organization_project_issue_substatus_summaries,
     organization_project_issue_summaries,
     prepare_organization_report,
     schedule_organizations,
@@ -177,7 +170,7 @@ def test_organization_project_issue_summaries(self):
         assert project_ctx.all_issue_count == 2
 
     @with_feature("organizations:issue-states")
-    def test_organization_project_issue_inbox_summaries(self):
+    def test_organization_project_issue_substatus_summaries(self):
         self.login_as(user=self.user)
 
         now = timezone.now()
@@ -193,7 +186,8 @@ def test_organization_project_issue_inbox_summaries(self):
             },
             project_id=self.project.id,
         )
-        add_group_to_inbox(event1.group, GroupInboxReason.NEW)
+        event1.group.substatus = GroupSubStatus.ONGOING
+        event1.group.save()
 
         event2 = self.store_event(
             data={
@@ -205,19 +199,20 @@ def test_organization_project_issue_inbox_summaries(self):
             },
             project_id=self.project.id,
         )
-        add_group_to_inbox(event2.group, GroupInboxReason.ONGOING)
+        event2.group.substatus = GroupSubStatus.NEW
+        event2.group.save()
         timestamp = to_timestamp(now)
 
         ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization)
-        organization_project_issue_inbox_summaries(ctx)
+        organization_project_issue_substatus_summaries(ctx)
 
         project_ctx = ctx.projects[self.project.id]
 
-        assert project_ctx.new_inbox_count == 1
-        assert project_ctx.escalating_inbox_count == 0
-        assert project_ctx.ongoing_inbox_count == 1
-        assert project_ctx.regression_inbox_count == 0
-        assert project_ctx.total_inbox_count == 2
+        assert project_ctx.new_substatus_count == 1
+        assert project_ctx.escalating_substatus_count == 0
+        assert project_ctx.ongoing_substatus_count == 1
+        assert project_ctx.regression_substatus_count == 0
+        assert project_ctx.total_substatus_count == 2
 
     @mock.patch("sentry.tasks.weekly_reports.MessageBuilder")
     def test_message_builder_simple(self, message_builder):
@@ -304,11 +299,11 @@ def test_message_builder_simple(self, message_builder):
                 "new_issue_count": 2,
                 "reopened_issue_count": 0,
                 # New escalating-issues
-                "escalating_inbox_count": 0,
-                "new_inbox_count": 0,
-                "ongoing_inbox_count": 0,
-                "regression_inbox_count": 0,
-                "total_inbox_count": 0,
+                "escalating_substatus_count": 0,
+                "new_substatus_count": 0,
+                "ongoing_substatus_count": 0,
+                "regression_substatus_count": 0,
+                "total_substatus_count": 0,
             }
             assert len(context["key_errors"]) == 2
             assert context["trends"]["total_error_count"] == 2
@@ -317,10 +312,8 @@ def test_message_builder_simple(self, message_builder):
 
     @mock.patch("sentry.tasks.weekly_reports.MessageBuilder")
     @with_feature("organizations:issue-states")
-    def test_message_builder_inbox_simple(self, message_builder):
+    def test_message_builder_substatus_simple(self, message_builder):
         now = timezone.now()
-
-        two_days_ago = now - timedelta(days=2)
         three_days_ago = now - timedelta(days=3)
 
         self.create_member(
@@ -337,7 +330,9 @@ def test_message_builder_inbox_simple(self, message_builder):
             },
             project_id=self.project.id,
         )
-        add_group_to_inbox(event1.group, GroupInboxReason.NEW)
+        group1 = event1.group
+        group1.substatus = GroupSubStatus.NEW
+        group1.save()
 
         event2 = self.store_event(
             data={
@@ -349,42 +344,8 @@ def test_message_builder_inbox_simple(self, message_builder):
             },
             project_id=self.project.id,
         )
-        self.store_outcomes(
-            {
-                "org_id": self.organization.id,
-                "project_id": self.project.id,
-                "outcome": Outcome.ACCEPTED,
-                "category": DataCategory.ERROR,
-                "timestamp": three_days_ago,
-                "key_id": 1,
-            },
-            num_times=2,
-        )
-        add_group_to_inbox(event2.group, GroupInboxReason.ONGOING)
-
-        self.store_outcomes(
-            {
-                "org_id": self.organization.id,
-                "project_id": self.project.id,
-                "outcome": Outcome.ACCEPTED,
-                "category": DataCategory.TRANSACTION,
-                "timestamp": three_days_ago,
-                "key_id": 1,
-            },
-            num_times=10,
-        )
-
-        group1 = event1.group
         group2 = event2.group
-
-        group1.status = GroupStatus.RESOLVED
-        group1.substatus = None
-        group1.resolved_at = two_days_ago
-        group1.save()
-
-        group2.status = GroupStatus.RESOLVED
-        group2.substatus = None
-        group2.resolved_at = two_days_ago
+        group2.substatus = GroupSubStatus.ONGOING
         group2.save()
 
         prepare_organization_report(to_timestamp(now), ONE_DAY * 7, self.organization.id)
@@ -402,16 +363,12 @@ def test_message_builder_inbox_simple(self, message_builder):
                 "existing_issue_count": 0,
                 "new_issue_count": 0,
                 "reopened_issue_count": 0,
-                "escalating_inbox_count": 0,
-                "new_inbox_count": 1,
-                "ongoing_inbox_count": 1,
-                "regression_inbox_count": 0,
-                "total_inbox_count": 2,
+                "escalating_substatus_count": 0,
+                "new_substatus_count": 1,
+                "ongoing_substatus_count": 1,
+                "regression_substatus_count": 0,
+                "total_substatus_count": 2,
             }
-            assert len(context["key_errors"]) == 2
-            assert context["trends"]["total_error_count"] == 2
-            assert context["trends"]["total_transaction_count"] == 10
-            assert "Weekly Report for" in message_params["subject"]
 
     @mock.patch("sentry.tasks.weekly_reports.MessageBuilder")
     def test_message_builder_advanced(self, message_builder):