test_group.py 22 KB

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