test_group.py 21 KB

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