test_group.py 22 KB

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