diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 25d51e875af5..e6ef1d0247d0 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -384,6 +384,8 @@ def register_temporary_features(manager: FeatureManager) -> None: # Use batched Snuba queries for weekly report key errors instead of per-project queries manager.add("organizations:weekly-report-batched-key-errors", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + # Show combined resolved "past issues" section instead of separate key errors / performance issues + manager.add("organizations:weekly-report-past-issues", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable week-over-week percentage change metric in weekly email reports manager.add("organizations:weekly-report-week-over-week-metric", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable logging to debug workflow engine process workflows diff --git a/src/sentry/snuba/referrer.py b/src/sentry/snuba/referrer.py index 5dac5652f65c..2c30a7d1e06c 100644 --- a/src/sentry/snuba/referrer.py +++ b/src/sentry/snuba/referrer.py @@ -787,6 +787,7 @@ class Referrer(StrEnum): REPORTS_KEY_ERRORS = "reports.key_errors" REPORTS_KEY_ERRORS_BATCHED = "reports.key_errors.batched" REPORTS_KEY_PERFORMANCE_ISSUES = "reports.key_performance_issues" + REPORTS_PAST_RESOLVED_ISSUES = "reports.past_resolved_issues" REPORTS_KEY_TRANSACTIONS_THIS_WEEK = "reports.key_transactions.this_week" REPORTS_KEY_TRANSACTIONS_LAST_WEEK = "reports.key_transactions.last_week" REPORTS_OUTCOME_SERIES = "reports.outcome_series" diff --git a/src/sentry/tasks/summaries/organization_report_context_factory.py b/src/sentry/tasks/summaries/organization_report_context_factory.py index 23a373737b18..551be40388bb 100644 --- a/src/sentry/tasks/summaries/organization_report_context_factory.py +++ b/src/sentry/tasks/summaries/organization_report_context_factory.py @@ -11,6 +11,7 @@ ProjectContext, fetch_key_error_groups, fetch_key_performance_issue_groups, + fetch_past_resolved_issue_links, org_key_errors, organization_project_issue_substatus_summaries, project_event_counts_for_organization, @@ -18,6 +19,7 @@ project_key_performance_issues, project_key_transactions_last_week, project_key_transactions_this_week, + project_past_resolved_issues, ) from sentry.tasks.summaries.weekly_report_cache import read_project_metrics from sentry.utils import metrics @@ -214,6 +216,21 @@ def _hydrate_key_performance_issue_groups(self, ctx: OrganizationReportContext) with sentry_sdk.start_span(op="weekly_reports.fetch_key_performance_issue_groups"): fetch_key_performance_issue_groups(ctx) + @metrics.wraps("weekly_report.create_context.project_past_resolved_issues") + def _append_project_past_resolved_issues(self, ctx: OrganizationReportContext) -> None: + with sentry_sdk.start_span(op="weekly_reports.project_past_resolved_issues"): + for project in ctx.organization.project_set.all(): + if project.id not in ctx.projects_context_map: + continue + project_ctx = ctx.projects_context_map[project.id] + resolved = project_past_resolved_issues( + ctx, project, referrer=Referrer.REPORTS_PAST_RESOLVED_ISSUES.value + ) + if resolved: + project_ctx.past_resolved_issues = resolved + + fetch_past_resolved_issue_links(ctx) + def create_context(self) -> OrganizationReportContext: ctx = OrganizationReportContext(self.timestamp, self.duration, self.organization) @@ -234,5 +251,7 @@ def create_context(self) -> OrganizationReportContext: self._append_project_key_errors(ctx) self._hydrate_key_error_groups(ctx) self._hydrate_key_performance_issue_groups(ctx) + if features.has("organizations:weekly-report-past-issues", self.organization): + self._append_project_past_resolved_issues(ctx) return ctx diff --git a/src/sentry/tasks/summaries/utils.py b/src/sentry/tasks/summaries/utils.py index 2ff8eb191358..f41df84c018c 100644 --- a/src/sentry/tasks/summaries/utils.py +++ b/src/sentry/tasks/summaries/utils.py @@ -16,8 +16,14 @@ from snuba_sdk.relationships import Relationship from sentry.constants import DataCategory +from sentry.issues.grouptype import ( + PERFORMANCE_ISSUE_CATEGORIES, + GroupCategory, + InvalidGroupTypeError, +) from sentry.models.group import Group, GroupStatus from sentry.models.grouphistory import GroupHistory +from sentry.models.grouplink import GroupLink from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.project import Project @@ -87,6 +93,8 @@ def __init__(self, project): self.key_transactions = [] # Array of (Group, count) self.key_performance_issues = [] + # Array of (Group, event_count, has_linked_pr_or_commit) + self.past_resolved_issues: list[tuple[Group, int, bool]] = [] self.key_replay_events = [] @@ -109,6 +117,7 @@ def check_if_project_is_empty(self): not self.key_errors_by_group and not self.key_transactions and not self.key_performance_issues + and not self.past_resolved_issues and not self.accepted_error_count and not self.accepted_transaction_count ) @@ -737,3 +746,186 @@ def organization_project_issue_substatus_summaries(ctx: OrganizationReportContex if item["substatus"] == GroupSubStatus.REGRESSED: project_ctx.regression_substatus_count = item["total"] project_ctx.total_substatus_count += item["total"] + + +PAST_ISSUES_CANDIDATE_LIMIT = 50 +PAST_ISSUES_LINK_BOOST = 2 + + +def project_past_resolved_issues( + ctx: OrganizationReportContext, project: Project, referrer: str +) -> list[tuple[Group, int, bool]]: + if not project.first_event: + return [] + + with sentry_sdk.start_span(op="weekly_reports.project_past_resolved_issues"): + candidates = list( + Group.objects.filter( + project_id=project.id, + status=GroupStatus.RESOLVED, + resolved_at__gte=ctx.start, + resolved_at__lt=ctx.end + timedelta(days=1), + ).order_by("-times_seen")[:PAST_ISSUES_CANDIDATE_LIMIT] + ) + + if not candidates: + return [] + + # Filter out groups with unregistered type IDs (deprecated/removed issue types) + valid_candidates = [] + for g in candidates: + if g.type is None: + valid_candidates.append(g) + continue + try: + g.issue_category + except InvalidGroupTypeError: + continue + valid_candidates.append(g) + + group_id_to_group = {g.id: g for g in valid_candidates} + + # Legacy groups may have a None .type which crashes issue_category; treat as error group + error_group_ids = [ + g.id + for g in valid_candidates + if g.type is None or g.issue_category == GroupCategory.ERROR + ] + perf_group_ids = [ + g.id + for g in valid_candidates + if g.type is not None + and ( + g.issue_category == GroupCategory.PERFORMANCE + or g.issue_category in PERFORMANCE_ISSUE_CATEGORIES + ) + ] + + event_counts: dict[int, int] = {} + + if error_group_ids: + error_counts = _past_resolved_error_counts(ctx, project, error_group_ids, referrer) + event_counts.update(error_counts) + + if perf_group_ids: + perf_counts = _past_resolved_perf_counts(ctx, project, perf_group_ids, referrer) + event_counts.update(perf_counts) + + # has_link is initially False; updated by fetch_past_resolved_issue_links at org level + scored = [] + for group_id, count in event_counts.items(): + group = group_id_to_group.get(group_id) + if group is None: + continue + scored.append((group, count, False)) + + scored.sort(key=lambda x: x[1], reverse=True) + return scored + + +def _past_resolved_error_counts( + ctx: OrganizationReportContext, + project: Project, + group_ids: list[int], + referrer: str, +) -> dict[int, int]: + events_entity = Entity("events", alias="events") + group_attributes_entity = Entity("group_attributes", alias="group_attributes") + query = Query( + match=Join([Relationship(events_entity, "attributes", group_attributes_entity)]), + select=[Column("group_id", entity=events_entity), Function("count", [])], + where=[ + Condition(Column("timestamp", entity=events_entity), Op.GTE, ctx.start), + Condition( + Column("timestamp", entity=events_entity), + Op.LT, + ctx.end + timedelta(days=1), + ), + Condition(Column("project_id", entity=events_entity), Op.EQ, project.id), + Condition(Column("project_id", entity=group_attributes_entity), Op.EQ, project.id), + Condition( + Column("group_id", entity=events_entity), + Op.IN, + group_ids, + ), + Condition( + Column("group_status", entity=group_attributes_entity), + Op.EQ, + GroupStatus.RESOLVED, + ), + ], + groupby=[Column("group_id", entity=events_entity)], + orderby=[OrderBy(Function("count", []), Direction.DESC)], + limit=Limit(len(group_ids)), + ) + + request = Request( + dataset=Dataset.Events.value, + app_id="reports", + query=query, + tenant_ids={"organization_id": ctx.organization.id}, + ) + rows = raw_snql_query(request, referrer=referrer)["data"] + return {row["events.group_id"]: row["count()"] for row in rows} + + +def _past_resolved_perf_counts( + ctx: OrganizationReportContext, + project: Project, + group_ids: list[int], + referrer: str, +) -> dict[int, int]: + query = Query( + match=Entity("search_issues"), + select=[Column("group_id"), Function("count", [])], + where=[ + Condition(Column("group_id"), Op.IN, group_ids), + Condition(Column("timestamp"), Op.GTE, ctx.start), + Condition(Column("timestamp"), Op.LT, ctx.end + timedelta(days=1)), + Condition(Column("project_id"), Op.EQ, project.id), + ], + groupby=[Column("group_id")], + orderby=[OrderBy(Function("count", []), Direction.DESC)], + limit=Limit(len(group_ids)), + ) + request = Request( + dataset=Dataset.IssuePlatform.value, + app_id="reports", + query=query, + tenant_ids={"organization_id": ctx.organization.id}, + ) + rows = raw_snql_query(request, referrer=referrer)["data"] + return {row["group_id"]: row["count()"] for row in rows} + + +def fetch_past_resolved_issue_links(ctx: OrganizationReportContext) -> None: + all_group_ids: list[int] = [] + for project_ctx in ctx.projects_context_map.values(): + all_group_ids.extend( + group.id for group, _count, _has_link in project_ctx.past_resolved_issues + ) + + if not all_group_ids: + return + + groups_with_links = set( + GroupLink.objects.filter( + group_id__in=all_group_ids, + linked_type__in=[GroupLink.LinkedType.commit, GroupLink.LinkedType.pull_request], + relationship=GroupLink.Relationship.resolves, + ).values_list("group_id", flat=True) + ) + + for project_ctx in ctx.projects_context_map.values(): + project_ctx.past_resolved_issues = [ + (group, count, group.id in groups_with_links) + for group, count, _has_link in project_ctx.past_resolved_issues + ] + + # Re-sort with link boost applied, then truncate to top 3 + for project_ctx in ctx.projects_context_map.values(): + project_ctx.past_resolved_issues.sort( + key=lambda x: x[1] * (PAST_ISSUES_LINK_BOOST if x[2] else 1), + reverse=True, + ) + project_ctx.past_resolved_issues = project_ctx.past_resolved_issues[:3] diff --git a/src/sentry/tasks/summaries/weekly_reports.py b/src/sentry/tasks/summaries/weekly_reports.py index cde6f96e5927..61e54e36d27e 100644 --- a/src/sentry/tasks/summaries/weekly_reports.py +++ b/src/sentry/tasks/summaries/weekly_reports.py @@ -36,7 +36,7 @@ from sentry.tasks.summaries.organization_report_context_factory import ( OrganizationReportContextFactory, ) -from sentry.tasks.summaries.utils import ONE_DAY, OrganizationReportContext +from sentry.tasks.summaries.utils import ONE_DAY, PAST_ISSUES_LINK_BOOST, OrganizationReportContext from sentry.tasks.summaries.weekly_report_cache import cache_project_metrics from sentry.taskworker.namespaces import reports_tasks from sentry.types.group import GroupSubStatus @@ -780,6 +780,23 @@ def all_key_performance_issues(): return heapq.nlargest(3, all_key_performance_issues(), lambda d: d["count"]) + def past_issues(): + def all_past_issues(): + for project_ctx in user_projects: + for group, count, has_linked_pr_or_commit in project_ctx.past_resolved_issues: + display = get_group_display(group) + yield { + "count": count, + "group": group, + "title": display["title"], + "message": display["message"], + "has_linked_pr_or_commit": has_linked_pr_or_commit, + "_relevance": count + * (PAST_ISSUES_LINK_BOOST if has_linked_pr_or_commit else 1), + } + + return heapq.nlargest(3, all_past_issues(), lambda d: d["_relevance"]) + def issue_summary(): new_substatus_count = 0 escalating_substatus_count = 0 @@ -800,6 +817,8 @@ def issue_summary(): "total_substatus_count": total_substatus_count, } + show_past_issues = features.has("organizations:weekly-report-past-issues", ctx.organization) + return { "organization": ctx.organization, "start": date_format(local_start), @@ -808,6 +827,8 @@ def issue_summary(): "key_errors": key_errors(), "key_transactions": key_transactions(), "key_performance_issues": key_performance_issues(), + "past_issues": past_issues() if show_past_issues else [], + "show_past_issues": show_past_issues, "issue_summary": issue_summary(), "user_project_count": len(user_projects), "notification_uuid": notification_uuid, diff --git a/src/sentry/templates/sentry/emails/reports/body.html b/src/sentry/templates/sentry/emails/reports/body.html index 50a37651ca57..e43f1f1471b6 100644 --- a/src/sentry/templates/sentry/emails/reports/body.html +++ b/src/sentry/templates/sentry/emails/reports/body.html @@ -470,6 +470,25 @@

Most frequent transactions

{% endfor %} {% endif %} + {% if past_issues|length > 0 and not enhanced_privacy %} +
+

Resolved this week

+ {% for a in past_issues %} +
+
{{a.count|small_count:1}}
+
+ {% url 'sentry-organization-issue-detail' issue_id=a.group.id organization_slug=organization.slug as issue_detail %} + {% querystring referrer="weekly_report" notification_uuid=notification_uuid as query %} + {{a.title}} +
{{a.message}}
+
{{a.group.project.name}}
+
+ Resolved +
+ {% endfor %} +
+ {% endif %} {% endblock %} diff --git a/src/sentry/web/frontend/debug/debug_weekly_report.py b/src/sentry/web/frontend/debug/debug_weekly_report.py index 119cd102c679..edce066eac2f 100644 --- a/src/sentry/web/frontend/debug/debug_weekly_report.py +++ b/src/sentry/web/frontend/debug/debug_weekly_report.py @@ -1,6 +1,7 @@ import time from datetime import datetime, timedelta, timezone from random import Random +from typing import Any from django.utils.decorators import method_decorator from django.utils.text import slugify @@ -16,7 +17,7 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.tasks.summaries.utils import ONE_DAY, OrganizationReportContext, ProjectContext -from sentry.tasks.summaries.weekly_reports import render_template_context +from sentry.tasks.summaries.weekly_reports import get_group_display, render_template_context from sentry.types.group import GroupSubStatus from sentry.utils import loremipsum from sentry.utils.dates import floor_to_utc_day, to_datetime @@ -192,6 +193,27 @@ def get_context(self, request): for group_index, performance_issue_type in enumerate(performance_issue_types) ] + project_context.past_resolved_issues = [ + ( + make_debug_group( + group_id=30000 + (project.id * 100) + group_index, + project=project, + title=make_debug_issue_title( + random, + random.choice(["TypeError", "ValueError", "RuntimeError"]), + ), + message=make_debug_issue_message(random), + group_type=ErrorGroupType, + event_type="error", + status=GroupStatus.RESOLVED, + substatus=GroupSubStatus.NEW, + ), + random.randint(100, 5000), + random.choice([True, False]), + ) + for group_index in range(3) + ] + ctx.projects_context_map[project.id] = project_context user_id = request.user.id @@ -201,6 +223,22 @@ def get_context(self, request): context["show_week_over_week_metric"] = ( request.GET.get("show_week_over_week_metric", "1") != "0" ) + context["show_past_issues"] = True + past_issues: list[dict[str, Any]] = [] + for project_ctx in ctx.projects_context_map.values(): + for group, count, has_link in project_ctx.past_resolved_issues: + display = get_group_display(group) + past_issues.append( + { + "count": count, + "group": group, + "title": display["title"], + "message": display["message"], + "has_linked_pr_or_commit": has_link, + } + ) + past_issues.sort(key=lambda x: x["count"], reverse=True) + context["past_issues"] = past_issues[:3] return context @property diff --git a/tests/sentry/tasks/test_weekly_reports.py b/tests/sentry/tasks/test_weekly_reports.py index 74fb34e42d4e..097e5e07af6e 100644 --- a/tests/sentry/tasks/test_weekly_reports.py +++ b/tests/sentry/tasks/test_weekly_reports.py @@ -12,9 +12,10 @@ from sentry.analytics.events.weekly_report import WeeklyReportSent from sentry.constants import DataCategory -from sentry.issues.grouptype import PerformanceNPlusOneGroupType +from sentry.issues.grouptype import GroupCategory, PerformanceNPlusOneGroupType from sentry.models.group import GroupStatus from sentry.models.grouphistory import GroupHistoryStatus +from sentry.models.grouplink import GroupLink from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.project import Project @@ -35,9 +36,11 @@ _project_key_errors_snuba, _project_key_performance_issues_eap, _project_key_performance_issues_snuba, + fetch_past_resolved_issue_links, org_key_errors, organization_project_issue_substatus_summaries, project_key_errors, + project_past_resolved_issues, user_project_ownership, ) from sentry.tasks.summaries.weekly_reports import ( @@ -1753,3 +1756,263 @@ def test_pct_change_helper(self) -> None: assert _pct_change(100, 0) is None assert _pct_change(0, 0) is None assert _pct_change(100, 100) is None + + @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) + def test_past_resolved_issues_basic(self) -> None: + self.project.first_event = self.now - timedelta(days=3) + self.project.save() + min_ago = (self.now - timedelta(minutes=1)).isoformat() + + event1 = self.store_event( + data={ + "event_id": "a" * 32, + "message": "resolved error", + "timestamp": min_ago, + "fingerprint": ["resolved-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + group1 = event1.group + group1.status = GroupStatus.RESOLVED + group1.substatus = None + group1.resolved_at = self.now - timedelta(minutes=1) + group1.save() + + timestamp = self.now.timestamp() + ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization) + + results = project_past_resolved_issues( + ctx, self.project, Referrer.REPORTS_PAST_RESOLVED_ISSUES.value + ) + assert len(results) == 1 + assert results[0][0].id == group1.id + assert results[0][1] >= 1 + assert results[0][2] is False + + @mock.patch("sentry.tasks.summaries.utils._past_resolved_perf_counts") + @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) + def test_past_resolved_issues_includes_current_performance_categories( + self, mock_perf_counts: mock.MagicMock + ) -> None: + self.project.first_event = self.now - timedelta(days=3) + self.project.save() + + perf_event = self.create_performance_issue() + assert perf_event.group is not None + group = perf_event.group + assert group.issue_category != GroupCategory.PERFORMANCE + group.status = GroupStatus.RESOLVED + group.substatus = None + group.resolved_at = self.now - timedelta(minutes=1) + group.save() + mock_perf_counts.return_value = {group.id: 1} + + timestamp = self.now.timestamp() + ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization) + + results = project_past_resolved_issues( + ctx, self.project, Referrer.REPORTS_PAST_RESOLVED_ISSUES.value + ) + + assert results == [(group, 1, False)] + mock_perf_counts.assert_called_once() + assert mock_perf_counts.call_args.args[2] == [group.id] + + @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) + def test_past_resolved_issues_excludes_unresolved(self) -> None: + self.project.first_event = self.now - timedelta(days=3) + self.project.save() + min_ago = (self.now - timedelta(minutes=1)).isoformat() + + event1 = self.store_event( + data={ + "event_id": "a" * 32, + "message": "unresolved error", + "timestamp": min_ago, + "fingerprint": ["unresolved-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + assert event1.group is not None + assert event1.group.status == GroupStatus.UNRESOLVED + + timestamp = self.now.timestamp() + ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization) + + results = project_past_resolved_issues( + ctx, self.project, Referrer.REPORTS_PAST_RESOLVED_ISSUES.value + ) + assert len(results) == 0 + + @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) + def test_past_resolved_issues_excludes_outside_window(self) -> None: + self.project.first_event = self.now - timedelta(days=30) + self.project.save() + min_ago = (self.now - timedelta(minutes=1)).isoformat() + + event1 = self.store_event( + data={ + "event_id": "a" * 32, + "message": "old resolved error", + "timestamp": min_ago, + "fingerprint": ["old-resolved-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + group1 = event1.group + group1.status = GroupStatus.RESOLVED + group1.substatus = None + group1.resolved_at = self.now - timedelta(days=14) + group1.save() + + timestamp = self.now.timestamp() + ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization) + + results = project_past_resolved_issues( + ctx, self.project, Referrer.REPORTS_PAST_RESOLVED_ISSUES.value + ) + assert len(results) == 0 + + @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) + def test_fetch_past_resolved_issue_links(self) -> None: + self.project.first_event = self.now - timedelta(days=3) + self.project.save() + min_ago = (self.now - timedelta(minutes=1)).isoformat() + + event1 = self.store_event( + data={ + "event_id": "a" * 32, + "message": "linked error", + "timestamp": min_ago, + "fingerprint": ["linked-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + event2 = self.store_event( + data={ + "event_id": "b" * 32, + "message": "unlinked error", + "timestamp": min_ago, + "fingerprint": ["unlinked-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + + group1 = event1.group + group1.status = GroupStatus.RESOLVED + group1.substatus = None + group1.resolved_at = self.now - timedelta(minutes=1) + group1.save() + + group2 = event2.group + group2.status = GroupStatus.RESOLVED + group2.substatus = None + group2.resolved_at = self.now - timedelta(minutes=1) + group2.save() + + GroupLink.objects.create( + group=group1, + project=self.project, + linked_type=GroupLink.LinkedType.commit, + linked_id=1, + relationship=GroupLink.Relationship.resolves, + ) + GroupLink.objects.create( + group=group2, + project=self.project, + linked_type=GroupLink.LinkedType.commit, + linked_id=2, + relationship=GroupLink.Relationship.references, + ) + + timestamp = self.now.timestamp() + ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization) + + results = project_past_resolved_issues( + ctx, self.project, Referrer.REPORTS_PAST_RESOLVED_ISSUES.value + ) + ctx.projects_context_map[self.project.id].past_resolved_issues = results + + fetch_past_resolved_issue_links(ctx) + + updated = ctx.projects_context_map[self.project.id].past_resolved_issues + has_link_by_group = {group.id: has_link for group, _count, has_link in updated} + assert has_link_by_group[group1.id] is True + assert has_link_by_group[group2.id] is False + + @mock.patch("sentry.analytics.record") + @mock.patch("sentry.tasks.summaries.weekly_reports.MessageBuilder") + @with_feature("organizations:weekly-report-past-issues") + def test_past_issues_in_template_context( + self, message_builder: mock.MagicMock, record: mock.MagicMock + ) -> None: + user = self.create_user() + self.create_member(teams=[self.team], user=user, organization=self.organization) + + event1 = self.store_event( + data={ + "event_id": "a" * 32, + "message": "resolved issue", + "timestamp": self.three_days_ago.isoformat(), + "fingerprint": ["past-issue-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + self.store_event_outcomes( + self.organization.id, self.project.id, self.three_days_ago, num_times=1 + ) + + group1 = event1.group + group1.status = GroupStatus.RESOLVED + group1.substatus = None + group1.resolved_at = self.two_days_ago + group1.save() + + prepare_organization_report( + self.now.timestamp(), ONE_DAY * 7, self.organization.id, self._dummy_batch_id + ) + + for call_args in message_builder.call_args_list: + context = call_args.kwargs["context"] + assert context["show_past_issues"] is True + assert len(context["past_issues"]) == 1 + assert context["past_issues"][0]["group"].id == group1.id + assert context["past_issues"][0]["count"] >= 1 + + @mock.patch("sentry.analytics.record") + @mock.patch("sentry.tasks.summaries.weekly_reports.MessageBuilder") + def test_past_issues_flag_off_uses_old_sections( + self, message_builder: mock.MagicMock, record: mock.MagicMock + ) -> None: + user = self.create_user() + self.create_member(teams=[self.team], user=user, organization=self.organization) + + self.store_event( + data={ + "event_id": "a" * 32, + "message": "error issue", + "timestamp": self.three_days_ago.isoformat(), + "fingerprint": ["old-section-1"], + }, + project_id=self.project.id, + default_event_type=EventType.DEFAULT, + ) + self.store_event_outcomes( + self.organization.id, self.project.id, self.three_days_ago, num_times=1 + ) + + prepare_organization_report( + self.now.timestamp(), ONE_DAY * 7, self.organization.id, self._dummy_batch_id + ) + + for call_args in message_builder.call_args_list: + context = call_args.kwargs["context"] + assert context["show_past_issues"] is False + assert len(context["past_issues"]) == 0 + assert len(context["key_errors"]) == 1