test_group.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import
  3. from sentry.utils.compat import mock
  4. import six
  5. from datetime import timedelta
  6. from django.utils import timezone
  7. from sentry.utils.compat.mock import patch
  8. from sentry.api.serializers import serialize
  9. from sentry.api.serializers.models.group import (
  10. GroupSerializerSnuba,
  11. StreamGroupSerializerSnuba,
  12. snuba_tsdb,
  13. )
  14. from sentry.models import (
  15. Group,
  16. Environment,
  17. GroupEnvironment,
  18. GroupLink,
  19. GroupResolution,
  20. GroupSnooze,
  21. GroupStatus,
  22. GroupSubscription,
  23. UserOption,
  24. UserOptionValue,
  25. )
  26. from sentry.testutils import APITestCase, SnubaTestCase
  27. from sentry.testutils.helpers.datetime import iso_format, before_now
  28. class GroupSerializerSnubaTest(APITestCase, SnubaTestCase):
  29. def setUp(self):
  30. super(GroupSerializerSnubaTest, self).setUp()
  31. self.min_ago = before_now(minutes=1)
  32. self.day_ago = before_now(days=1)
  33. self.week_ago = before_now(days=7)
  34. def test_permalink(self):
  35. group = self.create_group()
  36. result = serialize(group, self.user, serializer=GroupSerializerSnuba())
  37. assert "http://" in result["permalink"]
  38. assert "{}/issues/{}".format(group.organization.slug, group.id) in result["permalink"]
  39. def test_permalink_outside_org(self):
  40. outside_user = self.create_user()
  41. group = self.create_group()
  42. result = serialize(group, outside_user, serializer=GroupSerializerSnuba())
  43. assert result["permalink"] is None
  44. def test_is_ignored_with_expired_snooze(self):
  45. now = timezone.now()
  46. user = self.create_user()
  47. group = self.create_group(status=GroupStatus.IGNORED)
  48. GroupSnooze.objects.create(group=group, until=now - timedelta(minutes=1))
  49. result = serialize(group, user, serializer=GroupSerializerSnuba())
  50. assert result["status"] == "unresolved"
  51. assert result["statusDetails"] == {}
  52. def test_is_ignored_with_valid_snooze(self):
  53. now = timezone.now()
  54. user = self.create_user()
  55. group = self.create_group(status=GroupStatus.IGNORED)
  56. snooze = GroupSnooze.objects.create(group=group, until=now + timedelta(minutes=1))
  57. result = serialize(group, user, serializer=GroupSerializerSnuba())
  58. assert result["status"] == "ignored"
  59. assert result["statusDetails"]["ignoreCount"] == snooze.count
  60. assert result["statusDetails"]["ignoreWindow"] == snooze.window
  61. assert result["statusDetails"]["ignoreUserCount"] == snooze.user_count
  62. assert result["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  63. assert result["statusDetails"]["ignoreUntil"] == snooze.until
  64. assert result["statusDetails"]["actor"] is None
  65. def test_is_ignored_with_valid_snooze_and_actor(self):
  66. now = timezone.now()
  67. user = self.create_user()
  68. group = self.create_group(status=GroupStatus.IGNORED)
  69. GroupSnooze.objects.create(group=group, until=now + timedelta(minutes=1), actor_id=user.id)
  70. result = serialize(group, user, serializer=GroupSerializerSnuba())
  71. assert result["status"] == "ignored"
  72. assert result["statusDetails"]["actor"]["id"] == six.text_type(user.id)
  73. def test_resolved_in_next_release(self):
  74. release = self.create_release(project=self.project, version="a")
  75. user = self.create_user()
  76. group = self.create_group(status=GroupStatus.RESOLVED)
  77. GroupResolution.objects.create(
  78. group=group, release=release, type=GroupResolution.Type.in_next_release
  79. )
  80. result = serialize(group, user, serializer=GroupSerializerSnuba())
  81. assert result["status"] == "resolved"
  82. assert result["statusDetails"] == {"inNextRelease": True, "actor": None}
  83. def test_resolved_in_release(self):
  84. release = self.create_release(project=self.project, version="a")
  85. user = self.create_user()
  86. group = self.create_group(status=GroupStatus.RESOLVED)
  87. GroupResolution.objects.create(
  88. group=group, release=release, type=GroupResolution.Type.in_release
  89. )
  90. result = serialize(group, user, serializer=GroupSerializerSnuba())
  91. assert result["status"] == "resolved"
  92. assert result["statusDetails"] == {"inRelease": "a", "actor": None}
  93. def test_resolved_with_actor(self):
  94. release = self.create_release(project=self.project, version="a")
  95. user = self.create_user()
  96. group = self.create_group(status=GroupStatus.RESOLVED)
  97. GroupResolution.objects.create(
  98. group=group, release=release, type=GroupResolution.Type.in_release, actor_id=user.id
  99. )
  100. result = serialize(group, user, serializer=GroupSerializerSnuba())
  101. assert result["status"] == "resolved"
  102. assert result["statusDetails"]["actor"]["id"] == six.text_type(user.id)
  103. def test_resolved_in_commit(self):
  104. repo = self.create_repo(project=self.project)
  105. commit = self.create_commit(repo=repo)
  106. user = self.create_user()
  107. group = self.create_group(status=GroupStatus.RESOLVED)
  108. GroupLink.objects.create(
  109. group_id=group.id,
  110. project_id=group.project_id,
  111. linked_id=commit.id,
  112. linked_type=GroupLink.LinkedType.commit,
  113. relationship=GroupLink.Relationship.resolves,
  114. )
  115. result = serialize(group, user, serializer=GroupSerializerSnuba())
  116. assert result["status"] == "resolved"
  117. assert result["statusDetails"]["inCommit"]["id"] == commit.key
  118. @patch("sentry.models.Group.is_over_resolve_age")
  119. def test_auto_resolved(self, mock_is_over_resolve_age):
  120. mock_is_over_resolve_age.return_value = True
  121. user = self.create_user()
  122. group = self.create_group(status=GroupStatus.UNRESOLVED)
  123. result = serialize(group, user, serializer=GroupSerializerSnuba())
  124. assert result["status"] == "resolved"
  125. assert result["statusDetails"] == {"autoResolved": True}
  126. def test_subscribed(self):
  127. user = self.create_user()
  128. group = self.create_group()
  129. GroupSubscription.objects.create(
  130. user=user, group=group, project=group.project, is_active=True
  131. )
  132. result = serialize(group, user, serializer=GroupSerializerSnuba())
  133. assert result["isSubscribed"]
  134. assert result["subscriptionDetails"] == {"reason": "unknown"}
  135. def test_explicit_unsubscribed(self):
  136. user = self.create_user()
  137. group = self.create_group()
  138. GroupSubscription.objects.create(
  139. user=user, group=group, project=group.project, is_active=False
  140. )
  141. result = serialize(group, user, serializer=GroupSerializerSnuba())
  142. assert not result["isSubscribed"]
  143. assert not result["subscriptionDetails"]
  144. def test_implicit_subscribed(self):
  145. user = self.create_user()
  146. group = self.create_group()
  147. combinations = (
  148. # ((default, project), (subscribed, details))
  149. ((UserOptionValue.all_conversations, None), (True, None)),
  150. ((UserOptionValue.all_conversations, UserOptionValue.all_conversations), (True, None)),
  151. (
  152. (UserOptionValue.all_conversations, UserOptionValue.participating_only),
  153. (False, None),
  154. ),
  155. (
  156. (UserOptionValue.all_conversations, UserOptionValue.no_conversations),
  157. (False, {"disabled": True}),
  158. ),
  159. ((None, None), (False, None)),
  160. ((UserOptionValue.participating_only, None), (False, None)),
  161. ((UserOptionValue.participating_only, UserOptionValue.all_conversations), (True, None)),
  162. (
  163. (UserOptionValue.participating_only, UserOptionValue.participating_only),
  164. (False, None),
  165. ),
  166. (
  167. (UserOptionValue.participating_only, UserOptionValue.no_conversations),
  168. (False, {"disabled": True}),
  169. ),
  170. ((UserOptionValue.no_conversations, None), (False, {"disabled": True})),
  171. ((UserOptionValue.no_conversations, UserOptionValue.all_conversations), (True, None)),
  172. ((UserOptionValue.no_conversations, UserOptionValue.participating_only), (False, None)),
  173. (
  174. (UserOptionValue.no_conversations, UserOptionValue.no_conversations),
  175. (False, {"disabled": True}),
  176. ),
  177. )
  178. def maybe_set_value(project, value):
  179. if value is not None:
  180. UserOption.objects.set_value(
  181. user=user, project=project, key="workflow:notifications", value=value
  182. )
  183. else:
  184. UserOption.objects.unset_value(
  185. user=user, project=project, key="workflow:notifications"
  186. )
  187. for options, (is_subscribed, subscription_details) in combinations:
  188. default_value, project_value = options
  189. UserOption.objects.clear_local_cache()
  190. maybe_set_value(None, default_value)
  191. maybe_set_value(group.project, project_value)
  192. result = serialize(group, user, serializer=GroupSerializerSnuba())
  193. assert result["isSubscribed"] is is_subscribed
  194. assert result.get("subscriptionDetails") == subscription_details
  195. def test_global_no_conversations_overrides_group_subscription(self):
  196. user = self.create_user()
  197. group = self.create_group()
  198. GroupSubscription.objects.create(
  199. user=user, group=group, project=group.project, is_active=True
  200. )
  201. UserOption.objects.set_value(
  202. user=user,
  203. project=None,
  204. key="workflow:notifications",
  205. value=UserOptionValue.no_conversations,
  206. )
  207. result = serialize(group, user, serializer=GroupSerializerSnuba())
  208. assert not result["isSubscribed"]
  209. assert result["subscriptionDetails"] == {"disabled": True}
  210. def test_project_no_conversations_overrides_group_subscription(self):
  211. user = self.create_user()
  212. group = self.create_group()
  213. GroupSubscription.objects.create(
  214. user=user, group=group, project=group.project, is_active=True
  215. )
  216. UserOption.objects.set_value(
  217. user=user,
  218. project=group.project,
  219. key="workflow:notifications",
  220. value=UserOptionValue.no_conversations,
  221. )
  222. result = serialize(group, user, serializer=GroupSerializerSnuba())
  223. assert not result["isSubscribed"]
  224. assert result["subscriptionDetails"] == {"disabled": True}
  225. def test_no_user_unsubscribed(self):
  226. group = self.create_group()
  227. result = serialize(group, serializer=GroupSerializerSnuba())
  228. assert not result["isSubscribed"]
  229. def test_seen_stats(self):
  230. environment = self.create_environment(project=self.project)
  231. environment2 = self.create_environment(project=self.project)
  232. events = []
  233. for event_id, env, user_id, timestamp in [
  234. ("a" * 32, environment, 1, iso_format(self.min_ago)),
  235. ("b" * 32, environment, 2, iso_format(self.min_ago)),
  236. ("c" * 32, environment2, 3, iso_format(self.week_ago)),
  237. ]:
  238. events.append(
  239. self.store_event(
  240. data={
  241. "event_id": event_id,
  242. "fingerprint": ["put-me-in-group1"],
  243. "timestamp": timestamp,
  244. "environment": env.name,
  245. "user": {"id": user_id},
  246. },
  247. project_id=self.project.id,
  248. )
  249. )
  250. # Assert all events are in the same group
  251. (group_id,) = set(e.group.id for e in events)
  252. group = Group.objects.get(id=group_id)
  253. group.times_seen = 3
  254. group.first_seen = self.week_ago - timedelta(days=5)
  255. group.last_seen = self.week_ago
  256. group.save()
  257. # should use group columns when no environments arg passed
  258. result = serialize(group, serializer=GroupSerializerSnuba(environment_ids=[]))
  259. assert result["count"] == "3"
  260. assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
  261. assert result["firstSeen"] == group.first_seen
  262. # update this to something different to make sure it's being used
  263. group_env = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment.id)
  264. group_env.first_seen = self.day_ago - timedelta(days=3)
  265. group_env.save()
  266. group_env2 = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment2.id)
  267. result = serialize(
  268. group,
  269. serializer=GroupSerializerSnuba(environment_ids=[environment.id, environment2.id]),
  270. )
  271. assert result["count"] == "3"
  272. # result is rounded down to nearest second
  273. assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
  274. assert iso_format(result["firstSeen"]) == iso_format(group_env.first_seen)
  275. assert iso_format(group_env2.first_seen) > iso_format(group_env.first_seen)
  276. assert result["userCount"] == 3
  277. result = serialize(
  278. group,
  279. serializer=GroupSerializerSnuba(
  280. environment_ids=[environment.id, environment2.id],
  281. start=self.week_ago - timedelta(hours=1),
  282. end=self.week_ago + timedelta(hours=1),
  283. ),
  284. )
  285. assert result["userCount"] == 1
  286. assert iso_format(result["lastSeen"]) == iso_format(self.week_ago)
  287. assert iso_format(result["firstSeen"]) == iso_format(self.week_ago)
  288. assert result["count"] == "1"
  289. class StreamGroupSerializerTestCase(APITestCase, SnubaTestCase):
  290. def test_environment(self):
  291. group = self.group
  292. environment = Environment.get_or_create(group.project, "production")
  293. with mock.patch(
  294. "sentry.api.serializers.models.group.snuba_tsdb.get_range",
  295. side_effect=snuba_tsdb.get_range,
  296. ) as get_range:
  297. serialize(
  298. [group],
  299. serializer=StreamGroupSerializerSnuba(
  300. environment_ids=[environment.id], stats_period="14d"
  301. ),
  302. )
  303. assert get_range.call_count == 1
  304. for args, kwargs in get_range.call_args_list:
  305. assert kwargs["environment_ids"] == [environment.id]
  306. with mock.patch(
  307. "sentry.api.serializers.models.group.snuba_tsdb.get_range",
  308. side_effect=snuba_tsdb.get_range,
  309. ) as get_range:
  310. serialize(
  311. [group],
  312. serializer=StreamGroupSerializerSnuba(environment_ids=None, stats_period="14d"),
  313. )
  314. assert get_range.call_count == 1
  315. for args, kwargs in get_range.call_args_list:
  316. assert kwargs["environment_ids"] is None