test_group.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. from datetime import timedelta
  2. from unittest.mock import patch
  3. import pytz
  4. from django.utils import timezone
  5. from sentry.api.serializers import serialize
  6. from sentry.api.serializers.models.group import GroupSerializerSnuba
  7. from sentry.issues.grouptype import (
  8. PerformanceRenderBlockingAssetSpanGroupType,
  9. ProfileBlockedThreadGroupType,
  10. )
  11. from sentry.models import (
  12. Group,
  13. GroupEnvironment,
  14. GroupLink,
  15. GroupResolution,
  16. GroupSnooze,
  17. GroupStatus,
  18. GroupSubscription,
  19. NotificationSetting,
  20. UserOption,
  21. )
  22. from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes
  23. from sentry.testutils import APITestCase, SnubaTestCase
  24. from sentry.testutils.helpers.datetime import before_now, iso_format
  25. from sentry.testutils.performance_issues.store_transaction import PerfIssueTransactionTestMixin
  26. from sentry.testutils.silo import exempt_from_silo_limits, region_silo_test
  27. from sentry.types.integrations import ExternalProviders
  28. from tests.sentry.issues.test_utils import SearchIssueTestMixin
  29. @region_silo_test(stable=True)
  30. class GroupSerializerSnubaTest(APITestCase, SnubaTestCase):
  31. def setUp(self):
  32. super().setUp()
  33. self.min_ago = before_now(minutes=1)
  34. self.day_ago = before_now(days=1)
  35. self.week_ago = before_now(days=7)
  36. def test_permalink(self):
  37. group = self.create_group()
  38. result = serialize(group, self.user, serializer=GroupSerializerSnuba())
  39. assert "http://" in result["permalink"]
  40. assert f"{group.organization.slug}/issues/{group.id}" in result["permalink"]
  41. def test_permalink_outside_org(self):
  42. outside_user = self.create_user()
  43. group = self.create_group()
  44. result = serialize(group, outside_user, serializer=GroupSerializerSnuba())
  45. assert result["permalink"] is None
  46. def test_is_ignored_with_expired_snooze(self):
  47. now = timezone.now()
  48. user = self.create_user()
  49. group = self.create_group(status=GroupStatus.IGNORED)
  50. GroupSnooze.objects.create(group=group, until=now - timedelta(minutes=1))
  51. result = serialize(group, user, serializer=GroupSerializerSnuba())
  52. assert result["status"] == "unresolved"
  53. assert result["statusDetails"] == {}
  54. def test_is_ignored_with_valid_snooze(self):
  55. now = timezone.now()
  56. user = self.create_user()
  57. group = self.create_group(status=GroupStatus.IGNORED)
  58. snooze = GroupSnooze.objects.create(group=group, until=now + timedelta(minutes=1))
  59. result = serialize(group, user, serializer=GroupSerializerSnuba())
  60. assert result["status"] == "ignored"
  61. assert result["statusDetails"]["ignoreCount"] == snooze.count
  62. assert result["statusDetails"]["ignoreWindow"] == snooze.window
  63. assert result["statusDetails"]["ignoreUserCount"] == snooze.user_count
  64. assert result["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  65. assert result["statusDetails"]["ignoreUntil"] == snooze.until
  66. assert result["statusDetails"]["actor"] is None
  67. def test_is_ignored_with_valid_snooze_and_actor(self):
  68. now = timezone.now()
  69. user = self.create_user()
  70. group = self.create_group(status=GroupStatus.IGNORED)
  71. GroupSnooze.objects.create(group=group, until=now + timedelta(minutes=1), actor_id=user.id)
  72. result = serialize(group, user, serializer=GroupSerializerSnuba())
  73. assert result["status"] == "ignored"
  74. assert result["statusDetails"]["actor"]["id"] == str(user.id)
  75. def test_resolved_in_next_release(self):
  76. release = self.create_release(project=self.project, version="a")
  77. user = self.create_user()
  78. group = self.create_group(status=GroupStatus.RESOLVED)
  79. GroupResolution.objects.create(
  80. group=group, release=release, type=GroupResolution.Type.in_next_release
  81. )
  82. result = serialize(group, user, serializer=GroupSerializerSnuba())
  83. assert result["status"] == "resolved"
  84. assert result["statusDetails"] == {"inNextRelease": True, "actor": None}
  85. def test_resolved_in_release(self):
  86. release = self.create_release(project=self.project, version="a")
  87. user = self.create_user()
  88. group = self.create_group(status=GroupStatus.RESOLVED)
  89. GroupResolution.objects.create(
  90. group=group, release=release, type=GroupResolution.Type.in_release
  91. )
  92. result = serialize(group, user, serializer=GroupSerializerSnuba())
  93. assert result["status"] == "resolved"
  94. assert result["statusDetails"] == {"inRelease": "a", "actor": None}
  95. def test_resolved_with_actor(self):
  96. release = self.create_release(project=self.project, version="a")
  97. user = self.create_user()
  98. group = self.create_group(status=GroupStatus.RESOLVED)
  99. GroupResolution.objects.create(
  100. group=group, release=release, type=GroupResolution.Type.in_release, actor_id=user.id
  101. )
  102. result = serialize(group, user, serializer=GroupSerializerSnuba())
  103. assert result["status"] == "resolved"
  104. assert result["statusDetails"]["actor"]["id"] == str(user.id)
  105. def test_resolved_in_commit(self):
  106. repo = self.create_repo(project=self.project)
  107. commit = self.create_commit(repo=repo)
  108. user = self.create_user()
  109. group = self.create_group(status=GroupStatus.RESOLVED)
  110. GroupLink.objects.create(
  111. group_id=group.id,
  112. project_id=group.project_id,
  113. linked_id=commit.id,
  114. linked_type=GroupLink.LinkedType.commit,
  115. relationship=GroupLink.Relationship.resolves,
  116. )
  117. result = serialize(group, user, serializer=GroupSerializerSnuba())
  118. assert result["status"] == "resolved"
  119. assert result["statusDetails"]["inCommit"]["id"] == commit.key
  120. @patch("sentry.analytics.record")
  121. @patch("sentry.models.Group.is_over_resolve_age")
  122. def test_auto_resolved(self, mock_is_over_resolve_age, mock_record):
  123. mock_is_over_resolve_age.return_value = True
  124. user = self.create_user()
  125. group = self.create_group(status=GroupStatus.UNRESOLVED)
  126. result = serialize(group, user, serializer=GroupSerializerSnuba())
  127. assert result["status"] == "resolved"
  128. assert result["statusDetails"] == {"autoResolved": True}
  129. mock_record.assert_called_with(
  130. "issue.resolved",
  131. default_user_id=self.project.organization.get_default_owner().id,
  132. project_id=self.project.id,
  133. organization_id=self.project.organization_id,
  134. group_id=group.id,
  135. resolution_type="automatic",
  136. )
  137. def test_subscribed(self):
  138. user = self.create_user()
  139. group = self.create_group()
  140. GroupSubscription.objects.create(
  141. user_id=user.id, group=group, project=group.project, is_active=True
  142. )
  143. result = serialize(group, user, serializer=GroupSerializerSnuba())
  144. assert result["isSubscribed"]
  145. assert result["subscriptionDetails"] == {"reason": "unknown"}
  146. def test_explicit_unsubscribed(self):
  147. user = self.create_user()
  148. group = self.create_group()
  149. GroupSubscription.objects.create(
  150. user_id=user.id, group=group, project=group.project, is_active=False
  151. )
  152. result = serialize(group, user, serializer=GroupSerializerSnuba())
  153. assert not result["isSubscribed"]
  154. assert not result["subscriptionDetails"]
  155. def test_implicit_subscribed(self):
  156. user = self.create_user()
  157. group = self.create_group()
  158. combinations = (
  159. # (default, project, subscribed, has_details)
  160. (
  161. NotificationSettingOptionValues.ALWAYS,
  162. NotificationSettingOptionValues.DEFAULT,
  163. True,
  164. False,
  165. ),
  166. (
  167. NotificationSettingOptionValues.ALWAYS,
  168. NotificationSettingOptionValues.ALWAYS,
  169. True,
  170. False,
  171. ),
  172. (
  173. NotificationSettingOptionValues.ALWAYS,
  174. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  175. False,
  176. False,
  177. ),
  178. (
  179. NotificationSettingOptionValues.ALWAYS,
  180. NotificationSettingOptionValues.NEVER,
  181. False,
  182. True,
  183. ),
  184. (
  185. NotificationSettingOptionValues.DEFAULT,
  186. NotificationSettingOptionValues.DEFAULT,
  187. False,
  188. False,
  189. ),
  190. (
  191. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  192. NotificationSettingOptionValues.DEFAULT,
  193. False,
  194. False,
  195. ),
  196. (
  197. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  198. NotificationSettingOptionValues.ALWAYS,
  199. True,
  200. False,
  201. ),
  202. (
  203. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  204. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  205. False,
  206. False,
  207. ),
  208. (
  209. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  210. NotificationSettingOptionValues.NEVER,
  211. False,
  212. True,
  213. ),
  214. (
  215. NotificationSettingOptionValues.NEVER,
  216. NotificationSettingOptionValues.DEFAULT,
  217. False,
  218. True,
  219. ),
  220. (
  221. NotificationSettingOptionValues.NEVER,
  222. NotificationSettingOptionValues.ALWAYS,
  223. True,
  224. False,
  225. ),
  226. (
  227. NotificationSettingOptionValues.NEVER,
  228. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  229. False,
  230. False,
  231. ),
  232. (
  233. NotificationSettingOptionValues.NEVER,
  234. NotificationSettingOptionValues.NEVER,
  235. False,
  236. True,
  237. ),
  238. )
  239. for default_value, project_value, is_subscribed, has_details in combinations:
  240. with exempt_from_silo_limits():
  241. UserOption.objects.clear_local_cache()
  242. NotificationSetting.objects.update_settings(
  243. ExternalProviders.EMAIL,
  244. NotificationSettingTypes.WORKFLOW,
  245. default_value,
  246. user=user,
  247. )
  248. NotificationSetting.objects.update_settings(
  249. ExternalProviders.EMAIL,
  250. NotificationSettingTypes.WORKFLOW,
  251. project_value,
  252. user=user,
  253. project=group.project,
  254. )
  255. NotificationSetting.objects.update_settings(
  256. ExternalProviders.SLACK,
  257. NotificationSettingTypes.WORKFLOW,
  258. default_value,
  259. user=user,
  260. )
  261. NotificationSetting.objects.update_settings(
  262. ExternalProviders.SLACK,
  263. NotificationSettingTypes.WORKFLOW,
  264. project_value,
  265. user=user,
  266. project=group.project,
  267. )
  268. result = serialize(group, user, serializer=GroupSerializerSnuba())
  269. subscription_details = result.get("subscriptionDetails")
  270. assert result["isSubscribed"] is is_subscribed
  271. assert (
  272. subscription_details == {"disabled": True}
  273. if has_details
  274. else subscription_details is None
  275. )
  276. def test_global_no_conversations_overrides_group_subscription(self):
  277. user = self.create_user()
  278. group = self.create_group()
  279. GroupSubscription.objects.create(
  280. user_id=user.id, group=group, project=group.project, is_active=True
  281. )
  282. with exempt_from_silo_limits():
  283. for provider in [ExternalProviders.EMAIL, ExternalProviders.SLACK]:
  284. NotificationSetting.objects.update_settings(
  285. provider,
  286. NotificationSettingTypes.WORKFLOW,
  287. NotificationSettingOptionValues.NEVER,
  288. user=user,
  289. )
  290. result = serialize(group, user, serializer=GroupSerializerSnuba())
  291. assert not result["isSubscribed"]
  292. assert result["subscriptionDetails"] == {"disabled": True}
  293. def test_project_no_conversations_overrides_group_subscription(self):
  294. user = self.create_user()
  295. group = self.create_group()
  296. GroupSubscription.objects.create(
  297. user_id=user.id, group=group, project=group.project, is_active=True
  298. )
  299. for provider in [ExternalProviders.EMAIL, ExternalProviders.SLACK]:
  300. with exempt_from_silo_limits():
  301. NotificationSetting.objects.update_settings(
  302. provider,
  303. NotificationSettingTypes.WORKFLOW,
  304. NotificationSettingOptionValues.NEVER,
  305. user=user,
  306. project=group.project,
  307. )
  308. result = serialize(group, user, serializer=GroupSerializerSnuba())
  309. assert not result["isSubscribed"]
  310. assert result["subscriptionDetails"] == {"disabled": True}
  311. def test_no_user_unsubscribed(self):
  312. group = self.create_group()
  313. result = serialize(group, serializer=GroupSerializerSnuba())
  314. assert not result["isSubscribed"]
  315. def test_seen_stats(self):
  316. environment = self.create_environment(project=self.project)
  317. environment2 = self.create_environment(project=self.project)
  318. events = []
  319. for event_id, env, user_id, timestamp in [
  320. ("a" * 32, environment, 1, iso_format(self.min_ago)),
  321. ("b" * 32, environment, 2, iso_format(self.min_ago)),
  322. ("c" * 32, environment2, 3, iso_format(self.week_ago)),
  323. ]:
  324. events.append(
  325. self.store_event(
  326. data={
  327. "event_id": event_id,
  328. "fingerprint": ["put-me-in-group1"],
  329. "timestamp": timestamp,
  330. "environment": env.name,
  331. "user": {"id": user_id},
  332. },
  333. project_id=self.project.id,
  334. )
  335. )
  336. # Assert all events are in the same group
  337. (group_id,) = {e.group.id for e in events}
  338. group = Group.objects.get(id=group_id)
  339. group.times_seen = 3
  340. group.first_seen = self.week_ago - timedelta(days=5)
  341. group.last_seen = self.week_ago
  342. group.save()
  343. # should use group columns when no environments arg passed
  344. result = serialize(group, serializer=GroupSerializerSnuba(environment_ids=[]))
  345. assert result["count"] == "3"
  346. assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
  347. assert result["firstSeen"] == group.first_seen
  348. # update this to something different to make sure it's being used
  349. group_env = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment.id)
  350. group_env.first_seen = self.day_ago - timedelta(days=3)
  351. group_env.save()
  352. group_env2 = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment2.id)
  353. result = serialize(
  354. group,
  355. serializer=GroupSerializerSnuba(environment_ids=[environment.id, environment2.id]),
  356. )
  357. assert result["count"] == "3"
  358. # result is rounded down to nearest second
  359. assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
  360. assert iso_format(result["firstSeen"]) == iso_format(group_env.first_seen)
  361. assert iso_format(group_env2.first_seen) > iso_format(group_env.first_seen)
  362. assert result["userCount"] == 3
  363. result = serialize(
  364. group,
  365. serializer=GroupSerializerSnuba(
  366. environment_ids=[environment.id, environment2.id],
  367. start=self.week_ago - timedelta(hours=1),
  368. end=self.week_ago + timedelta(hours=1),
  369. ),
  370. )
  371. assert result["userCount"] == 1
  372. assert iso_format(result["lastSeen"]) == iso_format(self.week_ago)
  373. assert iso_format(result["firstSeen"]) == iso_format(self.week_ago)
  374. assert result["count"] == "1"
  375. def test_get_start_from_seen_stats(self):
  376. for days, expected in [(None, 30), (0, 14), (1000, 90)]:
  377. last_seen = None if days is None else before_now(days=days).replace(tzinfo=pytz.UTC)
  378. start = GroupSerializerSnuba._get_start_from_seen_stats({"": {"last_seen": last_seen}})
  379. assert iso_format(start) == iso_format(before_now(days=expected))
  380. @region_silo_test
  381. class PerformanceGroupSerializerSnubaTest(
  382. APITestCase,
  383. SnubaTestCase,
  384. PerfIssueTransactionTestMixin,
  385. ):
  386. def test_perf_seen_stats(self):
  387. proj = self.create_project()
  388. environment = self.create_environment(project=proj)
  389. first_group_fingerprint = f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group1"
  390. timestamp = timezone.now() - timedelta(days=5)
  391. times = 5
  392. for _ in range(0, times):
  393. self.store_transaction(
  394. proj.id,
  395. "user1",
  396. [first_group_fingerprint],
  397. environment.name,
  398. timestamp=timestamp + timedelta(minutes=1),
  399. )
  400. event = self.store_transaction(
  401. proj.id,
  402. "user2",
  403. [first_group_fingerprint],
  404. environment.name,
  405. timestamp=timestamp + timedelta(minutes=2),
  406. )
  407. first_group = event.groups[0]
  408. result = serialize(
  409. first_group,
  410. serializer=GroupSerializerSnuba(
  411. environment_ids=[environment.id],
  412. start=timestamp - timedelta(hours=1),
  413. end=timestamp + timedelta(hours=1),
  414. ),
  415. )
  416. assert result["userCount"] == 2
  417. assert iso_format(result["lastSeen"]) == iso_format(timestamp + timedelta(minutes=2))
  418. assert iso_format(result["firstSeen"]) == iso_format(timestamp + timedelta(minutes=1))
  419. assert result["count"] == str(times + 1)
  420. @region_silo_test
  421. class ProfilingGroupSerializerSnubaTest(
  422. APITestCase,
  423. SnubaTestCase,
  424. SearchIssueTestMixin,
  425. ):
  426. def test_profiling_seen_stats(self):
  427. proj = self.create_project()
  428. environment = self.create_environment(project=proj)
  429. first_group_fingerprint = f"{ProfileBlockedThreadGroupType.type_id}-group1"
  430. timestamp = timezone.now().replace(hour=0, minute=0, second=0)
  431. times = 5
  432. for incr in range(0, times):
  433. # for user_0 - user_4, first_group
  434. self.store_search_issue(
  435. proj.id,
  436. incr,
  437. [first_group_fingerprint],
  438. environment.name,
  439. timestamp + timedelta(minutes=incr),
  440. )
  441. # user_5, another_group
  442. event, issue_occurrence, group_info = self.store_search_issue(
  443. proj.id,
  444. 5,
  445. [first_group_fingerprint],
  446. environment.name,
  447. timestamp + timedelta(minutes=5),
  448. )
  449. first_group = group_info.group
  450. result = serialize(
  451. first_group,
  452. serializer=GroupSerializerSnuba(
  453. environment_ids=[environment.id],
  454. start=timestamp - timedelta(days=1),
  455. end=timestamp + timedelta(days=1),
  456. ),
  457. )
  458. assert result["userCount"] == 6
  459. assert iso_format(result["lastSeen"]) == iso_format(timestamp + timedelta(minutes=5))
  460. assert iso_format(result["firstSeen"]) == iso_format(timestamp)
  461. assert result["count"] == str(times + 1)