diff --git a/src/sentry/tasks/summaries/weekly_reports.py b/src/sentry/tasks/summaries/weekly_reports.py index 1010439b8a7709..cde6f96e5927d3 100644 --- a/src/sentry/tasks/summaries/weekly_reports.py +++ b/src/sentry/tasks/summaries/weekly_reports.py @@ -459,20 +459,20 @@ def record_delivery(self) -> bool: return is_duplicate_detected -project_breakdown_colors = ["#422C6E", "#895289", "#D6567F", "#F38150", "#F2B713"] +project_breakdown_colors = ["#7553FF", "#7C2282", "#F0369A", "#FF9838", "#FFD00E"] total_color = """ linear-gradient( -45deg, - #ccc 25%, + #A29FAA 25%, transparent 25%, transparent 50%, - #ccc 50%, - #ccc 75%, + #A29FAA 50%, + #A29FAA 75%, transparent 75%, transparent ); """ -other_color = "#f2f0fa" +other_color = "#DAD9DE" group_status_to_color = { GroupHistoryStatus.UNRESOLVED: "#FAD473", GroupHistoryStatus.RESOLVED: "#8ACBBC", @@ -513,19 +513,47 @@ def _pct_change(current: int, previous: int) -> str | None: 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 + Returns a tuple of (text, background_color, text_color) + Matches frontend Tag component: background.transparent.*.muted blended on white, content.* text. """ if group.status == GroupStatus.RESOLVED: - return ("Resolved", "rgba(108, 95, 199, 0.08)", "rgba(108, 95, 199, 0.5)") + return ("Resolved", "#E3F7E3", "#008900") if group.status == GroupStatus.UNRESOLVED: if group.substatus == GroupSubStatus.NEW: - return ("New", "rgba(245, 176, 0, 0.08)", "rgba(245, 176, 0, 0.55)") + return ("New", "#F9F0D2", "#A45200") if group.substatus == GroupSubStatus.REGRESSED: - return ("Regressed", "rgba(108, 95, 199, 0.08)", "rgba(108, 95, 199, 0.5)") + return ("Regressed", "#EDEEFE", "#653DE9") 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)") + return ("Escalating", "#FEE7E4", "#D50000") + return ("Ongoing", "#F0F0F2", "#6A6772") + + +def get_group_display(group: Group) -> dict[str, str]: + metadata = group.get_event_metadata() + event_type = group.get_event_type() + custom_title = metadata.get("title") + + if event_type == "error": + title = ( + custom_title + if custom_title and custom_title != "" + else metadata.get("type") or metadata.get("function") or "" + ) + message = metadata.get("value") + elif event_type in ("transaction", "generic"): + title = custom_title or group.title + message = metadata.get("value") + elif event_type == "csp": + title = custom_title or metadata.get("directive") or "" + message = metadata.get("message") + else: + title = custom_title or group.title + message = group.culprit + + return { + "title": title, + "message": message or group.message or "", + } def get_local_dates(ctx: OrganizationReportContext, user_id: int) -> tuple[datetime, datetime]: @@ -681,20 +709,23 @@ def key_errors(): def all_key_errors(): for project_ctx in user_projects: for group, count in project_ctx.key_errors_by_group: + display = get_group_display(group) ( substatus, substatus_color, - substatus_border_color, + substatus_text_color, ) = get_group_status_badge(group) yield { "count": count, "group": group, + "title": display["title"], + "message": display["message"], "status": "Unresolved", "status_color": (group_status_to_color[GroupHistoryStatus.NEW]), "group_substatus": substatus, "group_substatus_color": substatus_color, - "group_substatus_border_color": substatus_border_color, + "group_substatus_text_color": substatus_text_color, } return heapq.nlargest(3, all_key_errors(), lambda d: d["count"]) @@ -723,9 +754,17 @@ def key_performance_issues(): def all_key_performance_issues(): for project_ctx in user_projects: for group, group_history, count in project_ctx.key_performance_issues: + display = get_group_display(group) + ( + substatus, + substatus_color, + substatus_text_color, + ) = get_group_status_badge(group) yield { "count": count, "group": group, + "title": display["title"], + "message": display["message"], "status": ( group_history.get_status_display() if group_history else "Unresolved" ), @@ -734,6 +773,9 @@ def all_key_performance_issues(): if group_history else group_status_to_color[GroupHistoryStatus.NEW] ), + "group_substatus": substatus, + "group_substatus_color": substatus_color, + "group_substatus_text_color": substatus_text_color, } return heapq.nlargest(3, all_key_performance_issues(), lambda d: d["count"]) diff --git a/src/sentry/templates/sentry/emails/reports/body.html b/src/sentry/templates/sentry/emails/reports/body.html index 513e8052f803ec..50a37651ca5718 100644 --- a/src/sentry/templates/sentry/emails/reports/body.html +++ b/src/sentry/templates/sentry/emails/reports/body.html @@ -117,7 +117,7 @@ .project-breakdown .summary thead th { font-size: 12px; text-transform: uppercase; - color: #88859a; + color: #2f2936; font-weight: 500; } @@ -198,8 +198,7 @@

Total Project Errors

{{ trends.total_error_count|small_count:1 }} {% if show_week_over_week_metric and trends.error_pct_change %} - {{ trends.error_pct_change }} - (vs. last week) + {{ trends.error_pct_change }} {% endif %}

{% url 'sentry-organization-issue-list' organization.slug as issue_list %} @@ -218,7 +217,7 @@

{% empty %} - + {% endfor %} @@ -241,8 +240,7 @@

Total Project Transactions

{{ trends.total_transaction_count|small_count:1 }} {% if show_week_over_week_metric and trends.transaction_pct_change %} - {{ trends.transaction_pct_change }} - (vs. last week) + {{ trends.transaction_pct_change }} {% endif %}

{% url 'sentry-organization-performance' organization.slug as performance_landing %} @@ -260,7 +258,7 @@

{% empty %} - + {% endfor %} @@ -329,22 +327,22 @@

Something slow?

Errors by Issue Type

- New: {% percent issue_summary.new_issue_count issue_summary.all_issue_count "0.1f" %}% - Reopened: {% percent issue_summary.reopened_issue_count issue_summary.all_issue_count "0.1f" %}% - Existing: {% percent issue_summary.existing_issue_count issue_summary.all_issue_count "0.1f" %}% + New: {% percent issue_summary.new_issue_count issue_summary.all_issue_count "0.1f" %}% + Reopened: {% percent issue_summary.reopened_issue_count issue_summary.all_issue_count "0.1f" %}% + Existing: {% percent issue_summary.existing_issue_count issue_summary.all_issue_count "0.1f" %}% - - - @@ -361,26 +359,26 @@

Errors by Issue Type

Issues Breakdown

+   +   +  
- New ({{ issue_summary.new_substatus_count }}) - Escalating ({{ issue_summary.escalating_substatus_count }}) - Regressed ({{ issue_summary.regression_substatus_count }}) - Ongoing ({{ issue_summary.ongoing_substatus_count }}) + New ({{ issue_summary.new_substatus_count }}) + Escalating ({{ issue_summary.escalating_substatus_count }}) + Regressed ({{ issue_summary.regression_substatus_count }}) + Ongoing ({{ issue_summary.ongoing_substatus_count }})
- - - - @@ -408,17 +406,18 @@

Issues with the most errors

{% for a in key_errors %}
{{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.group.message}} + {{a.title}} +
{{a.message}}
{{a.group.project.name}}
{% if a.group_substatus and a.group_substatus_color %} - {{a.group_substatus}} + {{a.group_substatus}} {% else %} - {{a.status}} + {{a.status}} {% endif %}
{% endfor %} @@ -430,14 +429,19 @@

Most frequent performance issues

{% for a in key_performance_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.group.message}} -
{{a.group.get_type_display}}
+ {{a.title}} +
{{a.message}}
+
{{a.group.project.name}}
- {{a.status}} + {% if a.group_substatus and a.group_substatus_color %} + {{a.group_substatus}} + {% else %} + {{a.status}} + {% endif %}
{% endfor %}
diff --git a/src/sentry/web/frontend/debug/debug_weekly_report.py b/src/sentry/web/frontend/debug/debug_weekly_report.py index 2886d0b639315a..119cd102c679fc 100644 --- a/src/sentry/web/frontend/debug/debug_weekly_report.py +++ b/src/sentry/web/frontend/debug/debug_weekly_report.py @@ -5,11 +5,19 @@ from django.utils.decorators import method_decorator from django.utils.text import slugify -from sentry.models.group import Group +from sentry.grouping.grouptype import ErrorGroupType +from sentry.issues.grouptype import ( + GroupType, + PerformanceNPlusOneGroupType, + PerformanceP95EndpointRegressionGroupType, + PerformanceSlowDBQueryGroupType, +) +from sentry.models.group import Group, GroupStatus 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.types.group import GroupSubStatus from sentry.utils import loremipsum from sentry.utils.dates import floor_to_utc_day, to_datetime from sentry.web.decorators import login_required @@ -23,6 +31,38 @@ def get_random(request): return Random(seed) +def make_debug_group( + *, + group_id: int, + project: Project, + title: str, + message: str, + group_type: type[GroupType], + event_type: str, + status: int = GroupStatus.UNRESOLVED, + substatus: int = GroupSubStatus.ONGOING, +) -> Group: + group = Group( + id=group_id, + project=project, + project_id=project.id, + message=message, + status=status, + substatus=substatus, + type=group_type.type_id, + data={"type": event_type, "metadata": {"title": title, "value": message}}, + ) + return group + + +def make_debug_issue_message(random: Random) -> str: + return f"{' '.join(random.sample(loremipsum.words, 18))}" + + +def make_debug_issue_title(random: Random, prefix: str) -> str: + return f"{prefix}" + + @internal_cell_silo_view @method_decorator(login_required, name="dispatch") class DebugWeeklyReportView(MailPreviewView): @@ -80,8 +120,31 @@ def get_context(self, request): project_context.prev_week_accepted_transaction_count = int( project_context.accepted_transaction_count * random.uniform(0.5, 1.5) ) + substatuses = [ + (GroupStatus.UNRESOLVED, GroupSubStatus.NEW), + (GroupStatus.UNRESOLVED, GroupSubStatus.ESCALATING), + (GroupStatus.UNRESOLVED, GroupSubStatus.REGRESSED), + (GroupStatus.RESOLVED, GroupSubStatus.NEW), + (GroupStatus.UNRESOLVED, GroupSubStatus.ONGOING), + ] project_context.key_errors_by_group = [ - (g, random.randint(0, 1000)) for g in Group.objects.all()[:3] + ( + make_debug_group( + group_id=10000 + (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=status, + substatus=substatus, + ), + random.randint(100, 1000), + ) + for group_index, (status, substatus) in enumerate(substatuses) ] project_context.new_substatus_count = random.randint(5, 200) @@ -106,9 +169,27 @@ def get_context(self, request): ) for _ in range(0, 3) ] + performance_issue_types = [ + PerformanceSlowDBQueryGroupType, + PerformanceNPlusOneGroupType, + PerformanceP95EndpointRegressionGroupType, + ] project_context.key_performance_issues = [ - (g, None, random.randint(0, 1000)) - for g in Group.objects.filter(type__gte=1000, type__lt=2000).all()[:3] + ( + make_debug_group( + group_id=20000 + (project.id * 100) + group_index, + project=project, + title=make_debug_issue_title(random, performance_issue_type.description), + message=make_debug_issue_message(random), + group_type=performance_issue_type, + event_type="transaction", + status=substatuses[group_index][0], + substatus=substatuses[group_index][1], + ), + None, + random.randint(100, 1000), + ) + for group_index, performance_issue_type in enumerate(performance_issue_types) ] ctx.projects_context_map[project.id] = project_context diff --git a/tests/sentry/tasks/test_weekly_reports.py b/tests/sentry/tasks/test_weekly_reports.py index 54aa7dfd2cfcc5..74fb34e42d4e4e 100644 --- a/tests/sentry/tasks/test_weekly_reports.py +++ b/tests/sentry/tasks/test_weekly_reports.py @@ -30,6 +30,7 @@ from sentry.tasks.summaries.utils import ( ONE_DAY, OrganizationReportContext, + ProjectContext, _project_key_errors_eap, _project_key_errors_snuba, _project_key_performance_issues_eap, @@ -46,6 +47,7 @@ group_status_to_color, prepare_organization_report, prepare_template_context, + render_template_context, schedule_organizations, ) from sentry.testutils.cases import ( @@ -1046,13 +1048,13 @@ def test_message_builder_advanced(self, message_builder: mock.MagicMock) -> None assert ctx["trends"]["legend"][0] == { "slug": "bar", "url": f"http://testserver/organizations/baz/issues/?referrer=weekly_report¬ification_uuid={ctx['notification_uuid']}&project={self.project.id}", - "color": "#422C6E", + "color": "#7553FF", "accepted_error_count": 1, "accepted_transaction_count": 3, } assert ctx["trends"]["series"][-2][1][0] == { - "color": "#422C6E", + "color": "#7553FF", "error_count": 1, "transaction_count": 3, } @@ -1126,6 +1128,51 @@ def test_group_status_to_color_obj_correct_length(self) -> None: unique_enum_count = len(enum_values) assert len(group_status_to_color) == unique_enum_count + def test_key_errors_and_performance_issues_share_substatus_badges(self) -> None: + user = self.create_user() + self.create_member(teams=[self.team], user=user, organization=self.organization) + error_group = self.create_group( + project=self.project, + message="error message", + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.ONGOING, + data={ + "type": "error", + "metadata": {"type": "TypeError", "value": "error message"}, + }, + ) + performance_group = self.create_group( + project=self.project, + message="performance message", + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.ONGOING, + type=PerformanceNPlusOneGroupType.type_id, + data={ + "type": "transaction", + "metadata": {"title": "N+1 Query", "value": "performance message"}, + }, + ) + ctx = OrganizationReportContext(self.now.timestamp(), ONE_DAY * 7, self.organization) + project_context = ProjectContext(self.project) + project_context.key_errors_by_group = [(error_group, 10)] + project_context.key_performance_issues = [(performance_group, None, 10)] + ctx.projects_context_map = {self.project.id: project_context} + ctx.project_ownership[user.id] = {self.project.id} + + rendered_context = render_template_context(ctx, user.id) + + assert rendered_context is not None + key_error = rendered_context["key_errors"][0] + performance_issue = rendered_context["key_performance_issues"][0] + substatus_fields = ( + "group_substatus", + "group_substatus_color", + "group_substatus_text_color", + ) + assert {field: key_error[field] for field in substatus_fields} == { + field: performance_issue[field] for field in substatus_fields + } + @mock.patch("sentry.analytics.record") @mock.patch("sentry.tasks.summaries.weekly_reports.MessageBuilder") def test_email_override_simple(
+   +   +   +