diff --git a/src/sentry/analytics/events/__init__.py b/src/sentry/analytics/events/__init__.py index d2f92d36245a3b..b059eb52c36966 100644 --- a/src/sentry/analytics/events/__init__.py +++ b/src/sentry/analytics/events/__init__.py @@ -77,3 +77,4 @@ from .user_created import * # noqa: F401,F403 from .user_signup import * # noqa: F401,F403 from .webhook_repository_created import * # noqa: F401,F403 +from .weekly_report import * # noqa: F401,F403 diff --git a/src/sentry/analytics/events/weekly_report.py b/src/sentry/analytics/events/weekly_report.py new file mode 100644 index 00000000000000..6f0d28b53edbc0 --- /dev/null +++ b/src/sentry/analytics/events/weekly_report.py @@ -0,0 +1,15 @@ +from sentry import analytics + + +class WeeklyReportSent(analytics.Event): + type = "weekly_report.sent" + + attributes = ( + analytics.Attribute("organization_id"), + analytics.Attribute("user_id"), + analytics.Attribute("notification_uuid"), + analytics.Attribute("user_project_count", type=int), + ) + + +analytics.register(WeeklyReportSent) diff --git a/src/sentry/notifications/utils/__init__.py b/src/sentry/notifications/utils/__init__.py index dcc6479bb02a31..4de0f69f4e873e 100644 --- a/src/sentry/notifications/utils/__init__.py +++ b/src/sentry/notifications/utils/__init__.py @@ -1,6 +1,8 @@ from __future__ import annotations import logging +import random +import string import time from collections import defaultdict from dataclasses import dataclass @@ -441,6 +443,13 @@ def get_replay_id(event: Event | GroupEvent) -> str | None: return replay_id +def generate_notification_uuid() -> str: + """ + Generates a random string of 16 characters to be used as a notification uuid + """ + return "".join(random.choices(string.ascii_letters + string.digits, k=16)) + + @dataclass class PerformanceProblemContext: problem: PerformanceProblem diff --git a/src/sentry/tasks/weekly_reports.py b/src/sentry/tasks/weekly_reports.py index 60cce8100b4ab9..822f5662557067 100644 --- a/src/sentry/tasks/weekly_reports.py +++ b/src/sentry/tasks/weekly_reports.py @@ -19,7 +19,7 @@ from snuba_sdk.orderby import Direction, OrderBy from snuba_sdk.query import Limit, Query -from sentry import features +from sentry import analytics, features from sentry.api.serializers.snuba import zerofill from sentry.constants import DataCategory from sentry.models import ( @@ -32,6 +32,7 @@ OrganizationMember, OrganizationStatus, ) +from sentry.notifications.utils import generate_notification_uuid from sentry.services.hybrid_cloud.user_option import user_option_service from sentry.silo import SiloMode from sentry.snuba.dataset import Dataset @@ -739,6 +740,8 @@ def render_template_context(ctx, user_id): "organizations:session-replay", ctx.organization ) and features.has("organizations:session-replay-weekly-email", ctx.organization) + notification_uuid = generate_notification_uuid() + # 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(): @@ -793,7 +796,9 @@ def sum_event_counts(project_ctxs): legend = [ { "slug": project_ctx.project.slug, - "url": project_ctx.project.get_absolute_url(), + "url": project_ctx.project.get_absolute_url( + params={"referrer": "weekly_report", "notification_uuid": notification_uuid} + ), "color": project_breakdown_colors[i], "dropped_error_count": project_ctx.dropped_error_count, "accepted_error_count": project_ctx.accepted_error_count, @@ -1031,6 +1036,8 @@ def issue_summary(): "key_performance_issues": key_performance_issues(), "key_replays": key_replays() if has_replay_section else [], "issue_summary": issue_summary(), + "user_project_count": len(user_projects), + "notification_uuid": notification_uuid, } @@ -1052,6 +1059,14 @@ def send_email(ctx, user_id, dry_run=False, email_override=None): ) if dry_run: return + else: + analytics.record( + "weekly_report.sent", + user_id=user_id, + organization_id=ctx.organization.id, + notification_uuid=template_ctx["notification_uuid"], + user_project_count=template_ctx["user_project_count"], + ) if email_override: message.send(to=(email_override,)) else: diff --git a/src/sentry/templates/sentry/emails/reports/body.html b/src/sentry/templates/sentry/emails/reports/body.html index d77bf66fa2a87e..864e779d927ef4 100644 --- a/src/sentry/templates/sentry/emails/reports/body.html +++ b/src/sentry/templates/sentry/emails/reports/body.html @@ -233,7 +233,8 @@

Total Project Errors

{{ trends.total_error_count|small_count:1 }}

{% url 'sentry-organization-issue-list' organization.slug as issue_list %} - View All Errors @@ -274,7 +275,8 @@

{{ trends.total_error_count|small_cou

Total Project Transactions

{{ trends.total_transaction_count|small_count:1 }}

{% url 'sentry-organization-perfomance' organization.slug as performance_landing %} - View All Transactions
@@ -310,7 +312,8 @@

{{ trends.total_transaction_count|sma

Something slow?

Trace those 10-second page loads to poor-performing API calls.

{% url 'sentry-organization-performance' organization.slug as performance_landing %} - Set Up Performance @@ -321,7 +324,8 @@

Something slow?

Total Project Replays

{{ trends.total_replay_count|small_count:1 }}

{% url 'sentry-organization-replays' organization.slug as replay_landing %} - View All Replays
@@ -360,7 +364,8 @@

{{ trends.total_replay_count|small_co

Tricky bug?

Rewind and replay every step of a user’s journey before and after they encounter an issue.

{% url 'sentry-organization-replays' organization.slug as replay_landing %} - Set Up Session Replay @@ -479,8 +484,9 @@

Issues with the most 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}} + href="{% org_url organization issue_detail query=query %}">{{a.group.message}}
{{a.group.project.name}}
{% if a.group_substatus and a.group_substatus_color %} @@ -500,8 +506,9 @@

Most frequent 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}} + href="{% org_url organization issue_detail query=query %}">{{a.group.message}}
{{a.group.get_type_display}}
{{a.status}} @@ -516,8 +523,8 @@

Most frequent transactions

{{a.count|small_count:1}}
- {% querystring project=a.project.id transaction=a.name referrer="weekly_report" as query %} {% url 'sentry-organization-performance-summary' organization.slug as performance_summary %} + {% querystring project=a.project.id transaction=a.name referrer="weekly_report" notification_uuid=notification_uuid as query %} {{a.name}}
{{a.project.name}}
@@ -541,8 +548,8 @@

Most erroneous replays

{{a.count|small_count:1}}
- {% querystring referrer="weekly_report" as query %} {% url 'sentry-organization-replay-details' organization.slug a.replay.id as replay_details %} + {% querystring referrer="weekly_report" notification_uuid=notification_uuid as query %} {{a.id}}
{{a.project.name}}
diff --git a/tests/sentry/tasks/test_weekly_reports.py b/tests/sentry/tasks/test_weekly_reports.py index 42de4d05b2a072..71d883ead7ec4d 100644 --- a/tests/sentry/tasks/test_weekly_reports.py +++ b/tests/sentry/tasks/test_weekly_reports.py @@ -39,7 +39,6 @@ @region_silo_test(stable=True) class WeeklyReportsTest(OutcomesSnubaTest, SnubaTestCase): - @with_feature("organizations:weekly-email-refresh") @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) def test_integration(self): with unguarded_write(using=router.db_for_write(Project)): @@ -89,7 +88,6 @@ def test_with_empty_string_user_option(self): assert self.organization.name in message.subject @with_feature("organizations:customer-domains") - @with_feature("organizations:weekly-email-refresh") @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0)) def test_message_links_customer_domains(self): with unguarded_write(using=router.db_for_write(Project)): @@ -114,9 +112,10 @@ def test_message_links_customer_domains(self): assert isinstance(message, EmailMultiAlternatives) assert self.organization.name in message.subject html = message.alternatives[0][0] + assert isinstance(html, str) assert ( - f"http://{self.organization.slug}.testserver/issues/?referrer=weekly-email" in html + f"http://{self.organization.slug}.testserver/issues/?referrer=weekly_report" in html ) @mock.patch("sentry.tasks.weekly_reports.send_email") @@ -249,16 +248,16 @@ def test_organization_project_issue_substatus_summaries(self): assert project_ctx.regression_substatus_count == 0 assert project_ctx.total_substatus_count == 2 + @mock.patch("sentry.analytics.record") @mock.patch("sentry.tasks.weekly_reports.MessageBuilder") - def test_message_builder_simple(self, message_builder): + def test_message_builder_simple(self, message_builder, record): now = django_timezone.now() two_days_ago = now - timedelta(days=2) three_days_ago = now - timedelta(days=3) - self.create_member( - teams=[self.team], user=self.create_user(), organization=self.organization - ) + user = self.create_user() + self.create_member(teams=[self.team], user=user, organization=self.organization) event1 = self.store_event( data={ @@ -345,6 +344,16 @@ def test_message_builder_simple(self, message_builder): assert context["trends"]["total_transaction_count"] == 10 assert "Weekly Report for" in message_params["subject"] + assert isinstance(context["notification_uuid"], str) + + record.assert_called_with( + "weekly_report.sent", + user_id=user.id, + organization_id=self.organization.id, + notification_uuid=mock.ANY, + user_project_count=1, + ) + @mock.patch("sentry.tasks.weekly_reports.MessageBuilder") @with_feature("organizations:escalating-issues") def test_message_builder_substatus_simple(self, message_builder): @@ -459,7 +468,7 @@ def test_message_builder_advanced(self, message_builder): assert ctx["trends"]["legend"][0] == { "slug": "bar", - "url": f"http://testserver/organizations/baz/issues/?project={self.project.id}", + "url": f"http://testserver/organizations/baz/issues/?referrer=weekly_report¬ification_uuid={ctx['notification_uuid']}&project={self.project.id}", "color": "#422C6E", "dropped_error_count": 2, "accepted_error_count": 1, @@ -498,7 +507,7 @@ def test_empty_report(self, mock_send_email): assert mock_send_email.call_count == 0 @with_feature("organizations:session-replay") - @with_feature("organizations:session-replay-weekly-email") + @with_feature("organizations:session-replay-weekly_report") @mock.patch("sentry.tasks.weekly_reports.MessageBuilder") def test_message_builder_replays(self, message_builder): @@ -529,7 +538,7 @@ def test_message_builder_replays(self, message_builder): assert ctx["trends"]["legend"][0] == { "slug": "bar", - "url": f"http://testserver/organizations/baz/issues/?project={self.project.id}", + "url": f"http://testserver/organizations/baz/issues/?referrer=weekly_report¬ification_uuid={ctx['notification_uuid']}&project={self.project.id}", "color": "#422C6E", "dropped_error_count": 0, "accepted_error_count": 0,