test_group.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. from datetime import timedelta
  2. from unittest.mock import patch
  3. import pytz
  4. from django.utils import timezone
  5. from sentry.api.event_search import SearchFilter, SearchKey, SearchValue
  6. from sentry.api.serializers import serialize
  7. from sentry.api.serializers.models.group import GroupSerializerSnuba
  8. from sentry.issues.grouptype import PerformanceNPlusOneGroupType, ProfileFileIOGroupType
  9. from sentry.models import (
  10. Group,
  11. GroupEnvironment,
  12. GroupLink,
  13. GroupResolution,
  14. GroupSnooze,
  15. GroupStatus,
  16. GroupSubscription,
  17. NotificationSetting,
  18. UserOption,
  19. )
  20. from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes
  21. from sentry.testutils import APITestCase, SnubaTestCase
  22. from sentry.testutils.cases import PerformanceIssueTestCase
  23. from sentry.testutils.helpers.datetime import before_now, iso_format
  24. from sentry.testutils.performance_issues.store_transaction import PerfIssueTransactionTestMixin
  25. from sentry.testutils.silo import exempt_from_silo_limits, region_silo_test
  26. from sentry.types.integrations import ExternalProviders
  27. from sentry.utils.samples import load_data
  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. issue_type="error",
  137. issue_category="error",
  138. )
  139. def test_subscribed(self):
  140. user = self.create_user()
  141. group = self.create_group()
  142. GroupSubscription.objects.create(
  143. user_id=user.id, group=group, project=group.project, is_active=True
  144. )
  145. result = serialize(group, user, serializer=GroupSerializerSnuba())
  146. assert result["isSubscribed"]
  147. assert result["subscriptionDetails"] == {"reason": "unknown"}
  148. def test_explicit_unsubscribed(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=False
  153. )
  154. result = serialize(group, user, serializer=GroupSerializerSnuba())
  155. assert not result["isSubscribed"]
  156. assert not result["subscriptionDetails"]
  157. def test_implicit_subscribed(self):
  158. user = self.create_user()
  159. group = self.create_group()
  160. combinations = (
  161. # (default, project, subscribed, has_details)
  162. (
  163. NotificationSettingOptionValues.ALWAYS,
  164. NotificationSettingOptionValues.DEFAULT,
  165. True,
  166. False,
  167. ),
  168. (
  169. NotificationSettingOptionValues.ALWAYS,
  170. NotificationSettingOptionValues.ALWAYS,
  171. True,
  172. False,
  173. ),
  174. (
  175. NotificationSettingOptionValues.ALWAYS,
  176. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  177. False,
  178. False,
  179. ),
  180. (
  181. NotificationSettingOptionValues.ALWAYS,
  182. NotificationSettingOptionValues.NEVER,
  183. False,
  184. True,
  185. ),
  186. (
  187. NotificationSettingOptionValues.DEFAULT,
  188. NotificationSettingOptionValues.DEFAULT,
  189. False,
  190. False,
  191. ),
  192. (
  193. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  194. NotificationSettingOptionValues.DEFAULT,
  195. False,
  196. False,
  197. ),
  198. (
  199. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  200. NotificationSettingOptionValues.ALWAYS,
  201. True,
  202. False,
  203. ),
  204. (
  205. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  206. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  207. False,
  208. False,
  209. ),
  210. (
  211. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  212. NotificationSettingOptionValues.NEVER,
  213. False,
  214. True,
  215. ),
  216. (
  217. NotificationSettingOptionValues.NEVER,
  218. NotificationSettingOptionValues.DEFAULT,
  219. False,
  220. True,
  221. ),
  222. (
  223. NotificationSettingOptionValues.NEVER,
  224. NotificationSettingOptionValues.ALWAYS,
  225. True,
  226. False,
  227. ),
  228. (
  229. NotificationSettingOptionValues.NEVER,
  230. NotificationSettingOptionValues.SUBSCRIBE_ONLY,
  231. False,
  232. False,
  233. ),
  234. (
  235. NotificationSettingOptionValues.NEVER,
  236. NotificationSettingOptionValues.NEVER,
  237. False,
  238. True,
  239. ),
  240. )
  241. for default_value, project_value, is_subscribed, has_details in combinations:
  242. with exempt_from_silo_limits():
  243. UserOption.objects.clear_local_cache()
  244. NotificationSetting.objects.update_settings(
  245. ExternalProviders.EMAIL,
  246. NotificationSettingTypes.WORKFLOW,
  247. default_value,
  248. user_id=user.id,
  249. )
  250. NotificationSetting.objects.update_settings(
  251. ExternalProviders.EMAIL,
  252. NotificationSettingTypes.WORKFLOW,
  253. project_value,
  254. user_id=user.id,
  255. project=group.project.id,
  256. )
  257. NotificationSetting.objects.update_settings(
  258. ExternalProviders.SLACK,
  259. NotificationSettingTypes.WORKFLOW,
  260. default_value,
  261. user_id=user.id,
  262. )
  263. NotificationSetting.objects.update_settings(
  264. ExternalProviders.SLACK,
  265. NotificationSettingTypes.WORKFLOW,
  266. project_value,
  267. user_id=user.id,
  268. project=group.project.id,
  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 exempt_from_silo_limits():
  285. for provider in [ExternalProviders.EMAIL, ExternalProviders.SLACK]:
  286. NotificationSetting.objects.update_settings(
  287. provider,
  288. NotificationSettingTypes.WORKFLOW,
  289. NotificationSettingOptionValues.NEVER,
  290. user_id=user.id,
  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. for provider in [ExternalProviders.EMAIL, ExternalProviders.SLACK]:
  302. with exempt_from_silo_limits():
  303. NotificationSetting.objects.update_settings(
  304. provider,
  305. NotificationSettingTypes.WORKFLOW,
  306. NotificationSettingOptionValues.NEVER,
  307. user_id=user.id,
  308. project=group.project.id,
  309. )
  310. result = serialize(group, user, serializer=GroupSerializerSnuba())
  311. assert not result["isSubscribed"]
  312. assert result["subscriptionDetails"] == {"disabled": True}
  313. def test_no_user_unsubscribed(self):
  314. group = self.create_group()
  315. result = serialize(group, serializer=GroupSerializerSnuba())
  316. assert not result["isSubscribed"]
  317. def test_seen_stats(self):
  318. environment = self.create_environment(project=self.project)
  319. environment2 = self.create_environment(project=self.project)
  320. events = []
  321. for event_id, env, user_id, timestamp in [
  322. ("a" * 32, environment, 1, iso_format(self.min_ago)),
  323. ("b" * 32, environment, 2, iso_format(self.min_ago)),
  324. ("c" * 32, environment2, 3, iso_format(self.week_ago)),
  325. ]:
  326. events.append(
  327. self.store_event(
  328. data={
  329. "event_id": event_id,
  330. "fingerprint": ["put-me-in-group1"],
  331. "timestamp": timestamp,
  332. "environment": env.name,
  333. "user": {"id": user_id},
  334. },
  335. project_id=self.project.id,
  336. )
  337. )
  338. # Assert all events are in the same group
  339. (group_id,) = {e.group.id for e in events}
  340. group = Group.objects.get(id=group_id)
  341. group.times_seen = 3
  342. group.first_seen = self.week_ago - timedelta(days=5)
  343. group.last_seen = self.week_ago
  344. group.save()
  345. # should use group columns when no environments arg passed
  346. result = serialize(group, serializer=GroupSerializerSnuba(environment_ids=[]))
  347. assert result["count"] == "3"
  348. assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
  349. assert result["firstSeen"] == group.first_seen
  350. # update this to something different to make sure it's being used
  351. group_env = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment.id)
  352. group_env.first_seen = self.day_ago - timedelta(days=3)
  353. group_env.save()
  354. group_env2 = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment2.id)
  355. result = serialize(
  356. group,
  357. serializer=GroupSerializerSnuba(environment_ids=[environment.id, environment2.id]),
  358. )
  359. assert result["count"] == "3"
  360. # result is rounded down to nearest second
  361. assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
  362. assert iso_format(result["firstSeen"]) == iso_format(group_env.first_seen)
  363. assert iso_format(group_env2.first_seen) > iso_format(group_env.first_seen)
  364. assert result["userCount"] == 3
  365. result = serialize(
  366. group,
  367. serializer=GroupSerializerSnuba(
  368. environment_ids=[environment.id, environment2.id],
  369. start=self.week_ago - timedelta(hours=1),
  370. end=self.week_ago + timedelta(hours=1),
  371. ),
  372. )
  373. assert result["userCount"] == 1
  374. assert iso_format(result["lastSeen"]) == iso_format(self.week_ago)
  375. assert iso_format(result["firstSeen"]) == iso_format(self.week_ago)
  376. assert result["count"] == "1"
  377. def test_get_start_from_seen_stats(self):
  378. for days, expected in [(None, 30), (0, 14), (1000, 90)]:
  379. last_seen = None if days is None else before_now(days=days).replace(tzinfo=pytz.UTC)
  380. start = GroupSerializerSnuba._get_start_from_seen_stats({"": {"last_seen": last_seen}})
  381. assert iso_format(start) == iso_format(before_now(days=expected))
  382. def test_skipped_date_timestamp_filters(self):
  383. group = self.create_group()
  384. serializer = GroupSerializerSnuba(
  385. search_filters=[
  386. SearchFilter(
  387. SearchKey("timestamp"),
  388. ">",
  389. SearchValue(before_now(hours=1).replace(tzinfo=pytz.UTC)),
  390. ),
  391. SearchFilter(
  392. SearchKey("timestamp"),
  393. "<",
  394. SearchValue(before_now(seconds=1).replace(tzinfo=pytz.UTC)),
  395. ),
  396. SearchFilter(
  397. SearchKey("date"),
  398. ">",
  399. SearchValue(before_now(hours=1).replace(tzinfo=pytz.UTC)),
  400. ),
  401. SearchFilter(
  402. SearchKey("date"),
  403. "<",
  404. SearchValue(before_now(seconds=1).replace(tzinfo=pytz.UTC)),
  405. ),
  406. ]
  407. )
  408. assert not serializer.conditions
  409. result = serialize(group, self.user, serializer=serializer)
  410. assert result["id"] == str(group.id)
  411. @region_silo_test
  412. class PerformanceGroupSerializerSnubaTest(
  413. APITestCase,
  414. SnubaTestCase,
  415. PerfIssueTransactionTestMixin,
  416. PerformanceIssueTestCase,
  417. ):
  418. def test_perf_seen_stats(self):
  419. proj = self.create_project()
  420. first_group_fingerprint = f"{PerformanceNPlusOneGroupType.type_id}-group1"
  421. timestamp = timezone.now() - timedelta(days=5)
  422. times = 5
  423. for _ in range(0, times):
  424. event_data = load_data(
  425. "transaction-n-plus-one",
  426. timestamp=timestamp + timedelta(minutes=1),
  427. start_timestamp=timestamp + timedelta(minutes=1),
  428. )
  429. event_data["user"] = {"email": "test1@example.com"}
  430. self.create_performance_issue(
  431. event_data=event_data, fingerprint=first_group_fingerprint, project_id=proj.id
  432. )
  433. event_data = load_data(
  434. "transaction-n-plus-one",
  435. timestamp=timestamp + timedelta(minutes=2),
  436. start_timestamp=timestamp + timedelta(minutes=2),
  437. )
  438. event_data["user"] = {"email": "test2@example.com"}
  439. event = self.create_performance_issue(
  440. event_data=event_data, fingerprint=first_group_fingerprint, project_id=proj.id
  441. )
  442. first_group = event.group
  443. result = serialize(
  444. first_group,
  445. serializer=GroupSerializerSnuba(
  446. start=timezone.now() - timedelta(days=60),
  447. end=timezone.now() + timedelta(days=10),
  448. ),
  449. )
  450. assert result["userCount"] == 2
  451. assert iso_format(result["lastSeen"]) == iso_format(timestamp + timedelta(minutes=2))
  452. assert iso_format(result["firstSeen"]) == iso_format(timestamp + timedelta(minutes=1))
  453. assert result["count"] == str(times + 1)
  454. @region_silo_test
  455. class ProfilingGroupSerializerSnubaTest(
  456. APITestCase,
  457. SnubaTestCase,
  458. SearchIssueTestMixin,
  459. ):
  460. def test_profiling_seen_stats(self):
  461. proj = self.create_project()
  462. environment = self.create_environment(project=proj)
  463. first_group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group1"
  464. timestamp = (timezone.now() - timedelta(days=5)).replace(hour=0, minute=0, second=0)
  465. times = 5
  466. for incr in range(0, times):
  467. # for user_0 - user_4, first_group
  468. self.store_search_issue(
  469. proj.id,
  470. incr,
  471. [first_group_fingerprint],
  472. environment.name,
  473. timestamp + timedelta(minutes=incr),
  474. )
  475. # user_5, another_group
  476. event, issue_occurrence, group_info = self.store_search_issue(
  477. proj.id,
  478. 5,
  479. [first_group_fingerprint],
  480. environment.name,
  481. timestamp + timedelta(minutes=5),
  482. )
  483. first_group = group_info.group
  484. result = serialize(
  485. first_group,
  486. serializer=GroupSerializerSnuba(
  487. environment_ids=[environment.id],
  488. start=timestamp - timedelta(days=1),
  489. end=timestamp + timedelta(days=1),
  490. ),
  491. )
  492. assert result["userCount"] == 6
  493. assert iso_format(result["lastSeen"]) == iso_format(timestamp + timedelta(minutes=5))
  494. assert iso_format(result["firstSeen"]) == iso_format(timestamp)
  495. assert result["count"] == str(times + 1)