test_weekly_reports.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import copy
  2. import functools
  3. from datetime import datetime, timedelta
  4. from unittest import mock
  5. import pytz
  6. from django.core import mail
  7. from django.db.models import F
  8. from django.utils import timezone
  9. from freezegun import freeze_time
  10. from sentry.constants import DataCategory
  11. from sentry.models import GroupStatus, OrganizationMember, Project, UserOption
  12. from sentry.tasks.weekly_reports import (
  13. ONE_DAY,
  14. OrganizationReportContext,
  15. deliver_reports,
  16. organization_project_issue_summaries,
  17. prepare_organization_report,
  18. schedule_organizations,
  19. )
  20. from sentry.testutils.cases import OutcomesSnubaTest, SnubaTestCase
  21. from sentry.testutils.factories import DEFAULT_EVENT_DATA
  22. from sentry.testutils.helpers import with_feature
  23. from sentry.testutils.helpers.datetime import before_now, iso_format
  24. from sentry.utils.dates import floor_to_utc_day, to_timestamp
  25. from sentry.utils.outcomes import Outcome
  26. DISABLED_ORGANIZATIONS_USER_OPTION_KEY = "reports:disabled-organizations"
  27. class WeeklyReportsTest(OutcomesSnubaTest, SnubaTestCase):
  28. @with_feature("organizations:weekly-email-refresh")
  29. @freeze_time(before_now(days=2).replace(hour=0, minute=0, second=0, microsecond=0))
  30. def test_integration(self):
  31. Project.objects.all().delete()
  32. now = datetime.now().replace(tzinfo=pytz.utc)
  33. project = self.create_project(
  34. organization=self.organization, teams=[self.team], date_added=now - timedelta(days=90)
  35. )
  36. self.store_event(
  37. data={
  38. "timestamp": iso_format(before_now(days=1)),
  39. },
  40. project_id=project.id,
  41. )
  42. member_set = set(project.teams.first().member_set.all())
  43. with self.tasks():
  44. schedule_organizations(timestamp=to_timestamp(now))
  45. assert len(mail.outbox) == len(member_set) == 1
  46. message = mail.outbox[0]
  47. assert self.organization.name in message.subject
  48. @mock.patch("sentry.tasks.weekly_reports.send_email")
  49. def test_deliver_reports_respects_settings(self, mock_send_email):
  50. user = self.user
  51. organization = self.organization
  52. ctx = OrganizationReportContext(0, 0, organization)
  53. set_option_value = functools.partial(
  54. UserOption.objects.set_value, user, DISABLED_ORGANIZATIONS_USER_OPTION_KEY
  55. )
  56. # disabled
  57. set_option_value([organization.id])
  58. deliver_reports(ctx)
  59. assert mock_send_email.call_count == 0
  60. # enabled
  61. set_option_value([])
  62. deliver_reports(ctx)
  63. mock_send_email.assert_called_once_with(ctx, user, dry_run=False)
  64. @mock.patch("sentry.tasks.weekly_reports.send_email")
  65. def test_member_disabled(self, mock_send_email):
  66. ctx = OrganizationReportContext(0, 0, self.organization)
  67. OrganizationMember.objects.filter(user=self.user).update(
  68. flags=F("flags").bitor(OrganizationMember.flags["member-limit:restricted"])
  69. )
  70. # disabled
  71. deliver_reports(ctx)
  72. assert mock_send_email.call_count == 0
  73. @mock.patch("sentry.tasks.weekly_reports.send_email")
  74. def test_user_inactive(self, mock_send_email):
  75. ctx = OrganizationReportContext(0, 0, self.organization)
  76. self.user.update(is_active=False)
  77. # disabled
  78. deliver_reports(ctx)
  79. assert mock_send_email.call_count == 0
  80. def test_organization_project_issue_summaries(self):
  81. self.login_as(user=self.user)
  82. now = timezone.now()
  83. min_ago = iso_format(now - timedelta(minutes=1))
  84. self.store_event(
  85. data={
  86. "event_id": "a" * 32,
  87. "message": "message",
  88. "timestamp": min_ago,
  89. "stacktrace": copy.deepcopy(DEFAULT_EVENT_DATA["stacktrace"]),
  90. "fingerprint": ["group-1"],
  91. },
  92. project_id=self.project.id,
  93. )
  94. self.store_event(
  95. data={
  96. "event_id": "b" * 32,
  97. "message": "message",
  98. "timestamp": min_ago,
  99. "stacktrace": copy.deepcopy(DEFAULT_EVENT_DATA["stacktrace"]),
  100. "fingerprint": ["group-2"],
  101. },
  102. project_id=self.project.id,
  103. )
  104. timestamp = to_timestamp(now)
  105. ctx = OrganizationReportContext(timestamp, ONE_DAY * 7, self.organization)
  106. organization_project_issue_summaries(ctx)
  107. project_ctx = ctx.projects[self.project.id]
  108. assert project_ctx.reopened_issue_count == 0
  109. assert project_ctx.new_issue_count == 2
  110. assert project_ctx.existing_issue_count == 0
  111. assert project_ctx.all_issue_count == 2
  112. @mock.patch("sentry.tasks.weekly_reports.MessageBuilder")
  113. def test_message_builder_simple(self, message_builder):
  114. now = timezone.now()
  115. two_days_ago = now - timedelta(days=2)
  116. three_days_ago = now - timedelta(days=3)
  117. self.create_member(
  118. teams=[self.team], user=self.create_user(), organization=self.organization
  119. )
  120. event1 = self.store_event(
  121. data={
  122. "event_id": "a" * 32,
  123. "message": "message",
  124. "timestamp": iso_format(three_days_ago),
  125. "stacktrace": copy.deepcopy(DEFAULT_EVENT_DATA["stacktrace"]),
  126. "fingerprint": ["group-1"],
  127. },
  128. project_id=self.project.id,
  129. )
  130. event2 = self.store_event(
  131. data={
  132. "event_id": "b" * 32,
  133. "message": "message",
  134. "timestamp": iso_format(three_days_ago),
  135. "stacktrace": copy.deepcopy(DEFAULT_EVENT_DATA["stacktrace"]),
  136. "fingerprint": ["group-2"],
  137. },
  138. project_id=self.project.id,
  139. )
  140. self.store_outcomes(
  141. {
  142. "org_id": self.organization.id,
  143. "project_id": self.project.id,
  144. "outcome": Outcome.ACCEPTED,
  145. "category": DataCategory.ERROR,
  146. "timestamp": three_days_ago,
  147. "key_id": 1,
  148. },
  149. num_times=2,
  150. )
  151. self.store_outcomes(
  152. {
  153. "org_id": self.organization.id,
  154. "project_id": self.project.id,
  155. "outcome": Outcome.ACCEPTED,
  156. "category": DataCategory.TRANSACTION,
  157. "timestamp": three_days_ago,
  158. "key_id": 1,
  159. },
  160. num_times=10,
  161. )
  162. group1 = event1.group
  163. group2 = event2.group
  164. group1.status = GroupStatus.RESOLVED
  165. group1.resolved_at = two_days_ago
  166. group1.save()
  167. group2.status = GroupStatus.RESOLVED
  168. group2.resolved_at = two_days_ago
  169. group2.save()
  170. prepare_organization_report(to_timestamp(now), ONE_DAY * 7, self.organization.id)
  171. for call_args in message_builder.call_args_list:
  172. message_params = call_args.kwargs
  173. context = message_params["context"]
  174. assert message_params["template"] == "sentry/emails/reports/body.txt"
  175. assert message_params["html_template"] == "sentry/emails/reports/body.html"
  176. assert context["organization"] == self.organization
  177. assert context["issue_summary"] == {
  178. "all_issue_count": 2,
  179. "existing_issue_count": 0,
  180. "new_issue_count": 2,
  181. "reopened_issue_count": 0,
  182. }
  183. assert len(context["key_errors"]) == 2
  184. assert context["trends"]["total_error_count"] == 2
  185. assert context["trends"]["total_transaction_count"] == 10
  186. assert "Weekly Report for" in message_params["subject"]
  187. @mock.patch("sentry.tasks.weekly_reports.MessageBuilder")
  188. def test_message_builder_advanced(self, message_builder):
  189. now = timezone.now()
  190. two_days_ago = now - timedelta(days=2)
  191. three_days_ago = now - timedelta(days=3)
  192. timestamp = to_timestamp(floor_to_utc_day(now))
  193. for outcome, category, num in [
  194. (Outcome.ACCEPTED, DataCategory.ERROR, 1),
  195. (Outcome.RATE_LIMITED, DataCategory.ERROR, 2),
  196. (Outcome.ACCEPTED, DataCategory.TRANSACTION, 3),
  197. (Outcome.RATE_LIMITED, DataCategory.TRANSACTION, 4),
  198. # Filtered should be ignored in these emails
  199. (Outcome.FILTERED, DataCategory.TRANSACTION, 5),
  200. ]:
  201. self.store_outcomes(
  202. {
  203. "org_id": self.organization.id,
  204. "project_id": self.project.id,
  205. "outcome": outcome,
  206. "category": category,
  207. "timestamp": two_days_ago,
  208. "key_id": 1,
  209. },
  210. num_times=num,
  211. )
  212. event1 = self.store_event(
  213. data={
  214. "event_id": "a" * 32,
  215. "message": "message",
  216. "timestamp": iso_format(three_days_ago),
  217. "stacktrace": copy.deepcopy(DEFAULT_EVENT_DATA["stacktrace"]),
  218. "fingerprint": ["group-1"],
  219. },
  220. project_id=self.project.id,
  221. )
  222. group1 = event1.group
  223. group1.status = GroupStatus.RESOLVED
  224. group1.resolved_at = two_days_ago
  225. group1.save()
  226. prepare_organization_report(timestamp, ONE_DAY * 7, self.organization.id)
  227. message_params = message_builder.call_args.kwargs
  228. ctx = message_params["context"]
  229. assert ctx["trends"]["legend"][0] == {
  230. "slug": "bar",
  231. "url": f"http://testserver/organizations/baz/issues/?project={self.project.id}",
  232. "color": "#422C6E",
  233. "dropped_error_count": 2,
  234. "accepted_error_count": 1,
  235. "dropped_transaction_count": 9,
  236. "accepted_transaction_count": 3,
  237. }
  238. assert ctx["trends"]["series"][-2][1][0] == {
  239. "color": "#422C6E",
  240. "error_count": 1,
  241. "transaction_count": 3,
  242. }
  243. @mock.patch("sentry.tasks.weekly_reports.send_email")
  244. def test_empty_report(self, mock_send_email):
  245. now = timezone.now()
  246. # date is out of range
  247. ten_days_ago = now - timedelta(days=10)
  248. self.store_event(
  249. data={
  250. "event_id": "a" * 32,
  251. "message": "message",
  252. "timestamp": iso_format(ten_days_ago),
  253. "stacktrace": copy.deepcopy(DEFAULT_EVENT_DATA["stacktrace"]),
  254. "fingerprint": ["group-1"],
  255. },
  256. project_id=self.project.id,
  257. )
  258. prepare_organization_report(to_timestamp(now), ONE_DAY * 7, self.organization.id)
  259. assert mock_send_email.call_count == 0