test_group.py 21 KB

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