test_group_details.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. from unittest import mock
  2. from rest_framework.exceptions import ErrorDetail
  3. from sentry import tsdb
  4. from sentry.issues.forecasts import generate_and_save_forecasts
  5. from sentry.issues.priority import PriorityLevel
  6. from sentry.models.activity import Activity
  7. from sentry.models.environment import Environment
  8. from sentry.models.group import GroupStatus
  9. from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox, remove_group_from_inbox
  10. from sentry.models.groupowner import GROUP_OWNER_TYPE, GroupOwner, GroupOwnerType
  11. from sentry.models.release import Release
  12. from sentry.testutils.cases import APITestCase, SnubaTestCase
  13. from sentry.testutils.helpers.datetime import before_now, iso_format
  14. from sentry.testutils.helpers.features import with_feature
  15. from sentry.testutils.silo import region_silo_test
  16. from sentry.types.activity import ActivityType
  17. @region_silo_test
  18. class GroupDetailsTest(APITestCase, SnubaTestCase):
  19. def test_multiple_environments(self):
  20. group = self.create_group()
  21. self.login_as(user=self.user)
  22. environment = Environment.get_or_create(group.project, "production")
  23. environment2 = Environment.get_or_create(group.project, "staging")
  24. url = f"/api/0/issues/{group.id}/"
  25. with mock.patch(
  26. "sentry.api.endpoints.group_details.tsdb.backend.get_range",
  27. side_effect=tsdb.backend.get_range,
  28. ) as get_range:
  29. response = self.client.get(
  30. f"{url}?environment=production&environment=staging", format="json"
  31. )
  32. assert response.status_code == 200
  33. assert get_range.call_count == 2
  34. for args, kwargs in get_range.call_args_list:
  35. assert kwargs["environment_ids"] == [environment.id, environment2.id]
  36. response = self.client.get(f"{url}?environment=invalid", format="json")
  37. assert response.status_code == 404
  38. def test_with_first_last_release(self):
  39. self.login_as(user=self.user)
  40. first_release = {
  41. "firstEvent": before_now(minutes=3),
  42. "lastEvent": before_now(minutes=2, seconds=30),
  43. }
  44. last_release = {
  45. "firstEvent": before_now(minutes=1, seconds=30),
  46. "lastEvent": before_now(minutes=1),
  47. }
  48. for timestamp in first_release.values():
  49. self.store_event(
  50. data={"release": "1.0", "timestamp": iso_format(timestamp)},
  51. project_id=self.project.id,
  52. )
  53. self.store_event(
  54. data={"release": "1.1", "timestamp": iso_format(before_now(minutes=2))},
  55. project_id=self.project.id,
  56. )
  57. event = [
  58. self.store_event(
  59. data={"release": "1.0a", "timestamp": iso_format(timestamp)},
  60. project_id=self.project.id,
  61. )
  62. for timestamp in last_release.values()
  63. ][-1]
  64. group = event.group
  65. url = f"/api/0/issues/{group.id}/"
  66. response = self.client.get(url, format="json")
  67. assert response.status_code == 200, response.content
  68. assert response.data["id"] == str(group.id)
  69. release = response.data["firstRelease"]
  70. assert release["version"] == "1.0"
  71. for event, timestamp in first_release.items():
  72. assert release[event].ctime() == timestamp.ctime()
  73. release = response.data["lastRelease"]
  74. assert release["version"] == "1.0a"
  75. for event, timestamp in last_release.items():
  76. assert release[event].ctime() == timestamp.ctime()
  77. def test_first_last_only_one_tagstore(self):
  78. self.login_as(user=self.user)
  79. event = self.store_event(
  80. data={"release": "1.0", "timestamp": iso_format(before_now(days=3))},
  81. project_id=self.project.id,
  82. )
  83. self.store_event(
  84. data={"release": "1.1", "timestamp": iso_format(before_now(minutes=3))},
  85. project_id=self.project.id,
  86. )
  87. group = event.group
  88. url = f"/api/0/issues/{group.id}/"
  89. with mock.patch("sentry.tagstore.backend.get_release_tags") as get_release_tags:
  90. response = self.client.get(url, format="json")
  91. assert response.status_code == 200
  92. assert get_release_tags.call_count == 1
  93. def test_first_release_only(self):
  94. self.login_as(user=self.user)
  95. first_event = before_now(days=3)
  96. self.store_event(
  97. data={"release": "1.0", "timestamp": iso_format(first_event)},
  98. project_id=self.project.id,
  99. )
  100. event = self.store_event(
  101. data={"release": "1.1", "timestamp": iso_format(before_now(days=1))},
  102. project_id=self.project.id,
  103. )
  104. # Forcibly remove one of the releases
  105. Release.objects.get(version="1.1").delete()
  106. group = event.group
  107. url = f"/api/0/issues/{group.id}/"
  108. response = self.client.get(url, format="json")
  109. assert response.status_code == 200, response.content
  110. assert response.data["firstRelease"]["version"] == "1.0"
  111. # only one event
  112. assert (
  113. response.data["firstRelease"]["firstEvent"]
  114. == response.data["firstRelease"]["lastEvent"]
  115. )
  116. assert response.data["firstRelease"]["firstEvent"].ctime() == first_event.ctime()
  117. assert response.data["lastRelease"] is None
  118. def test_group_expand_inbox(self):
  119. self.login_as(user=self.user)
  120. event = self.store_event(
  121. data={"timestamp": iso_format(before_now(minutes=3))},
  122. project_id=self.project.id,
  123. )
  124. group = event.group
  125. add_group_to_inbox(group, GroupInboxReason.NEW)
  126. url = f"/api/0/issues/{group.id}/?expand=inbox"
  127. response = self.client.get(url, format="json")
  128. assert response.status_code == 200, response.content
  129. assert response.data["inbox"] is not None
  130. assert response.data["inbox"]["reason"] == GroupInboxReason.NEW.value
  131. assert response.data["inbox"]["reason_details"] is None
  132. remove_group_from_inbox(event.group)
  133. response = self.client.get(url, format="json")
  134. assert response.status_code == 200, response.content
  135. assert response.data["inbox"] is None
  136. def test_group_expand_owners(self):
  137. self.login_as(user=self.user)
  138. event = self.store_event(
  139. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  140. project_id=self.project.id,
  141. )
  142. group = event.group
  143. url = f"/api/0/issues/{group.id}/?expand=owners"
  144. self.login_as(user=self.user)
  145. # Test with no owner
  146. response = self.client.get(url, format="json")
  147. assert response.status_code == 200
  148. assert response.data["owners"] is None
  149. # Test with owners
  150. GroupOwner.objects.create(
  151. group=event.group,
  152. project=event.project,
  153. organization=event.project.organization,
  154. type=GroupOwnerType.SUSPECT_COMMIT.value,
  155. user_id=self.user.id,
  156. )
  157. response = self.client.get(url, format="json")
  158. assert response.status_code == 200, response.content
  159. assert response.data["owners"] is not None
  160. assert len(response.data["owners"]) == 1
  161. assert response.data["owners"][0]["owner"] == f"user:{self.user.id}"
  162. assert response.data["owners"][0]["type"] == GROUP_OWNER_TYPE[GroupOwnerType.SUSPECT_COMMIT]
  163. def test_group_expand_forecasts(self):
  164. self.login_as(user=self.user)
  165. event = self.store_event(
  166. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  167. project_id=self.project.id,
  168. )
  169. group = event.group
  170. generate_and_save_forecasts([group])
  171. url = f"/api/0/issues/{group.id}/?expand=forecast"
  172. response = self.client.get(url, format="json")
  173. assert response.status_code == 200, response.content
  174. assert response.data["forecast"] is not None
  175. assert response.data["forecast"]["data"] is not None
  176. assert response.data["forecast"]["date_added"] is not None
  177. @with_feature("projects:issue-priority")
  178. def test_group_get_priority(self):
  179. self.login_as(user=self.user)
  180. group = self.create_group(
  181. project=self.project,
  182. status=GroupStatus.IGNORED,
  183. priority=PriorityLevel.LOW,
  184. )
  185. url = f"/api/0/issues/{group.id}/"
  186. response = self.client.get(url, format="json")
  187. assert response.status_code == 200, response.content
  188. assert response.data["priority"] == "low"
  189. assert response.data["priorityLockedAt"] is None
  190. def test_group_get_priority_no_ff(self):
  191. self.login_as(user=self.user)
  192. group = self.create_group(
  193. project=self.project,
  194. status=GroupStatus.IGNORED,
  195. priority=PriorityLevel.LOW,
  196. )
  197. url = f"/api/0/issues/{group.id}/"
  198. response = self.client.get(url, format="json")
  199. assert response.status_code == 200, response.content
  200. assert "priority" not in response.data
  201. assert "priorityLockedAt" not in response.data
  202. @with_feature("projects:issue-priority")
  203. def test_group_post_priority(self):
  204. self.login_as(user=self.user)
  205. group = self.create_group(
  206. project=self.project,
  207. status=GroupStatus.IGNORED,
  208. priority=PriorityLevel.LOW,
  209. )
  210. url = f"/api/0/issues/{group.id}/"
  211. get_response_before = self.client.get(url, format="json")
  212. assert get_response_before.status_code == 200, get_response_before.content
  213. assert get_response_before.data["priority"] == "low"
  214. response = self.client.put(url, {"priority": "high"}, format="json")
  215. assert response.status_code == 200, response.content
  216. assert response.data["priority"] == "high"
  217. act_for_group = Activity.objects.get_activities_for_group(group=group, num=100)
  218. assert len(act_for_group) == 2
  219. assert act_for_group[0].type == ActivityType.SET_PRIORITY.value
  220. assert act_for_group[-1].type == ActivityType.FIRST_SEEN.value
  221. assert act_for_group[0].user_id == self.user.id
  222. assert act_for_group[0].data["priority"] == "high"
  223. get_response_after = self.client.get(url, format="json")
  224. assert get_response_after.status_code == 200, get_response_after.content
  225. assert get_response_after.data["priority"] == "high"
  226. assert get_response_after.data["priorityLockedAt"] is not None
  227. def test_group_post_priority_no_ff(self):
  228. self.login_as(user=self.user)
  229. group = self.create_group(
  230. project=self.project,
  231. status=GroupStatus.IGNORED,
  232. priority=PriorityLevel.LOW,
  233. )
  234. url = f"/api/0/issues/{group.id}/"
  235. get_response_before = self.client.get(url, format="json")
  236. assert get_response_before.status_code == 200, get_response_before.content
  237. response = self.client.put(url, {"priority": "high"}, format="json")
  238. assert response.status_code == 200, response.content
  239. act_for_group = Activity.objects.get_activities_for_group(group=group, num=100)
  240. assert len(act_for_group) == 1
  241. assert act_for_group[0].type == ActivityType.FIRST_SEEN.value
  242. get_response_after = self.client.get(url, format="json")
  243. assert get_response_after.status_code == 200, get_response_after.content
  244. assert get_response_after.content == get_response_before.content
  245. def test_assigned_to_unknown(self):
  246. self.login_as(user=self.user)
  247. event = self.store_event(
  248. data={"timestamp": iso_format(before_now(minutes=3))},
  249. project_id=self.project.id,
  250. )
  251. group = event.group
  252. url = f"/api/0/issues/{group.id}/"
  253. response = self.client.put(
  254. url, {"assignedTo": "admin@localhost", "status": "unresolved"}, format="json"
  255. )
  256. assert response.status_code == 200
  257. response = self.client.put(
  258. url, {"assignedTo": "user@doesnotexist.com", "status": "unresolved"}, format="json"
  259. )
  260. assert response.status_code == 400
  261. assert response.data == {
  262. "assignedTo": [
  263. ErrorDetail(
  264. string="Could not parse actor. Format should be `type:id` where type is `team` or `user`.",
  265. code="invalid",
  266. )
  267. ]
  268. }
  269. def test_collapse_stats_does_not_work(self):
  270. """
  271. 'collapse' param should hide the stats data and not return anything in the response, but the impl
  272. doesn't seem to respect this param.
  273. include this test here in-case the endpoint behavior changes in the future.
  274. """
  275. self.login_as(user=self.user)
  276. event = self.store_event(
  277. data={"timestamp": iso_format(before_now(minutes=3))},
  278. project_id=self.project.id,
  279. )
  280. group = event.group
  281. url = f"/api/0/issues/{group.id}/"
  282. response = self.client.get(url, {"collapse": ["stats"]}, format="json")
  283. assert response.status_code == 200
  284. assert int(response.data["id"]) == event.group.id
  285. assert response.data["stats"] # key shouldn't be present
  286. assert response.data["count"] is not None # key shouldn't be present
  287. assert response.data["userCount"] is not None # key shouldn't be present
  288. assert response.data["firstSeen"] is not None # key shouldn't be present
  289. assert response.data["lastSeen"] is not None # key shouldn't be present
  290. def test_issue_type_category(self):
  291. """Test that the issue's type and category is returned in the results"""
  292. self.login_as(user=self.user)
  293. event = self.store_event(
  294. data={"timestamp": iso_format(before_now(minutes=3))},
  295. project_id=self.project.id,
  296. )
  297. url = f"/api/0/issues/{event.group.id}/"
  298. response = self.client.get(url, format="json")
  299. assert response.status_code == 200
  300. assert int(response.data["id"]) == event.group.id
  301. assert response.data["issueType"] == "error"
  302. assert response.data["issueCategory"] == "error"