test_group.py 15 KB

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