test_organization_group_index.py 130 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307
  1. from datetime import timedelta
  2. from unittest.mock import Mock, patch
  3. from uuid import uuid4
  4. from dateutil.parser import parse as parse_datetime
  5. from django.test import override_settings
  6. from django.urls import reverse
  7. from django.utils import timezone
  8. from freezegun import freeze_time
  9. from rest_framework import status
  10. from sentry import options
  11. from sentry.models import (
  12. GROUP_OWNER_TYPE,
  13. Activity,
  14. ApiToken,
  15. Commit,
  16. ExternalIssue,
  17. Group,
  18. GroupAssignee,
  19. GroupBookmark,
  20. GroupHash,
  21. GroupHistory,
  22. GroupInbox,
  23. GroupInboxReason,
  24. GroupLink,
  25. GroupOwner,
  26. GroupOwnerType,
  27. GroupResolution,
  28. GroupSeen,
  29. GroupShare,
  30. GroupSnooze,
  31. GroupStatus,
  32. GroupSubscription,
  33. GroupTombstone,
  34. Integration,
  35. OrganizationIntegration,
  36. Release,
  37. ReleaseCommit,
  38. ReleaseStages,
  39. Repository,
  40. UserOption,
  41. add_group_to_inbox,
  42. remove_group_from_inbox,
  43. )
  44. from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
  45. from sentry.search.events.constants import (
  46. RELEASE_STAGE_ALIAS,
  47. SEMVER_ALIAS,
  48. SEMVER_BUILD_ALIAS,
  49. SEMVER_PACKAGE_ALIAS,
  50. )
  51. from sentry.testutils import APITestCase, SnubaTestCase
  52. from sentry.testutils.helpers import apply_feature_flag_on_cls, parse_link_header
  53. from sentry.testutils.helpers.datetime import before_now, iso_format
  54. from sentry.types.activity import ActivityType
  55. from sentry.utils import json
  56. @apply_feature_flag_on_cls("organizations:release-committer-assignees")
  57. class GroupListTest(APITestCase, SnubaTestCase):
  58. endpoint = "sentry-api-0-organization-group-index"
  59. def setUp(self):
  60. super().setUp()
  61. self.min_ago = before_now(minutes=1)
  62. def _parse_links(self, header):
  63. # links come in {url: {...attrs}}, but we need {rel: {...attrs}}
  64. links = {}
  65. for url, attrs in parse_link_header(header).items():
  66. links[attrs["rel"]] = attrs
  67. attrs["href"] = url
  68. return links
  69. def get_response(self, *args, **kwargs):
  70. if not args:
  71. org = self.project.organization.slug
  72. else:
  73. org = args[0]
  74. return super().get_response(org, **kwargs)
  75. def test_sort_by_date_with_tag(self):
  76. # XXX(dcramer): this tests a case where an ambiguous column name existed
  77. event = self.store_event(
  78. data={"event_id": "a" * 32, "timestamp": iso_format(before_now(seconds=1))},
  79. project_id=self.project.id,
  80. )
  81. group = event.group
  82. self.login_as(user=self.user)
  83. response = self.get_success_response(sort_by="date", query="is:unresolved")
  84. assert len(response.data) == 1
  85. assert response.data[0]["id"] == str(group.id)
  86. def test_sort_by_trend(self):
  87. group = self.store_event(
  88. data={
  89. "timestamp": iso_format(before_now(seconds=10)),
  90. "fingerprint": ["group-1"],
  91. },
  92. project_id=self.project.id,
  93. ).group
  94. self.store_event(
  95. data={
  96. "timestamp": iso_format(before_now(seconds=10)),
  97. "fingerprint": ["group-1"],
  98. },
  99. project_id=self.project.id,
  100. )
  101. self.store_event(
  102. data={
  103. "timestamp": iso_format(before_now(hours=13)),
  104. "fingerprint": ["group-1"],
  105. },
  106. project_id=self.project.id,
  107. )
  108. group_2 = self.store_event(
  109. data={
  110. "timestamp": iso_format(before_now(seconds=5)),
  111. "fingerprint": ["group-2"],
  112. },
  113. project_id=self.project.id,
  114. ).group
  115. self.store_event(
  116. data={
  117. "timestamp": iso_format(before_now(hours=13)),
  118. "fingerprint": ["group-2"],
  119. },
  120. project_id=self.project.id,
  121. )
  122. self.login_as(user=self.user)
  123. response = self.get_success_response(
  124. sort="trend",
  125. query="is:unresolved",
  126. limit=1,
  127. start=iso_format(before_now(days=1)),
  128. end=iso_format(before_now(seconds=1)),
  129. )
  130. assert len(response.data) == 1
  131. assert [item["id"] for item in response.data] == [str(group.id)]
  132. header_links = parse_link_header(response["Link"])
  133. cursor = [link for link in header_links.values() if link["rel"] == "next"][0]["cursor"]
  134. response = self.get_success_response(
  135. sort="trend",
  136. query="is:unresolved",
  137. limit=1,
  138. start=iso_format(before_now(days=1)),
  139. end=iso_format(before_now(seconds=1)),
  140. cursor=cursor,
  141. )
  142. assert [item["id"] for item in response.data] == [str(group_2.id)]
  143. def test_sort_by_inbox(self):
  144. group_1 = self.store_event(
  145. data={
  146. "event_id": "a" * 32,
  147. "timestamp": iso_format(before_now(seconds=1)),
  148. "fingerprint": ["group-1"],
  149. },
  150. project_id=self.project.id,
  151. ).group
  152. inbox_1 = add_group_to_inbox(group_1, GroupInboxReason.NEW)
  153. group_2 = self.store_event(
  154. data={
  155. "event_id": "a" * 32,
  156. "timestamp": iso_format(before_now(seconds=1)),
  157. "fingerprint": ["group-2"],
  158. },
  159. project_id=self.project.id,
  160. ).group
  161. inbox_2 = add_group_to_inbox(group_2, GroupInboxReason.NEW)
  162. inbox_2.update(date_added=inbox_1.date_added - timedelta(hours=1))
  163. self.login_as(user=self.user)
  164. response = self.get_success_response(
  165. sort="inbox", query="is:unresolved is:for_review", limit=1
  166. )
  167. assert len(response.data) == 1
  168. assert response.data[0]["id"] == str(group_1.id)
  169. header_links = parse_link_header(response["Link"])
  170. cursor = [link for link in header_links.values() if link["rel"] == "next"][0]["cursor"]
  171. response = self.get_response(
  172. sort="inbox", cursor=cursor, query="is:unresolved is:for_review", limit=1
  173. )
  174. assert [item["id"] for item in response.data] == [str(group_2.id)]
  175. def test_sort_by_inbox_me_or_none(self):
  176. group_1 = self.store_event(
  177. data={
  178. "event_id": "a" * 32,
  179. "timestamp": iso_format(before_now(seconds=1)),
  180. "fingerprint": ["group-1"],
  181. },
  182. project_id=self.project.id,
  183. ).group
  184. inbox_1 = add_group_to_inbox(group_1, GroupInboxReason.NEW)
  185. group_2 = self.store_event(
  186. data={
  187. "event_id": "b" * 32,
  188. "timestamp": iso_format(before_now(seconds=1)),
  189. "fingerprint": ["group-2"],
  190. },
  191. project_id=self.project.id,
  192. ).group
  193. inbox_2 = add_group_to_inbox(group_2, GroupInboxReason.NEW)
  194. inbox_2.update(date_added=inbox_1.date_added - timedelta(hours=1))
  195. GroupOwner.objects.create(
  196. group=group_2,
  197. project=self.project,
  198. organization=self.organization,
  199. type=GroupOwnerType.OWNERSHIP_RULE.value,
  200. user=self.user,
  201. )
  202. owner_by_other = self.store_event(
  203. data={
  204. "event_id": "c" * 32,
  205. "timestamp": iso_format(before_now(seconds=1)),
  206. "fingerprint": ["group-3"],
  207. },
  208. project_id=self.project.id,
  209. ).group
  210. inbox_3 = add_group_to_inbox(owner_by_other, GroupInboxReason.NEW)
  211. inbox_3.update(date_added=inbox_1.date_added - timedelta(hours=1))
  212. other_user = self.create_user()
  213. GroupOwner.objects.create(
  214. group=owner_by_other,
  215. project=self.project,
  216. organization=self.organization,
  217. type=GroupOwnerType.OWNERSHIP_RULE.value,
  218. user=other_user,
  219. )
  220. owned_me_assigned_to_other = self.store_event(
  221. data={
  222. "event_id": "d" * 32,
  223. "timestamp": iso_format(before_now(seconds=1)),
  224. "fingerprint": ["group-4"],
  225. },
  226. project_id=self.project.id,
  227. ).group
  228. inbox_4 = add_group_to_inbox(owned_me_assigned_to_other, GroupInboxReason.NEW)
  229. inbox_4.update(date_added=inbox_1.date_added - timedelta(hours=1))
  230. GroupAssignee.objects.assign(owned_me_assigned_to_other, other_user)
  231. GroupOwner.objects.create(
  232. group=owned_me_assigned_to_other,
  233. project=self.project,
  234. organization=self.organization,
  235. type=GroupOwnerType.OWNERSHIP_RULE.value,
  236. user=self.user,
  237. )
  238. unowned_assigned_to_other = self.store_event(
  239. data={
  240. "event_id": "e" * 32,
  241. "timestamp": iso_format(before_now(seconds=1)),
  242. "fingerprint": ["group-5"],
  243. },
  244. project_id=self.project.id,
  245. ).group
  246. inbox_5 = add_group_to_inbox(unowned_assigned_to_other, GroupInboxReason.NEW)
  247. inbox_5.update(date_added=inbox_1.date_added - timedelta(hours=1))
  248. GroupAssignee.objects.assign(unowned_assigned_to_other, other_user)
  249. self.login_as(user=self.user)
  250. response = self.get_success_response(
  251. sort="inbox",
  252. query="is:unresolved is:for_review assigned_or_suggested:[me, none]",
  253. limit=10,
  254. )
  255. assert [item["id"] for item in response.data] == [str(group_1.id), str(group_2.id)]
  256. def test_trace_search(self):
  257. event = self.store_event(
  258. data={
  259. "event_id": "a" * 32,
  260. "timestamp": iso_format(before_now(seconds=1)),
  261. "contexts": {
  262. "trace": {
  263. "parent_span_id": "8988cec7cc0779c1",
  264. "type": "trace",
  265. "op": "foobar",
  266. "trace_id": "a7d67cf796774551a95be6543cacd459",
  267. "span_id": "babaae0d4b7512d9",
  268. "status": "ok",
  269. }
  270. },
  271. },
  272. project_id=self.project.id,
  273. )
  274. self.login_as(user=self.user)
  275. response = self.get_success_response(
  276. sort_by="date", query="is:unresolved trace:a7d67cf796774551a95be6543cacd459"
  277. )
  278. assert len(response.data) == 1
  279. assert response.data[0]["id"] == str(event.group.id)
  280. def test_feature_gate(self):
  281. # ensure there are two or more projects
  282. self.create_project(organization=self.project.organization)
  283. self.login_as(user=self.user)
  284. response = self.get_response()
  285. assert response.status_code == 400
  286. assert response.data["detail"] == "You do not have the multi project stream feature enabled"
  287. with self.feature("organizations:global-views"):
  288. response = self.get_response()
  289. assert response.status_code == 200
  290. def test_with_all_projects(self):
  291. # ensure there are two or more projects
  292. self.create_project(organization=self.project.organization)
  293. self.login_as(user=self.user)
  294. with self.feature("organizations:global-views"):
  295. response = self.get_success_response(project_id=[-1])
  296. assert response.status_code == 200
  297. def test_boolean_search_feature_flag(self):
  298. self.login_as(user=self.user)
  299. response = self.get_response(sort_by="date", query="title:hello OR title:goodbye")
  300. assert response.status_code == 400
  301. assert (
  302. response.data["detail"]
  303. == 'Error parsing search query: Boolean statements containing "OR" or "AND" are not supported in this search'
  304. )
  305. response = self.get_response(sort_by="date", query="title:hello AND title:goodbye")
  306. assert response.status_code == 400
  307. assert (
  308. response.data["detail"]
  309. == 'Error parsing search query: Boolean statements containing "OR" or "AND" are not supported in this search'
  310. )
  311. def test_invalid_query(self):
  312. now = timezone.now()
  313. self.create_group(last_seen=now - timedelta(seconds=1))
  314. self.login_as(user=self.user)
  315. response = self.get_response(sort_by="date", query="timesSeen:>1t")
  316. assert response.status_code == 400
  317. assert "Invalid number" in response.data["detail"]
  318. def test_valid_numeric_query(self):
  319. now = timezone.now()
  320. self.create_group(last_seen=now - timedelta(seconds=1))
  321. self.login_as(user=self.user)
  322. response = self.get_response(sort_by="date", query="timesSeen:>1k")
  323. assert response.status_code == 200
  324. def test_invalid_sort_key(self):
  325. now = timezone.now()
  326. self.create_group(last_seen=now - timedelta(seconds=1))
  327. self.login_as(user=self.user)
  328. response = self.get_response(sort="meow", query="is:unresolved")
  329. assert response.status_code == 400
  330. def test_simple_pagination(self):
  331. event1 = self.store_event(
  332. data={"timestamp": iso_format(before_now(seconds=2)), "fingerprint": ["group-1"]},
  333. project_id=self.project.id,
  334. )
  335. group1 = event1.group
  336. event2 = self.store_event(
  337. data={"timestamp": iso_format(before_now(seconds=1)), "fingerprint": ["group-2"]},
  338. project_id=self.project.id,
  339. )
  340. group2 = event2.group
  341. self.login_as(user=self.user)
  342. response = self.get_success_response(sort_by="date", limit=1)
  343. assert len(response.data) == 1
  344. assert response.data[0]["id"] == str(group2.id)
  345. links = self._parse_links(response["Link"])
  346. assert links["previous"]["results"] == "false"
  347. assert links["next"]["results"] == "true"
  348. response = self.client.get(links["next"]["href"], format="json")
  349. assert response.status_code == 200
  350. assert len(response.data) == 1
  351. assert response.data[0]["id"] == str(group1.id)
  352. links = self._parse_links(response["Link"])
  353. assert links["previous"]["results"] == "true"
  354. assert links["next"]["results"] == "false"
  355. def test_stats_period(self):
  356. # TODO(dcramer): this test really only checks if validation happens
  357. # on groupStatsPeriod
  358. now = timezone.now()
  359. self.create_group(last_seen=now - timedelta(seconds=1))
  360. self.create_group(last_seen=now)
  361. self.login_as(user=self.user)
  362. self.get_success_response(groupStatsPeriod="24h")
  363. self.get_success_response(groupStatsPeriod="14d")
  364. self.get_success_response(groupStatsPeriod="")
  365. response = self.get_response(groupStatsPeriod="48h")
  366. assert response.status_code == 400
  367. def test_environment(self):
  368. self.store_event(
  369. data={
  370. "fingerprint": ["put-me-in-group1"],
  371. "timestamp": iso_format(self.min_ago),
  372. "environment": "production",
  373. },
  374. project_id=self.project.id,
  375. )
  376. self.store_event(
  377. data={
  378. "fingerprint": ["put-me-in-group2"],
  379. "timestamp": iso_format(self.min_ago),
  380. "environment": "staging",
  381. },
  382. project_id=self.project.id,
  383. )
  384. self.login_as(user=self.user)
  385. response = self.get_success_response(environment="production")
  386. assert len(response.data) == 1
  387. response = self.get_response(environment="garbage")
  388. assert response.status_code == 404
  389. def test_auto_resolved(self):
  390. project = self.project
  391. project.update_option("sentry:resolve_age", 1)
  392. self.store_event(
  393. data={"event_id": "a" * 32, "timestamp": iso_format(before_now(seconds=1))},
  394. project_id=project.id,
  395. )
  396. event2 = self.store_event(
  397. data={"event_id": "b" * 32, "timestamp": iso_format(before_now(seconds=1))},
  398. project_id=project.id,
  399. )
  400. group2 = event2.group
  401. self.login_as(user=self.user)
  402. response = self.get_success_response()
  403. assert len(response.data) == 1
  404. assert response.data[0]["id"] == str(group2.id)
  405. def test_lookup_by_event_id(self):
  406. project = self.project
  407. project.update_option("sentry:resolve_age", 1)
  408. event_id = "c" * 32
  409. event = self.store_event(
  410. data={"event_id": event_id, "timestamp": iso_format(self.min_ago)},
  411. project_id=self.project.id,
  412. )
  413. self.login_as(user=self.user)
  414. response = self.get_success_response(query="c" * 32)
  415. assert response["X-Sentry-Direct-Hit"] == "1"
  416. assert len(response.data) == 1
  417. assert response.data[0]["id"] == str(event.group.id)
  418. assert response.data[0]["matchingEventId"] == event_id
  419. def test_lookup_by_event_id_incorrect_project_id(self):
  420. self.store_event(
  421. data={"event_id": "a" * 32, "timestamp": iso_format(self.min_ago)},
  422. project_id=self.project.id,
  423. )
  424. event_id = "b" * 32
  425. event = self.store_event(
  426. data={"event_id": event_id, "timestamp": iso_format(self.min_ago)},
  427. project_id=self.project.id,
  428. )
  429. other_project = self.create_project(teams=[self.team])
  430. user = self.create_user()
  431. self.create_member(organization=self.organization, teams=[self.team], user=user)
  432. self.login_as(user=user)
  433. with self.feature("organizations:global-views"):
  434. response = self.get_success_response(query=event_id, project=[other_project.id])
  435. assert response["X-Sentry-Direct-Hit"] == "1"
  436. assert len(response.data) == 1
  437. assert response.data[0]["id"] == str(event.group.id)
  438. assert response.data[0]["matchingEventId"] == event_id
  439. def test_lookup_by_event_id_with_whitespace(self):
  440. project = self.project
  441. project.update_option("sentry:resolve_age", 1)
  442. event_id = "c" * 32
  443. event = self.store_event(
  444. data={"event_id": event_id, "timestamp": iso_format(self.min_ago)},
  445. project_id=self.project.id,
  446. )
  447. self.login_as(user=self.user)
  448. response = self.get_success_response(query=" {} ".format("c" * 32))
  449. assert response["X-Sentry-Direct-Hit"] == "1"
  450. assert len(response.data) == 1
  451. assert response.data[0]["id"] == str(event.group.id)
  452. assert response.data[0]["matchingEventId"] == event_id
  453. def test_lookup_by_unknown_event_id(self):
  454. project = self.project
  455. project.update_option("sentry:resolve_age", 1)
  456. self.create_group()
  457. self.create_group()
  458. self.login_as(user=self.user)
  459. response = self.get_success_response(query="c" * 32)
  460. assert len(response.data) == 0
  461. def test_lookup_by_short_id(self):
  462. group = self.group
  463. short_id = group.qualified_short_id
  464. self.login_as(user=self.user)
  465. response = self.get_success_response(query=short_id, shortIdLookup=1)
  466. assert len(response.data) == 1
  467. def test_lookup_by_short_id_ignores_project_list(self):
  468. organization = self.create_organization()
  469. project = self.create_project(organization=organization)
  470. project2 = self.create_project(organization=organization)
  471. group = self.create_group(project=project2)
  472. user = self.create_user()
  473. self.create_member(organization=organization, user=user)
  474. short_id = group.qualified_short_id
  475. self.login_as(user=user)
  476. response = self.get_success_response(
  477. organization.slug, project=project.id, query=short_id, shortIdLookup=1
  478. )
  479. assert len(response.data) == 1
  480. def test_lookup_by_short_id_no_perms(self):
  481. organization = self.create_organization()
  482. project = self.create_project(organization=organization)
  483. group = self.create_group(project=project)
  484. user = self.create_user()
  485. self.create_member(organization=organization, user=user, has_global_access=False)
  486. short_id = group.qualified_short_id
  487. self.login_as(user=user)
  488. response = self.get_success_response(organization.slug, query=short_id, shortIdLookup=1)
  489. assert len(response.data) == 0
  490. def test_lookup_by_group_id(self):
  491. self.login_as(user=self.user)
  492. response = self.get_success_response(group=self.group.id)
  493. assert len(response.data) == 1
  494. assert response.data[0]["id"] == str(self.group.id)
  495. group_2 = self.create_group()
  496. response = self.get_success_response(group=[self.group.id, group_2.id])
  497. assert {g["id"] for g in response.data} == {str(self.group.id), str(group_2.id)}
  498. def test_lookup_by_group_id_no_perms(self):
  499. organization = self.create_organization()
  500. project = self.create_project(organization=organization)
  501. group = self.create_group(project=project)
  502. user = self.create_user()
  503. self.create_member(organization=organization, user=user, has_global_access=False)
  504. self.login_as(user=user)
  505. response = self.get_response(group=[group.id])
  506. assert response.status_code == 403
  507. def test_lookup_by_first_release(self):
  508. self.login_as(self.user)
  509. project = self.project
  510. project2 = self.create_project(name="baz", organization=project.organization)
  511. release = Release.objects.create(organization=project.organization, version="12345")
  512. release.add_project(project)
  513. release.add_project(project2)
  514. event = self.store_event(
  515. data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))},
  516. project_id=project.id,
  517. )
  518. event2 = self.store_event(
  519. data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))},
  520. project_id=project2.id,
  521. )
  522. with self.feature("organizations:global-views"):
  523. response = self.get_success_response(
  524. **{"query": 'first-release:"%s"' % release.version}
  525. )
  526. issues = json.loads(response.content)
  527. assert len(issues) == 2
  528. assert int(issues[0]["id"]) == event2.group.id
  529. assert int(issues[1]["id"]) == event.group.id
  530. def test_lookup_by_release(self):
  531. self.login_as(self.user)
  532. project = self.project
  533. release = Release.objects.create(organization=project.organization, version="12345")
  534. release.add_project(project)
  535. event = self.store_event(
  536. data={
  537. "timestamp": iso_format(before_now(seconds=1)),
  538. "tags": {"sentry:release": release.version},
  539. },
  540. project_id=project.id,
  541. )
  542. response = self.get_success_response(release=release.version)
  543. issues = json.loads(response.content)
  544. assert len(issues) == 1
  545. assert int(issues[0]["id"]) == event.group.id
  546. def test_lookup_by_release_wildcard(self):
  547. self.login_as(self.user)
  548. project = self.project
  549. release = Release.objects.create(organization=project.organization, version="12345")
  550. release.add_project(project)
  551. event = self.store_event(
  552. data={
  553. "timestamp": iso_format(before_now(seconds=1)),
  554. "tags": {"sentry:release": release.version},
  555. },
  556. project_id=project.id,
  557. )
  558. response = self.get_success_response(release=release.version[:3] + "*")
  559. issues = json.loads(response.content)
  560. assert len(issues) == 1
  561. assert int(issues[0]["id"]) == event.group.id
  562. def test_lookup_by_regressed_in_release(self):
  563. self.login_as(self.user)
  564. project = self.project
  565. release = self.create_release()
  566. event = self.store_event(
  567. data={
  568. "timestamp": iso_format(before_now(seconds=1)),
  569. "tags": {"sentry:release": release.version},
  570. },
  571. project_id=project.id,
  572. )
  573. record_group_history(event.group, GroupHistoryStatus.REGRESSED, release=release)
  574. response = self.get_success_response(query=f"regressed_in_release:{release.version}")
  575. issues = json.loads(response.content)
  576. assert [int(issue["id"]) for issue in issues] == [event.group.id]
  577. def test_pending_delete_pending_merge_excluded(self):
  578. events = []
  579. for i in "abcd":
  580. events.append(
  581. self.store_event(
  582. data={
  583. "event_id": i * 32,
  584. "fingerprint": [i],
  585. "timestamp": iso_format(self.min_ago),
  586. },
  587. project_id=self.project.id,
  588. )
  589. )
  590. events[0].group.update(status=GroupStatus.PENDING_DELETION)
  591. events[2].group.update(status=GroupStatus.DELETION_IN_PROGRESS)
  592. events[3].group.update(status=GroupStatus.PENDING_MERGE)
  593. self.login_as(user=self.user)
  594. response = self.get_success_response()
  595. assert len(response.data) == 1
  596. assert response.data[0]["id"] == str(events[1].group.id)
  597. def test_filters_based_on_retention(self):
  598. self.login_as(user=self.user)
  599. self.create_group(last_seen=timezone.now() - timedelta(days=2))
  600. with self.options({"system.event-retention-days": 1}):
  601. response = self.get_success_response()
  602. assert len(response.data) == 0
  603. def test_token_auth(self):
  604. token = ApiToken.objects.create(user=self.user, scope_list=["event:read"])
  605. response = self.client.get(
  606. reverse("sentry-api-0-organization-group-index", args=[self.project.organization.slug]),
  607. format="json",
  608. HTTP_AUTHORIZATION=f"Bearer {token.token}",
  609. )
  610. assert response.status_code == 200, response.content
  611. def test_date_range(self):
  612. with self.options({"system.event-retention-days": 2}):
  613. event = self.store_event(
  614. data={"timestamp": iso_format(before_now(hours=5))}, project_id=self.project.id
  615. )
  616. group = event.group
  617. self.login_as(user=self.user)
  618. response = self.get_success_response(statsPeriod="6h")
  619. assert len(response.data) == 1
  620. assert response.data[0]["id"] == str(group.id)
  621. response = self.get_success_response(statsPeriod="1h")
  622. assert len(response.data) == 0
  623. @patch("sentry.analytics.record")
  624. def test_advanced_search_errors(self, mock_record):
  625. self.login_as(user=self.user)
  626. response = self.get_response(sort_by="date", query="!has:user")
  627. assert response.status_code == 200, response.data
  628. assert not any(
  629. c[0][0] == "advanced_search.feature_gated" for c in mock_record.call_args_list
  630. )
  631. with self.feature({"organizations:advanced-search": False}):
  632. response = self.get_response(sort_by="date", query="!has:user")
  633. assert response.status_code == 400, response.data
  634. assert (
  635. "You need access to the advanced search feature to use negative "
  636. "search" == response.data["detail"]
  637. )
  638. mock_record.assert_called_with(
  639. "advanced_search.feature_gated",
  640. user_id=self.user.id,
  641. default_user_id=self.user.id,
  642. organization_id=self.organization.id,
  643. )
  644. # This seems like a random override, but this test needed a way to override
  645. # the orderby being sent to snuba for a certain call. This function has a simple
  646. # return value and can be used to set variables in the snuba payload.
  647. @patch("sentry.utils.snuba.get_query_params_to_update_for_projects")
  648. def test_assigned_to_pagination(self, patched_params_update):
  649. old_sample_size = options.get("snuba.search.hits-sample-size")
  650. assert options.set("snuba.search.hits-sample-size", 1)
  651. days = reversed(range(4))
  652. self.login_as(user=self.user)
  653. groups = []
  654. for day in days:
  655. patched_params_update.side_effect = [
  656. (self.organization.id, {"project": [self.project.id]})
  657. ]
  658. group = self.store_event(
  659. data={
  660. "timestamp": iso_format(before_now(days=day)),
  661. "fingerprint": [f"group-{day}"],
  662. },
  663. project_id=self.project.id,
  664. ).group
  665. groups.append(group)
  666. assigned_groups = groups[:2]
  667. for ag in assigned_groups:
  668. ag.update(status=GroupStatus.RESOLVED, resolved_at=before_now(seconds=5))
  669. GroupAssignee.objects.assign(ag, self.user)
  670. # This side_effect is meant to override the `calculate_hits` snuba query specifically.
  671. # If this test is failing it's because the -last_seen override is being applied to
  672. # different snuba query.
  673. def _my_patched_params(query_params, **kwargs):
  674. if query_params.aggregations == [
  675. ["uniq", "group_id", "total"],
  676. ["multiply(toUInt64(max(timestamp)), 1000)", "", "last_seen"],
  677. ]:
  678. return (
  679. self.organization.id,
  680. {"project": [self.project.id], "orderby": ["-last_seen"]},
  681. )
  682. else:
  683. return (self.organization.id, {"project": [self.project.id]})
  684. patched_params_update.side_effect = _my_patched_params
  685. response = self.get_response(limit=1, query=f"assigned:{self.user.email}")
  686. assert len(response.data) == 1
  687. assert response.data[0]["id"] == str(assigned_groups[1].id)
  688. header_links = parse_link_header(response["Link"])
  689. cursor = [link for link in header_links.values() if link["rel"] == "next"][0]["cursor"]
  690. response = self.get_response(limit=1, cursor=cursor, query=f"assigned:{self.user.email}")
  691. assert len(response.data) == 1
  692. assert response.data[0]["id"] == str(assigned_groups[0].id)
  693. assert options.set("snuba.search.hits-sample-size", old_sample_size)
  694. def test_assigned_me_none(self):
  695. self.login_as(user=self.user)
  696. groups = []
  697. for i in range(5):
  698. group = self.store_event(
  699. data={
  700. "timestamp": iso_format(before_now(minutes=10, days=i)),
  701. "fingerprint": [f"group-{i}"],
  702. },
  703. project_id=self.project.id,
  704. ).group
  705. groups.append(group)
  706. assigned_groups = groups[:2]
  707. for ag in assigned_groups:
  708. GroupAssignee.objects.assign(ag, self.user)
  709. response = self.get_response(limit=10, query="assigned:me")
  710. assert [row["id"] for row in response.data] == [str(g.id) for g in assigned_groups]
  711. response = self.get_response(limit=10, query="assigned:[me, none]")
  712. assert len(response.data) == 5
  713. GroupAssignee.objects.assign(assigned_groups[1], self.create_user("other@user.com"))
  714. response = self.get_response(limit=10, query="assigned:[me, none]")
  715. assert len(response.data) == 4
  716. def test_seen_stats(self):
  717. self.store_event(
  718. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  719. project_id=self.project.id,
  720. )
  721. before_now_300_seconds = iso_format(before_now(seconds=300))
  722. before_now_350_seconds = iso_format(before_now(seconds=350))
  723. event2 = self.store_event(
  724. data={"timestamp": before_now_300_seconds, "fingerprint": ["group-2"]},
  725. project_id=self.project.id,
  726. )
  727. group2 = event2.group
  728. group2.first_seen = before_now_350_seconds
  729. group2.times_seen = 55
  730. group2.save()
  731. before_now_250_seconds = iso_format(before_now(seconds=250))
  732. self.store_event(
  733. data={
  734. "timestamp": before_now_250_seconds,
  735. "fingerprint": ["group-2"],
  736. "tags": {"server": "example.com", "trace": "meow", "message": "foo"},
  737. },
  738. project_id=self.project.id,
  739. )
  740. self.store_event(
  741. data={
  742. "timestamp": iso_format(before_now(seconds=200)),
  743. "fingerprint": ["group-1"],
  744. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  745. },
  746. project_id=self.project.id,
  747. )
  748. before_now_150_seconds = iso_format(before_now(seconds=150))
  749. self.store_event(
  750. data={
  751. "timestamp": before_now_150_seconds,
  752. "fingerprint": ["group-2"],
  753. "tags": {"trace": "ribbit", "server": "example.com"},
  754. },
  755. project_id=self.project.id,
  756. )
  757. before_now_100_seconds = iso_format(before_now(seconds=100))
  758. self.store_event(
  759. data={
  760. "timestamp": before_now_100_seconds,
  761. "fingerprint": ["group-2"],
  762. "tags": {"message": "foo", "trace": "meow"},
  763. },
  764. project_id=self.project.id,
  765. )
  766. self.login_as(user=self.user)
  767. response = self.get_response(sort_by="date", limit=10, query="server:example.com")
  768. assert response.status_code == 200
  769. assert len(response.data) == 2
  770. assert int(response.data[0]["id"]) == group2.id
  771. assert response.data[0]["lifetime"] is not None
  772. assert response.data[0]["filtered"] is not None
  773. assert response.data[0]["filtered"]["stats"] is not None
  774. assert response.data[0]["lifetime"]["stats"] is None
  775. assert response.data[0]["filtered"]["stats"] != response.data[0]["stats"]
  776. assert response.data[0]["lifetime"]["firstSeen"] == parse_datetime(
  777. before_now_350_seconds # Should match overridden value, not event value
  778. ).replace(tzinfo=timezone.utc)
  779. assert response.data[0]["lifetime"]["lastSeen"] == parse_datetime(
  780. before_now_100_seconds
  781. ).replace(tzinfo=timezone.utc)
  782. assert response.data[0]["lifetime"]["count"] == "55"
  783. assert response.data[0]["filtered"]["count"] == "2"
  784. assert response.data[0]["filtered"]["firstSeen"] == parse_datetime(
  785. before_now_250_seconds
  786. ).replace(tzinfo=timezone.utc)
  787. assert response.data[0]["filtered"]["lastSeen"] == parse_datetime(
  788. before_now_150_seconds
  789. ).replace(tzinfo=timezone.utc)
  790. # Empty filter test:
  791. response = self.get_response(sort_by="date", limit=10, query="")
  792. assert response.status_code == 200
  793. assert len(response.data) == 2
  794. assert int(response.data[0]["id"]) == group2.id
  795. assert response.data[0]["lifetime"] is not None
  796. assert response.data[0]["filtered"] is None
  797. assert response.data[0]["lifetime"]["stats"] is None
  798. assert response.data[0]["lifetime"]["count"] == "55"
  799. assert response.data[0]["lifetime"]["firstSeen"] == parse_datetime(
  800. before_now_350_seconds # Should match overridden value, not event value
  801. ).replace(tzinfo=timezone.utc)
  802. assert response.data[0]["lifetime"]["lastSeen"] == parse_datetime(
  803. before_now_100_seconds
  804. ).replace(tzinfo=timezone.utc)
  805. def test_semver_seen_stats(self):
  806. release_1 = self.create_release(version="test@1.2.3")
  807. release_2 = self.create_release(version="test@1.2.4")
  808. release_3 = self.create_release(version="test@1.2.5")
  809. release_1_e_1 = self.store_event(
  810. data={
  811. "timestamp": iso_format(before_now(minutes=5)),
  812. "fingerprint": ["group-1"],
  813. "release": release_1.version,
  814. },
  815. project_id=self.project.id,
  816. )
  817. group_1 = release_1_e_1.group
  818. release_2_e_1 = self.store_event(
  819. data={
  820. "timestamp": iso_format(before_now(minutes=3)),
  821. "fingerprint": ["group-1"],
  822. "release": release_2.version,
  823. },
  824. project_id=self.project.id,
  825. )
  826. release_3_e_1 = self.store_event(
  827. data={
  828. "timestamp": iso_format(before_now(minutes=1)),
  829. "fingerprint": ["group-1"],
  830. "release": release_3.version,
  831. },
  832. project_id=self.project.id,
  833. )
  834. group_1.update(times_seen=3)
  835. self.login_as(user=self.user)
  836. response = self.get_success_response(
  837. sort_by="date", limit=10, query="release.version:1.2.3"
  838. )
  839. assert [int(row["id"]) for row in response.data] == [group_1.id]
  840. group_data = response.data[0]
  841. assert group_data["lifetime"]["firstSeen"] == release_1_e_1.datetime
  842. assert group_data["filtered"]["firstSeen"] == release_1_e_1.datetime
  843. assert group_data["lifetime"]["lastSeen"] == release_3_e_1.datetime
  844. assert group_data["filtered"]["lastSeen"] == release_1_e_1.datetime
  845. assert int(group_data["lifetime"]["count"]) == 3
  846. assert int(group_data["filtered"]["count"]) == 1
  847. response = self.get_success_response(
  848. sort_by="date", limit=10, query="release.version:>=1.2.3"
  849. )
  850. assert [int(row["id"]) for row in response.data] == [group_1.id]
  851. group_data = response.data[0]
  852. assert group_data["lifetime"]["firstSeen"] == release_1_e_1.datetime
  853. assert group_data["filtered"]["firstSeen"] == release_1_e_1.datetime
  854. assert group_data["lifetime"]["lastSeen"] == release_3_e_1.datetime
  855. assert group_data["filtered"]["lastSeen"] == release_3_e_1.datetime
  856. assert int(group_data["lifetime"]["count"]) == 3
  857. assert int(group_data["filtered"]["count"]) == 3
  858. response = self.get_success_response(
  859. sort_by="date", limit=10, query="release.version:=1.2.4"
  860. )
  861. assert [int(row["id"]) for row in response.data] == [group_1.id]
  862. group_data = response.data[0]
  863. assert group_data["lifetime"]["firstSeen"] == release_1_e_1.datetime
  864. assert group_data["filtered"]["firstSeen"] == release_2_e_1.datetime
  865. assert group_data["lifetime"]["lastSeen"] == release_3_e_1.datetime
  866. assert group_data["filtered"]["lastSeen"] == release_2_e_1.datetime
  867. assert int(group_data["lifetime"]["count"]) == 3
  868. assert int(group_data["filtered"]["count"]) == 1
  869. def test_inbox_search(self):
  870. self.store_event(
  871. data={
  872. "timestamp": iso_format(before_now(seconds=200)),
  873. "fingerprint": ["group-1"],
  874. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  875. },
  876. project_id=self.project.id,
  877. )
  878. event = self.store_event(
  879. data={
  880. "timestamp": iso_format(before_now(seconds=200)),
  881. "fingerprint": ["group-2"],
  882. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  883. },
  884. project_id=self.project.id,
  885. )
  886. self.store_event(
  887. data={
  888. "timestamp": iso_format(before_now(seconds=200)),
  889. "fingerprint": ["group-3"],
  890. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  891. },
  892. project_id=self.project.id,
  893. )
  894. add_group_to_inbox(event.group, GroupInboxReason.NEW)
  895. self.login_as(user=self.user)
  896. response = self.get_response(
  897. sort_by="date", limit=10, query="is:unresolved is:for_review", expand=["inbox"]
  898. )
  899. assert response.status_code == 200
  900. assert len(response.data) == 1
  901. assert int(response.data[0]["id"]) == event.group.id
  902. assert response.data[0]["inbox"] is not None
  903. assert response.data[0]["inbox"]["reason"] == GroupInboxReason.NEW.value
  904. def test_inbox_search_outside_retention(self):
  905. self.login_as(user=self.user)
  906. response = self.get_response(
  907. sort="inbox",
  908. limit=10,
  909. query="is:unresolved is:for_review",
  910. collapse="stats",
  911. expand=["inbox", "owners"],
  912. start=iso_format(before_now(days=20)),
  913. end=iso_format(before_now(days=15)),
  914. )
  915. assert response.status_code == 200
  916. assert len(response.data) == 0
  917. def test_assigned_or_suggested_search(self):
  918. event = self.store_event(
  919. data={
  920. "timestamp": iso_format(before_now(seconds=180)),
  921. "fingerprint": ["group-1"],
  922. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  923. },
  924. project_id=self.project.id,
  925. )
  926. event1 = self.store_event(
  927. data={
  928. "timestamp": iso_format(before_now(seconds=185)),
  929. "fingerprint": ["group-2"],
  930. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  931. },
  932. project_id=self.project.id,
  933. )
  934. event2 = self.store_event(
  935. data={
  936. "timestamp": iso_format(before_now(seconds=190)),
  937. "fingerprint": ["group-3"],
  938. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  939. },
  940. project_id=self.project.id,
  941. )
  942. assigned_event = self.store_event(
  943. data={
  944. "timestamp": iso_format(before_now(seconds=195)),
  945. "fingerprint": ["group-4"],
  946. },
  947. project_id=self.project.id,
  948. )
  949. assigned_to_other_event = self.store_event(
  950. data={
  951. "timestamp": iso_format(before_now(seconds=195)),
  952. "fingerprint": ["group-5"],
  953. },
  954. project_id=self.project.id,
  955. )
  956. self.login_as(user=self.user)
  957. response = self.get_response(sort_by="date", limit=10, query="assigned_or_suggested:me")
  958. assert response.status_code == 200
  959. assert len(response.data) == 0
  960. GroupOwner.objects.create(
  961. group=assigned_to_other_event.group,
  962. project=assigned_to_other_event.group.project,
  963. organization=assigned_to_other_event.group.project.organization,
  964. type=0,
  965. team_id=None,
  966. user_id=self.user.id,
  967. )
  968. GroupOwner.objects.create(
  969. group=event.group,
  970. project=event.group.project,
  971. organization=event.group.project.organization,
  972. type=0,
  973. team_id=None,
  974. user_id=self.user.id,
  975. )
  976. response = self.get_response(sort_by="date", limit=10, query="assigned_or_suggested:me")
  977. assert response.status_code == 200
  978. assert len(response.data) == 2
  979. assert int(response.data[0]["id"]) == event.group.id
  980. assert int(response.data[1]["id"]) == assigned_to_other_event.group.id
  981. # Because assigned_to_other_event is assigned to self.other_user, it should not show up in assigned_or_suggested search for anyone but self.other_user. (aka. they are now the only owner)
  982. other_user = self.create_user("other@user.com", is_superuser=False)
  983. GroupAssignee.objects.create(
  984. group=assigned_to_other_event.group,
  985. project=assigned_to_other_event.group.project,
  986. user=other_user,
  987. )
  988. response = self.get_response(sort_by="date", limit=10, query="assigned_or_suggested:me")
  989. assert response.status_code == 200
  990. assert len(response.data) == 1
  991. assert int(response.data[0]["id"]) == event.group.id
  992. response = self.get_response(
  993. sort_by="date", limit=10, query=f"assigned_or_suggested:{other_user.email}"
  994. )
  995. assert response.status_code == 200
  996. assert len(response.data) == 1
  997. assert int(response.data[0]["id"]) == assigned_to_other_event.group.id
  998. GroupAssignee.objects.create(
  999. group=assigned_event.group, project=assigned_event.group.project, user=self.user
  1000. )
  1001. response = self.get_response(
  1002. sort_by="date", limit=10, query=f"assigned_or_suggested:{self.user.email}"
  1003. )
  1004. assert response.status_code == 200
  1005. assert len(response.data) == 2
  1006. assert int(response.data[0]["id"]) == event.group.id
  1007. assert int(response.data[1]["id"]) == assigned_event.group.id
  1008. response = self.get_response(
  1009. sort_by="date", limit=10, query=f"assigned_or_suggested:#{self.team.slug}"
  1010. )
  1011. assert response.status_code == 200
  1012. assert len(response.data) == 0
  1013. GroupOwner.objects.create(
  1014. group=event.group,
  1015. project=event.group.project,
  1016. organization=event.group.project.organization,
  1017. type=0,
  1018. team_id=self.team.id,
  1019. user_id=None,
  1020. )
  1021. response = self.get_response(
  1022. sort_by="date", limit=10, query=f"assigned_or_suggested:#{self.team.slug}"
  1023. )
  1024. assert response.status_code == 200
  1025. assert len(response.data) == 1
  1026. assert int(response.data[0]["id"]) == event.group.id
  1027. response = self.get_response(
  1028. sort_by="date", limit=10, query="assigned_or_suggested:[me, none]"
  1029. )
  1030. assert response.status_code == 200
  1031. assert len(response.data) == 4
  1032. assert int(response.data[0]["id"]) == event.group.id
  1033. assert int(response.data[1]["id"]) == event1.group.id
  1034. assert int(response.data[2]["id"]) == event2.group.id
  1035. assert int(response.data[3]["id"]) == assigned_event.group.id
  1036. not_me = self.create_user(email="notme@sentry.io")
  1037. GroupOwner.objects.create(
  1038. group=event2.group,
  1039. project=event2.group.project,
  1040. organization=event2.group.project.organization,
  1041. type=0,
  1042. team_id=None,
  1043. user_id=not_me.id,
  1044. )
  1045. response = self.get_response(
  1046. sort_by="date", limit=10, query="assigned_or_suggested:[me, none]"
  1047. )
  1048. assert response.status_code == 200
  1049. assert len(response.data) == 3
  1050. assert int(response.data[0]["id"]) == event.group.id
  1051. assert int(response.data[1]["id"]) == event1.group.id
  1052. assert int(response.data[2]["id"]) == assigned_event.group.id
  1053. GroupOwner.objects.create(
  1054. group=event2.group,
  1055. project=event2.group.project,
  1056. organization=event2.group.project.organization,
  1057. type=0,
  1058. team_id=None,
  1059. user_id=self.user.id,
  1060. )
  1061. # Should now include event2 as it has shared ownership.
  1062. response = self.get_response(
  1063. sort_by="date", limit=10, query="assigned_or_suggested:[me, none]"
  1064. )
  1065. assert response.status_code == 200
  1066. assert len(response.data) == 4
  1067. assert int(response.data[0]["id"]) == event.group.id
  1068. assert int(response.data[1]["id"]) == event1.group.id
  1069. assert int(response.data[2]["id"]) == event2.group.id
  1070. assert int(response.data[3]["id"]) == assigned_event.group.id
  1071. # Assign group to another user and now it shouldn't show up in owner search for this team.
  1072. GroupAssignee.objects.create(
  1073. group=event.group,
  1074. project=event.group.project,
  1075. user=other_user,
  1076. )
  1077. response = self.get_response(
  1078. sort_by="date", limit=10, query=f"assigned_or_suggested:#{self.team.slug}"
  1079. )
  1080. assert response.status_code == 200
  1081. assert len(response.data) == 0
  1082. def test_semver(self):
  1083. release_1 = self.create_release(version="test@1.2.3")
  1084. release_2 = self.create_release(version="test@1.2.4")
  1085. release_3 = self.create_release(version="test@1.2.5")
  1086. release_1_g_1 = self.store_event(
  1087. data={
  1088. "timestamp": iso_format(before_now(minutes=1)),
  1089. "fingerprint": ["group-1"],
  1090. "release": release_1.version,
  1091. },
  1092. project_id=self.project.id,
  1093. ).group.id
  1094. release_1_g_2 = self.store_event(
  1095. data={
  1096. "timestamp": iso_format(before_now(minutes=2)),
  1097. "fingerprint": ["group-2"],
  1098. "release": release_1.version,
  1099. },
  1100. project_id=self.project.id,
  1101. ).group.id
  1102. release_2_g_1 = self.store_event(
  1103. data={
  1104. "timestamp": iso_format(before_now(minutes=3)),
  1105. "fingerprint": ["group-3"],
  1106. "release": release_2.version,
  1107. },
  1108. project_id=self.project.id,
  1109. ).group.id
  1110. release_2_g_2 = self.store_event(
  1111. data={
  1112. "timestamp": iso_format(before_now(minutes=4)),
  1113. "fingerprint": ["group-4"],
  1114. "release": release_2.version,
  1115. },
  1116. project_id=self.project.id,
  1117. ).group.id
  1118. release_3_g_1 = self.store_event(
  1119. data={
  1120. "timestamp": iso_format(before_now(minutes=5)),
  1121. "fingerprint": ["group-5"],
  1122. "release": release_3.version,
  1123. },
  1124. project_id=self.project.id,
  1125. ).group.id
  1126. release_3_g_2 = self.store_event(
  1127. data={
  1128. "timestamp": iso_format(before_now(minutes=6)),
  1129. "fingerprint": ["group-6"],
  1130. "release": release_3.version,
  1131. },
  1132. project_id=self.project.id,
  1133. ).group.id
  1134. self.login_as(user=self.user)
  1135. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_ALIAS}:>1.2.3")
  1136. assert response.status_code == 200, response.content
  1137. assert [int(r["id"]) for r in response.json()] == [
  1138. release_2_g_1,
  1139. release_2_g_2,
  1140. release_3_g_1,
  1141. release_3_g_2,
  1142. ]
  1143. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_ALIAS}:>=1.2.3")
  1144. assert response.status_code == 200, response.content
  1145. assert [int(r["id"]) for r in response.json()] == [
  1146. release_1_g_1,
  1147. release_1_g_2,
  1148. release_2_g_1,
  1149. release_2_g_2,
  1150. release_3_g_1,
  1151. release_3_g_2,
  1152. ]
  1153. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_ALIAS}:<1.2.4")
  1154. assert response.status_code == 200, response.content
  1155. assert [int(r["id"]) for r in response.json()] == [release_1_g_1, release_1_g_2]
  1156. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_ALIAS}:<1.0")
  1157. assert response.status_code == 200, response.content
  1158. assert [int(r["id"]) for r in response.json()] == []
  1159. response = self.get_response(sort_by="date", limit=10, query=f"!{SEMVER_ALIAS}:1.2.4")
  1160. assert response.status_code == 200, response.content
  1161. assert [int(r["id"]) for r in response.json()] == [
  1162. release_1_g_1,
  1163. release_1_g_2,
  1164. release_3_g_1,
  1165. release_3_g_2,
  1166. ]
  1167. def test_release_stage(self):
  1168. replaced_release = self.create_release(
  1169. version="replaced_release",
  1170. environments=[self.environment],
  1171. adopted=timezone.now(),
  1172. unadopted=timezone.now(),
  1173. )
  1174. adopted_release = self.create_release(
  1175. version="adopted_release",
  1176. environments=[self.environment],
  1177. adopted=timezone.now(),
  1178. )
  1179. self.create_release(version="not_adopted_release", environments=[self.environment])
  1180. adopted_release_g_1 = self.store_event(
  1181. data={
  1182. "timestamp": iso_format(before_now(minutes=1)),
  1183. "fingerprint": ["group-1"],
  1184. "release": adopted_release.version,
  1185. "environment": self.environment.name,
  1186. },
  1187. project_id=self.project.id,
  1188. ).group.id
  1189. adopted_release_g_2 = self.store_event(
  1190. data={
  1191. "timestamp": iso_format(before_now(minutes=2)),
  1192. "fingerprint": ["group-2"],
  1193. "release": adopted_release.version,
  1194. "environment": self.environment.name,
  1195. },
  1196. project_id=self.project.id,
  1197. ).group.id
  1198. replaced_release_g_1 = self.store_event(
  1199. data={
  1200. "timestamp": iso_format(before_now(minutes=3)),
  1201. "fingerprint": ["group-3"],
  1202. "release": replaced_release.version,
  1203. "environment": self.environment.name,
  1204. },
  1205. project_id=self.project.id,
  1206. ).group.id
  1207. replaced_release_g_2 = self.store_event(
  1208. data={
  1209. "timestamp": iso_format(before_now(minutes=4)),
  1210. "fingerprint": ["group-4"],
  1211. "release": replaced_release.version,
  1212. "environment": self.environment.name,
  1213. },
  1214. project_id=self.project.id,
  1215. ).group.id
  1216. self.login_as(user=self.user)
  1217. response = self.get_response(
  1218. sort_by="date",
  1219. limit=10,
  1220. query=f"{RELEASE_STAGE_ALIAS}:{ReleaseStages.ADOPTED}",
  1221. environment=self.environment.name,
  1222. )
  1223. assert response.status_code == 200, response.content
  1224. assert [int(r["id"]) for r in response.json()] == [
  1225. adopted_release_g_1,
  1226. adopted_release_g_2,
  1227. ]
  1228. response = self.get_response(
  1229. sort_by="date",
  1230. limit=10,
  1231. query=f"!{RELEASE_STAGE_ALIAS}:{ReleaseStages.LOW_ADOPTION}",
  1232. environment=self.environment.name,
  1233. )
  1234. assert response.status_code == 200, response.content
  1235. assert [int(r["id"]) for r in response.json()] == [
  1236. adopted_release_g_1,
  1237. adopted_release_g_2,
  1238. replaced_release_g_1,
  1239. replaced_release_g_2,
  1240. ]
  1241. response = self.get_response(
  1242. sort_by="date",
  1243. limit=10,
  1244. query=f"{RELEASE_STAGE_ALIAS}:[{ReleaseStages.ADOPTED}, {ReleaseStages.REPLACED}]",
  1245. environment=self.environment.name,
  1246. )
  1247. assert response.status_code == 200, response.content
  1248. assert [int(r["id"]) for r in response.json()] == [
  1249. adopted_release_g_1,
  1250. adopted_release_g_2,
  1251. replaced_release_g_1,
  1252. replaced_release_g_2,
  1253. ]
  1254. response = self.get_response(
  1255. sort_by="date",
  1256. limit=10,
  1257. query=f"!{RELEASE_STAGE_ALIAS}:[{ReleaseStages.LOW_ADOPTION}, {ReleaseStages.REPLACED}]",
  1258. environment=self.environment.name,
  1259. )
  1260. assert response.status_code == 200, response.content
  1261. assert [int(r["id"]) for r in response.json()] == [
  1262. adopted_release_g_1,
  1263. adopted_release_g_2,
  1264. ]
  1265. def test_semver_package(self):
  1266. release_1 = self.create_release(version="test@1.2.3")
  1267. release_2 = self.create_release(version="test2@1.2.4")
  1268. release_1_g_1 = self.store_event(
  1269. data={
  1270. "timestamp": iso_format(before_now(minutes=1)),
  1271. "fingerprint": ["group-1"],
  1272. "release": release_1.version,
  1273. },
  1274. project_id=self.project.id,
  1275. ).group.id
  1276. release_1_g_2 = self.store_event(
  1277. data={
  1278. "timestamp": iso_format(before_now(minutes=2)),
  1279. "fingerprint": ["group-2"],
  1280. "release": release_1.version,
  1281. },
  1282. project_id=self.project.id,
  1283. ).group.id
  1284. release_2_g_1 = self.store_event(
  1285. data={
  1286. "timestamp": iso_format(before_now(minutes=3)),
  1287. "fingerprint": ["group-3"],
  1288. "release": release_2.version,
  1289. },
  1290. project_id=self.project.id,
  1291. ).group.id
  1292. self.login_as(user=self.user)
  1293. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_PACKAGE_ALIAS}:test")
  1294. assert response.status_code == 200, response.content
  1295. assert [int(r["id"]) for r in response.json()] == [
  1296. release_1_g_1,
  1297. release_1_g_2,
  1298. ]
  1299. response = self.get_response(
  1300. sort_by="date", limit=10, query=f"{SEMVER_PACKAGE_ALIAS}:test2"
  1301. )
  1302. assert response.status_code == 200, response.content
  1303. assert [int(r["id"]) for r in response.json()] == [
  1304. release_2_g_1,
  1305. ]
  1306. def test_semver_build(self):
  1307. release_1 = self.create_release(version="test@1.2.3+123")
  1308. release_2 = self.create_release(version="test2@1.2.4+124")
  1309. release_1_g_1 = self.store_event(
  1310. data={
  1311. "timestamp": iso_format(before_now(minutes=1)),
  1312. "fingerprint": ["group-1"],
  1313. "release": release_1.version,
  1314. },
  1315. project_id=self.project.id,
  1316. ).group.id
  1317. release_1_g_2 = self.store_event(
  1318. data={
  1319. "timestamp": iso_format(before_now(minutes=2)),
  1320. "fingerprint": ["group-2"],
  1321. "release": release_1.version,
  1322. },
  1323. project_id=self.project.id,
  1324. ).group.id
  1325. release_2_g_1 = self.store_event(
  1326. data={
  1327. "timestamp": iso_format(before_now(minutes=3)),
  1328. "fingerprint": ["group-3"],
  1329. "release": release_2.version,
  1330. },
  1331. project_id=self.project.id,
  1332. ).group.id
  1333. self.login_as(user=self.user)
  1334. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_BUILD_ALIAS}:123")
  1335. assert response.status_code == 200, response.content
  1336. assert [int(r["id"]) for r in response.json()] == [
  1337. release_1_g_1,
  1338. release_1_g_2,
  1339. ]
  1340. response = self.get_response(sort_by="date", limit=10, query=f"{SEMVER_BUILD_ALIAS}:124")
  1341. assert response.status_code == 200, response.content
  1342. assert [int(r["id"]) for r in response.json()] == [
  1343. release_2_g_1,
  1344. ]
  1345. def test_aggregate_stats_regression_test(self):
  1346. self.store_event(
  1347. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1348. project_id=self.project.id,
  1349. )
  1350. self.login_as(user=self.user)
  1351. response = self.get_response(
  1352. sort_by="date", limit=10, query="times_seen:>0 last_seen:-1h date:-1h"
  1353. )
  1354. assert response.status_code == 200
  1355. assert len(response.data) == 1
  1356. def test_skipped_fields(self):
  1357. event = self.store_event(
  1358. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1359. project_id=self.project.id,
  1360. )
  1361. self.store_event(
  1362. data={
  1363. "timestamp": iso_format(before_now(seconds=200)),
  1364. "fingerprint": ["group-1"],
  1365. "tags": {"server": "example.com", "trace": "woof", "message": "foo"},
  1366. },
  1367. project_id=self.project.id,
  1368. )
  1369. query = "server:example.com"
  1370. query += " status:unresolved"
  1371. query += " first_seen:" + iso_format(before_now(seconds=500))
  1372. self.login_as(user=self.user)
  1373. response = self.get_response(sort_by="date", limit=10, query=query)
  1374. assert response.status_code == 200
  1375. assert len(response.data) == 1
  1376. assert int(response.data[0]["id"]) == event.group.id
  1377. assert response.data[0]["lifetime"] is not None
  1378. assert response.data[0]["filtered"] is not None
  1379. def test_inbox_fields(self):
  1380. event = self.store_event(
  1381. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1382. project_id=self.project.id,
  1383. )
  1384. add_group_to_inbox(event.group, GroupInboxReason.NEW)
  1385. query = "status:unresolved"
  1386. self.login_as(user=self.user)
  1387. response = self.get_response(sort_by="date", limit=10, query=query, expand=["inbox"])
  1388. assert response.status_code == 200
  1389. assert len(response.data) == 1
  1390. assert int(response.data[0]["id"]) == event.group.id
  1391. assert response.data[0]["inbox"] is not None
  1392. assert response.data[0]["inbox"]["reason"] == GroupInboxReason.NEW.value
  1393. assert response.data[0]["inbox"]["reason_details"] is None
  1394. remove_group_from_inbox(event.group)
  1395. snooze_details = {
  1396. "until": None,
  1397. "count": 3,
  1398. "window": None,
  1399. "user_count": None,
  1400. "user_window": 5,
  1401. }
  1402. add_group_to_inbox(event.group, GroupInboxReason.UNIGNORED, snooze_details)
  1403. response = self.get_response(sort_by="date", limit=10, query=query, expand=["inbox"])
  1404. assert response.status_code == 200
  1405. assert len(response.data) == 1
  1406. assert int(response.data[0]["id"]) == event.group.id
  1407. assert response.data[0]["inbox"] is not None
  1408. assert response.data[0]["inbox"]["reason"] == GroupInboxReason.UNIGNORED.value
  1409. assert response.data[0]["inbox"]["reason_details"] == snooze_details
  1410. def test_expand_string(self):
  1411. event = self.store_event(
  1412. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1413. project_id=self.project.id,
  1414. )
  1415. add_group_to_inbox(event.group, GroupInboxReason.NEW)
  1416. query = "status:unresolved"
  1417. self.login_as(user=self.user)
  1418. response = self.get_response(sort_by="date", limit=10, query=query, expand="inbox")
  1419. assert response.status_code == 200
  1420. assert len(response.data) == 1
  1421. assert int(response.data[0]["id"]) == event.group.id
  1422. assert response.data[0]["inbox"] is not None
  1423. assert response.data[0]["inbox"]["reason"] == GroupInboxReason.NEW.value
  1424. assert response.data[0]["inbox"]["reason_details"] is None
  1425. def test_expand_owners(self):
  1426. event = self.store_event(
  1427. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1428. project_id=self.project.id,
  1429. )
  1430. query = "status:unresolved"
  1431. self.login_as(user=self.user)
  1432. # Test with no owner
  1433. response = self.get_response(sort_by="date", limit=10, query=query, expand="owners")
  1434. assert response.status_code == 200
  1435. assert len(response.data) == 1
  1436. assert int(response.data[0]["id"]) == event.group.id
  1437. assert response.data[0]["owners"] is None
  1438. # Test with owners
  1439. GroupOwner.objects.create(
  1440. group=event.group,
  1441. project=event.project,
  1442. organization=event.project.organization,
  1443. type=GroupOwnerType.SUSPECT_COMMIT.value,
  1444. user=self.user,
  1445. )
  1446. GroupOwner.objects.create(
  1447. group=event.group,
  1448. project=event.project,
  1449. organization=event.project.organization,
  1450. type=GroupOwnerType.OWNERSHIP_RULE.value,
  1451. team=self.team,
  1452. )
  1453. response = self.get_response(sort_by="date", limit=10, query=query, expand="owners")
  1454. assert response.status_code == 200
  1455. assert len(response.data) == 1
  1456. assert int(response.data[0]["id"]) == event.group.id
  1457. assert response.data[0]["owners"] is not None
  1458. assert len(response.data[0]["owners"]) == 2
  1459. assert response.data[0]["owners"][0]["owner"] == f"user:{self.user.id}"
  1460. assert response.data[0]["owners"][1]["owner"] == f"team:{self.team.id}"
  1461. assert (
  1462. response.data[0]["owners"][0]["type"] == GROUP_OWNER_TYPE[GroupOwnerType.SUSPECT_COMMIT]
  1463. )
  1464. assert (
  1465. response.data[0]["owners"][1]["type"] == GROUP_OWNER_TYPE[GroupOwnerType.OWNERSHIP_RULE]
  1466. )
  1467. @override_settings(SENTRY_SELF_HOSTED=False)
  1468. def test_ratelimit(self):
  1469. self.login_as(user=self.user)
  1470. with freeze_time("2000-01-01"):
  1471. for i in range(10):
  1472. self.get_success_response()
  1473. self.get_error_response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
  1474. def test_filter_not_unresolved(self):
  1475. event = self.store_event(
  1476. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1477. project_id=self.project.id,
  1478. )
  1479. event.group.update(status=GroupStatus.RESOLVED)
  1480. self.login_as(user=self.user)
  1481. response = self.get_response(
  1482. sort_by="date", limit=10, query="!is:unresolved", expand="inbox", collapse="stats"
  1483. )
  1484. assert response.status_code == 200
  1485. assert [int(r["id"]) for r in response.data] == [event.group.id]
  1486. def test_collapse_stats(self):
  1487. event = self.store_event(
  1488. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1489. project_id=self.project.id,
  1490. )
  1491. self.create_group(status=GroupStatus.UNRESOLVED)
  1492. self.login_as(user=self.user)
  1493. response = self.get_response(
  1494. sort_by="date", limit=10, query="is:unresolved", expand="inbox", collapse="stats"
  1495. )
  1496. assert response.status_code == 200
  1497. assert len(response.data) == 1
  1498. assert int(response.data[0]["id"]) == event.group.id
  1499. assert "stats" not in response.data[0]
  1500. assert "firstSeen" not in response.data[0]
  1501. assert "lastSeen" not in response.data[0]
  1502. assert "count" not in response.data[0]
  1503. assert "lifetime" not in response.data[0]
  1504. assert "filtered" not in response.data[0]
  1505. def test_collapse_lifetime(self):
  1506. event = self.store_event(
  1507. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1508. project_id=self.project.id,
  1509. )
  1510. self.create_group(status=GroupStatus.UNRESOLVED)
  1511. self.login_as(user=self.user)
  1512. response = self.get_response(
  1513. sort_by="date", limit=10, query="is:unresolved", collapse="lifetime"
  1514. )
  1515. assert response.status_code == 200
  1516. assert len(response.data) == 1
  1517. assert int(response.data[0]["id"]) == event.group.id
  1518. assert "stats" in response.data[0]
  1519. assert "firstSeen" in response.data[0]
  1520. assert "lastSeen" in response.data[0]
  1521. assert "count" in response.data[0]
  1522. assert "lifetime" not in response.data[0]
  1523. assert "filtered" in response.data[0]
  1524. def test_collapse_filtered(self):
  1525. event = self.store_event(
  1526. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1527. project_id=self.project.id,
  1528. )
  1529. self.create_group(status=GroupStatus.UNRESOLVED)
  1530. self.login_as(user=self.user)
  1531. response = self.get_response(
  1532. sort_by="date", limit=10, query="is:unresolved", collapse="filtered"
  1533. )
  1534. assert response.status_code == 200
  1535. assert len(response.data) == 1
  1536. assert int(response.data[0]["id"]) == event.group.id
  1537. assert "stats" in response.data[0]
  1538. assert "firstSeen" in response.data[0]
  1539. assert "lastSeen" in response.data[0]
  1540. assert "count" in response.data[0]
  1541. assert "lifetime" in response.data[0]
  1542. assert "filtered" not in response.data[0]
  1543. def test_collapse_lifetime_and_filtered(self):
  1544. event = self.store_event(
  1545. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1546. project_id=self.project.id,
  1547. )
  1548. self.create_group(status=GroupStatus.UNRESOLVED)
  1549. self.login_as(user=self.user)
  1550. response = self.get_response(
  1551. sort_by="date", limit=10, query="is:unresolved", collapse=["filtered", "lifetime"]
  1552. )
  1553. assert response.status_code == 200
  1554. assert len(response.data) == 1
  1555. assert int(response.data[0]["id"]) == event.group.id
  1556. assert "stats" in response.data[0]
  1557. assert "firstSeen" in response.data[0]
  1558. assert "lastSeen" in response.data[0]
  1559. assert "count" in response.data[0]
  1560. assert "lifetime" not in response.data[0]
  1561. assert "filtered" not in response.data[0]
  1562. def test_collapse_base(self):
  1563. event = self.store_event(
  1564. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1565. project_id=self.project.id,
  1566. )
  1567. self.create_group(status=GroupStatus.UNRESOLVED)
  1568. self.login_as(user=self.user)
  1569. response = self.get_response(
  1570. sort_by="date", limit=10, query="is:unresolved", collapse=["base"]
  1571. )
  1572. assert response.status_code == 200
  1573. assert len(response.data) == 1
  1574. assert int(response.data[0]["id"]) == event.group.id
  1575. assert "title" not in response.data[0]
  1576. assert "hasSeen" not in response.data[0]
  1577. assert "stats" in response.data[0]
  1578. assert "firstSeen" in response.data[0]
  1579. assert "lastSeen" in response.data[0]
  1580. assert "count" in response.data[0]
  1581. assert "lifetime" in response.data[0]
  1582. assert "filtered" in response.data[0]
  1583. def test_collapse_stats_group_snooze_bug(self):
  1584. # There was a bug where we tried to access attributes on seen_stats if this feature is active
  1585. # but seen_stats could be null when we collapse stats.
  1586. event = self.store_event(
  1587. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  1588. project_id=self.project.id,
  1589. )
  1590. GroupSnooze.objects.create(
  1591. group=event.group,
  1592. user_count=10,
  1593. until=timezone.now() + timedelta(days=1),
  1594. count=10,
  1595. state={"times_seen": 0},
  1596. )
  1597. self.login_as(user=self.user)
  1598. # The presence of the group above with attached GroupSnooze would have previously caused this error.
  1599. response = self.get_response(
  1600. sort_by="date", limit=10, query="is:unresolved", expand="inbox", collapse="stats"
  1601. )
  1602. assert response.status_code == 200
  1603. assert len(response.data) == 1
  1604. assert int(response.data[0]["id"]) == event.group.id
  1605. def test_only_release_committers(self):
  1606. release = self.create_release(project=self.project, version="1.0.0")
  1607. event = self.store_event(
  1608. data={
  1609. "timestamp": iso_format(before_now(seconds=500)),
  1610. "fingerprint": ["group-1"],
  1611. "release": release.version,
  1612. },
  1613. project_id=self.project.id,
  1614. )
  1615. repo = Repository.objects.create(
  1616. organization_id=self.project.organization_id, name=self.project.name
  1617. )
  1618. user2 = self.create_user()
  1619. self.create_member(organization=self.organization, user=user2)
  1620. author = self.create_commit_author(project=self.project, user=user2)
  1621. commit = Commit.objects.create(
  1622. organization_id=self.project.organization_id,
  1623. repository_id=repo.id,
  1624. key="a" * 40,
  1625. author=author,
  1626. )
  1627. commit2 = Commit.objects.create(
  1628. organization_id=self.project.organization_id,
  1629. repository_id=repo.id,
  1630. key="b" * 40,
  1631. author=author,
  1632. )
  1633. ReleaseCommit.objects.create(
  1634. organization_id=self.project.organization_id,
  1635. release=release,
  1636. commit=commit,
  1637. order=1,
  1638. )
  1639. ReleaseCommit.objects.create(
  1640. organization_id=self.project.organization_id,
  1641. release=release,
  1642. commit=commit2,
  1643. order=2,
  1644. )
  1645. query = "status:unresolved"
  1646. self.login_as(user=self.user)
  1647. response = self.get_response(sort_by="date", limit=10, query=query, expand="owners")
  1648. assert response.status_code == 200
  1649. assert len(response.data) == 1
  1650. assert int(response.data[0]["id"]) == event.group.id
  1651. owners = response.data[0]["owners"]
  1652. assert len(owners) == 1
  1653. assert owners[0]["owner"] == f"user:{user2.id}"
  1654. assert owners[0]["type"] == "releaseCommit"
  1655. def test_multiple_committers(self):
  1656. release = self.create_release(project=self.project, version="1.0.0")
  1657. event = self.store_event(
  1658. data={
  1659. "timestamp": iso_format(before_now(seconds=500)),
  1660. "fingerprint": ["group-1"],
  1661. "release": release.version,
  1662. },
  1663. project_id=self.project.id,
  1664. )
  1665. repo = Repository.objects.create(
  1666. organization_id=self.project.organization_id, name=self.project.name
  1667. )
  1668. user2 = self.create_user()
  1669. self.create_member(organization=self.organization, user=user2)
  1670. author = self.create_commit_author(project=self.project, user=user2)
  1671. commit = Commit.objects.create(
  1672. organization_id=self.project.organization_id,
  1673. repository_id=repo.id,
  1674. key="a" * 40,
  1675. author=author,
  1676. )
  1677. ReleaseCommit.objects.create(
  1678. organization_id=self.project.organization_id,
  1679. release=release,
  1680. commit=commit,
  1681. order=1,
  1682. )
  1683. query = "status:unresolved"
  1684. self.login_as(user=self.user)
  1685. # Test with owners
  1686. GroupOwner.objects.create(
  1687. group=event.group,
  1688. project=event.project,
  1689. organization=event.project.organization,
  1690. type=GroupOwnerType.SUSPECT_COMMIT.value,
  1691. user=self.user,
  1692. )
  1693. response = self.get_response(sort_by="date", limit=10, query=query, expand="owners")
  1694. assert response.status_code == 200
  1695. assert len(response.data) == 1
  1696. assert int(response.data[0]["id"]) == event.group.id
  1697. owners = response.data[0]["owners"]
  1698. assert len(owners) == 2
  1699. assert owners[0]["owner"] == f"user:{self.user.id}"
  1700. assert owners[0]["type"] == GROUP_OWNER_TYPE[GroupOwnerType.SUSPECT_COMMIT]
  1701. assert owners[1]["owner"] == f"user:{user2.id}"
  1702. assert owners[1]["type"] == "releaseCommit"
  1703. class GroupUpdateTest(APITestCase, SnubaTestCase):
  1704. endpoint = "sentry-api-0-organization-group-index"
  1705. method = "put"
  1706. def setUp(self):
  1707. super().setUp()
  1708. self.min_ago = timezone.now() - timedelta(minutes=1)
  1709. def get_response(self, *args, **kwargs):
  1710. if not args:
  1711. org = self.project.organization.slug
  1712. else:
  1713. org = args[0]
  1714. return super().get_response(org, **kwargs)
  1715. def assertNoResolution(self, group):
  1716. assert not GroupResolution.objects.filter(group=group).exists()
  1717. def test_global_resolve(self):
  1718. group1 = self.create_group(status=GroupStatus.RESOLVED)
  1719. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  1720. group3 = self.create_group(status=GroupStatus.IGNORED)
  1721. group4 = self.create_group(
  1722. project=self.create_project(slug="foo"),
  1723. status=GroupStatus.UNRESOLVED,
  1724. )
  1725. self.login_as(user=self.user)
  1726. response = self.get_success_response(
  1727. qs_params={"status": "unresolved", "project": self.project.id}, status="resolved"
  1728. )
  1729. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  1730. # the previously resolved entry should not be included
  1731. new_group1 = Group.objects.get(id=group1.id)
  1732. assert new_group1.status == GroupStatus.RESOLVED
  1733. assert new_group1.resolved_at is None
  1734. # this wont exist because it wasn't affected
  1735. assert not GroupSubscription.objects.filter(user=self.user, group=new_group1).exists()
  1736. new_group2 = Group.objects.get(id=group2.id)
  1737. assert new_group2.status == GroupStatus.RESOLVED
  1738. assert new_group2.resolved_at is not None
  1739. assert GroupSubscription.objects.filter(
  1740. user=self.user, group=new_group2, is_active=True
  1741. ).exists()
  1742. # the ignored entry should not be included
  1743. new_group3 = Group.objects.get(id=group3.id)
  1744. assert new_group3.status == GroupStatus.IGNORED
  1745. assert new_group3.resolved_at is None
  1746. assert not GroupSubscription.objects.filter(user=self.user, group=new_group3)
  1747. new_group4 = Group.objects.get(id=group4.id)
  1748. assert new_group4.status == GroupStatus.UNRESOLVED
  1749. assert new_group4.resolved_at is None
  1750. assert not GroupSubscription.objects.filter(user=self.user, group=new_group4)
  1751. assert not GroupHistory.objects.filter(
  1752. group=group1, status=GroupHistoryStatus.RESOLVED
  1753. ).exists()
  1754. assert GroupHistory.objects.filter(
  1755. group=group2, status=GroupHistoryStatus.RESOLVED
  1756. ).exists()
  1757. assert not GroupHistory.objects.filter(
  1758. group=group3, status=GroupHistoryStatus.RESOLVED
  1759. ).exists()
  1760. assert not GroupHistory.objects.filter(
  1761. group=group4, status=GroupHistoryStatus.RESOLVED
  1762. ).exists()
  1763. def test_resolve_member(self):
  1764. group = self.create_group(status=GroupStatus.UNRESOLVED)
  1765. member = self.create_user()
  1766. self.create_member(
  1767. organization=self.organization, teams=group.project.teams.all(), user=member
  1768. )
  1769. self.login_as(user=member)
  1770. response = self.get_success_response(
  1771. qs_params={"status": "unresolved", "project": self.project.id}, status="resolved"
  1772. )
  1773. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  1774. assert response.status_code == 200
  1775. def test_bulk_resolve(self):
  1776. self.login_as(user=self.user)
  1777. for i in range(200):
  1778. self.store_event(
  1779. data={
  1780. "fingerprint": [i],
  1781. "timestamp": iso_format(self.min_ago - timedelta(seconds=i)),
  1782. },
  1783. project_id=self.project.id,
  1784. )
  1785. response = self.get_success_response(query="is:unresolved", sort_by="date", method="get")
  1786. assert len(response.data) == 100
  1787. response = self.get_success_response(qs_params={"status": "unresolved"}, status="resolved")
  1788. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  1789. response = self.get_success_response(query="is:unresolved", sort_by="date", method="get")
  1790. assert len(response.data) == 0
  1791. @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
  1792. def test_resolve_with_integration(self, mock_sync_status_outbound):
  1793. self.login_as(user=self.user)
  1794. org = self.organization
  1795. integration = Integration.objects.create(provider="example", name="Example")
  1796. integration.add_organization(org, self.user)
  1797. event = self.store_event(
  1798. data={"timestamp": iso_format(self.min_ago)}, project_id=self.project.id
  1799. )
  1800. group = event.group
  1801. OrganizationIntegration.objects.filter(
  1802. integration_id=integration.id, organization_id=group.organization.id
  1803. ).update(
  1804. config={
  1805. "sync_comments": True,
  1806. "sync_status_outbound": True,
  1807. "sync_status_inbound": True,
  1808. "sync_assignee_outbound": True,
  1809. "sync_assignee_inbound": True,
  1810. }
  1811. )
  1812. external_issue = ExternalIssue.objects.get_or_create(
  1813. organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
  1814. )[0]
  1815. GroupLink.objects.get_or_create(
  1816. group_id=group.id,
  1817. project_id=group.project_id,
  1818. linked_type=GroupLink.LinkedType.issue,
  1819. linked_id=external_issue.id,
  1820. relationship=GroupLink.Relationship.references,
  1821. )[0]
  1822. response = self.get_success_response(sort_by="date", query="is:unresolved", method="get")
  1823. assert len(response.data) == 1
  1824. with self.tasks():
  1825. with self.feature({"organizations:integrations-issue-sync": True}):
  1826. response = self.get_success_response(
  1827. qs_params={"status": "unresolved"}, status="resolved"
  1828. )
  1829. group = Group.objects.get(id=group.id)
  1830. assert group.status == GroupStatus.RESOLVED
  1831. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  1832. mock_sync_status_outbound.assert_called_once_with(
  1833. external_issue, True, group.project_id
  1834. )
  1835. response = self.get_success_response(sort_by="date", query="is:unresolved", method="get")
  1836. assert len(response.data) == 0
  1837. @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
  1838. def test_set_unresolved_with_integration(self, mock_sync_status_outbound):
  1839. release = self.create_release(project=self.project, version="abc")
  1840. group = self.create_group(status=GroupStatus.RESOLVED)
  1841. org = self.organization
  1842. integration = Integration.objects.create(provider="example", name="Example")
  1843. integration.add_organization(org, self.user)
  1844. OrganizationIntegration.objects.filter(
  1845. integration_id=integration.id, organization_id=group.organization.id
  1846. ).update(
  1847. config={
  1848. "sync_comments": True,
  1849. "sync_status_outbound": True,
  1850. "sync_status_inbound": True,
  1851. "sync_assignee_outbound": True,
  1852. "sync_assignee_inbound": True,
  1853. }
  1854. )
  1855. GroupResolution.objects.create(group=group, release=release)
  1856. external_issue = ExternalIssue.objects.get_or_create(
  1857. organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
  1858. )[0]
  1859. GroupLink.objects.get_or_create(
  1860. group_id=group.id,
  1861. project_id=group.project_id,
  1862. linked_type=GroupLink.LinkedType.issue,
  1863. linked_id=external_issue.id,
  1864. relationship=GroupLink.Relationship.references,
  1865. )[0]
  1866. self.login_as(user=self.user)
  1867. with self.tasks():
  1868. with self.feature({"organizations:integrations-issue-sync": True}):
  1869. response = self.get_success_response(
  1870. qs_params={"id": group.id}, status="unresolved"
  1871. )
  1872. assert response.status_code == 200
  1873. assert response.data == {"status": "unresolved", "statusDetails": {}}
  1874. group = Group.objects.get(id=group.id)
  1875. assert group.status == GroupStatus.UNRESOLVED
  1876. self.assertNoResolution(group)
  1877. assert GroupSubscription.objects.filter(
  1878. user=self.user, group=group, is_active=True
  1879. ).exists()
  1880. mock_sync_status_outbound.assert_called_once_with(
  1881. external_issue, False, group.project_id
  1882. )
  1883. def test_self_assign_issue(self):
  1884. group = self.create_group(status=GroupStatus.UNRESOLVED)
  1885. user = self.user
  1886. uo1 = UserOption.objects.create(key="self_assign_issue", value="1", project=None, user=user)
  1887. self.login_as(user=user)
  1888. response = self.get_success_response(qs_params={"id": group.id}, status="resolved")
  1889. assert response.data["assignedTo"]["id"] == str(user.id)
  1890. assert response.data["assignedTo"]["type"] == "user"
  1891. assert response.data["status"] == "resolved"
  1892. assert GroupAssignee.objects.filter(group=group, user=user).exists()
  1893. assert GroupSubscription.objects.filter(user=user, group=group, is_active=True).exists()
  1894. uo1.delete()
  1895. def test_self_assign_issue_next_release(self):
  1896. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  1897. release.add_project(self.project)
  1898. group = self.create_group(status=GroupStatus.UNRESOLVED)
  1899. uo1 = UserOption.objects.create(
  1900. key="self_assign_issue", value="1", project=None, user=self.user
  1901. )
  1902. self.login_as(user=self.user)
  1903. response = self.get_success_response(
  1904. qs_params={"id": group.id}, status="resolvedInNextRelease"
  1905. )
  1906. assert response.data["status"] == "resolved"
  1907. assert response.data["statusDetails"]["inNextRelease"]
  1908. assert response.data["assignedTo"]["id"] == str(self.user.id)
  1909. assert response.data["assignedTo"]["type"] == "user"
  1910. group = Group.objects.get(id=group.id)
  1911. assert group.status == GroupStatus.RESOLVED
  1912. assert GroupResolution.objects.filter(group=group, release=release).exists()
  1913. assert GroupSubscription.objects.filter(
  1914. user=self.user, group=group, is_active=True
  1915. ).exists()
  1916. activity = Activity.objects.get(
  1917. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  1918. )
  1919. assert activity.data["version"] == ""
  1920. uo1.delete()
  1921. def test_in_semver_projects_group_resolution_stores_current_release_version(self):
  1922. """
  1923. Test that ensures that when we resolve a group in the next release, then
  1924. GroupResolution.current_release_version is set to the latest release associated with a
  1925. Group, when the project follows semantic versioning scheme
  1926. """
  1927. release_1 = self.create_release(version="fake_package@21.1.0")
  1928. release_2 = self.create_release(version="fake_package@21.1.1")
  1929. release_3 = self.create_release(version="fake_package@21.1.2")
  1930. self.store_event(
  1931. data={
  1932. "timestamp": iso_format(before_now(seconds=10)),
  1933. "fingerprint": ["group-1"],
  1934. "release": release_2.version,
  1935. },
  1936. project_id=self.project.id,
  1937. )
  1938. group = self.store_event(
  1939. data={
  1940. "timestamp": iso_format(before_now(seconds=12)),
  1941. "fingerprint": ["group-1"],
  1942. "release": release_1.version,
  1943. },
  1944. project_id=self.project.id,
  1945. ).group
  1946. self.login_as(user=self.user)
  1947. response = self.get_success_response(
  1948. qs_params={"id": group.id}, status="resolvedInNextRelease"
  1949. )
  1950. assert response.data["status"] == "resolved"
  1951. assert response.data["statusDetails"]["inNextRelease"]
  1952. # The current_release_version should be to the latest (in semver) release associated with
  1953. # a group
  1954. grp_resolution = GroupResolution.objects.filter(group=group)
  1955. assert len(grp_resolution) == 1
  1956. grp_resolution = grp_resolution.first()
  1957. assert grp_resolution.current_release_version == release_2.version
  1958. # "resolvedInNextRelease" with semver releases is considered as "resolvedInRelease"
  1959. assert grp_resolution.type == GroupResolution.Type.in_release
  1960. assert grp_resolution.status == GroupResolution.Status.resolved
  1961. # Add release that is between 2 and 3 to ensure that any release after release 2 should
  1962. # not have a resolution
  1963. release_4 = self.create_release(version="fake_package@21.1.1+1")
  1964. for release in [release_1, release_2]:
  1965. assert GroupResolution.has_resolution(group=group, release=release)
  1966. for release in [release_3, release_4]:
  1967. assert not GroupResolution.has_resolution(group=group, release=release)
  1968. # Ensure that Activity has `current_release_version` set on `Resolved in next release`
  1969. activity = Activity.objects.filter(
  1970. group=grp_resolution.group,
  1971. type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
  1972. ident=grp_resolution.id,
  1973. ).first()
  1974. assert "current_release_version" in activity.data
  1975. assert activity.data["current_release_version"] == release_2.version
  1976. def test_in_non_semver_projects_group_resolution_stores_current_release_version(self):
  1977. """
  1978. Test that ensures that when we resolve a group in the next release, then
  1979. GroupResolution.current_release_version is set to the most recent release associated with a
  1980. Group, when the project does not follow semantic versioning scheme
  1981. """
  1982. release_1 = self.create_release(
  1983. date_added=timezone.now() - timedelta(minutes=45), version="foobar 1"
  1984. )
  1985. release_2 = self.create_release(version="foobar 2")
  1986. group = self.store_event(
  1987. data={
  1988. "timestamp": iso_format(before_now(seconds=12)),
  1989. "fingerprint": ["group-1"],
  1990. "release": release_1.version,
  1991. },
  1992. project_id=self.project.id,
  1993. ).group
  1994. self.login_as(user=self.user)
  1995. response = self.get_success_response(
  1996. qs_params={"id": group.id}, status="resolvedInNextRelease"
  1997. )
  1998. assert response.data["status"] == "resolved"
  1999. assert response.data["statusDetails"]["inNextRelease"]
  2000. # Add a new release that is between 1 and 2, to make sure that if a the same issue/group
  2001. # occurs in that issue, then it should not have a resolution
  2002. release_3 = self.create_release(
  2003. date_added=timezone.now() - timedelta(minutes=30), version="foobar 3"
  2004. )
  2005. grp_resolution = GroupResolution.objects.filter(group=group)
  2006. assert len(grp_resolution) == 1
  2007. assert grp_resolution[0].current_release_version == release_1.version
  2008. assert GroupResolution.has_resolution(group=group, release=release_1)
  2009. for release in [release_2, release_3]:
  2010. assert not GroupResolution.has_resolution(group=group, release=release)
  2011. def test_in_non_semver_projects_store_actual_current_release_version_not_cached_version(self):
  2012. """
  2013. Test that ensures that the current_release_version is actually the latest version
  2014. associated with a group, not the cached version because currently
  2015. `group.get_last_release` fetches the latest release associated with a group and caches
  2016. that value, and we don't want to cache that value when resolving in next release in case a
  2017. new release appears to be associated with a group because if we store the cached rather
  2018. than the actual latest release, we might have unexpected results with the regression
  2019. algorithm
  2020. """
  2021. release_1 = self.create_release(
  2022. date_added=timezone.now() - timedelta(minutes=45), version="foobar 1"
  2023. )
  2024. release_2 = self.create_release(version="foobar 2")
  2025. group = self.store_event(
  2026. data={
  2027. "timestamp": iso_format(before_now(seconds=12)),
  2028. "fingerprint": ["group-1"],
  2029. "release": release_1.version,
  2030. },
  2031. project_id=self.project.id,
  2032. ).group
  2033. # Call this function to cache the `last_seen` release to release_1
  2034. # i.e. Set the first last observed by Sentry
  2035. assert group.get_last_release() == release_1.version
  2036. self.login_as(user=self.user)
  2037. self.store_event(
  2038. data={
  2039. "timestamp": iso_format(before_now(seconds=0)),
  2040. "fingerprint": ["group-1"],
  2041. "release": release_2.version,
  2042. },
  2043. project_id=self.project.id,
  2044. )
  2045. # Cached (i.e. first last observed release by Sentry) is returned here since `use_cache`
  2046. # is set to its default of `True`
  2047. assert Group.objects.get(id=group.id).get_last_release() == release_1.version
  2048. response = self.get_success_response(
  2049. qs_params={"id": group.id}, status="resolvedInNextRelease"
  2050. )
  2051. assert response.data["status"] == "resolved"
  2052. assert response.data["statusDetails"]["inNextRelease"]
  2053. # Changes here to release_2 and actual latest because `resolvedInNextRelease`,
  2054. # sets `use_cache` to False when fetching the last release associated with a group
  2055. assert Group.objects.get(id=group.id).get_last_release() == release_2.version
  2056. grp_resolution = GroupResolution.objects.filter(group=group)
  2057. assert len(grp_resolution) == 1
  2058. assert grp_resolution[0].current_release_version == release_2.version
  2059. def test_in_non_semver_projects_resolved_in_next_release_is_equated_to_in_release(self):
  2060. """
  2061. Test that ensures that if we basically know the next release when clicking on Resolved
  2062. In Next Release because that release exists, then we can short circuit setting
  2063. GroupResolution to type "inNextRelease", and then having `clear_expired_resolutions` run
  2064. once a new release is created to convert GroupResolution to in_release and set Activity.
  2065. Basically we treat "ResolvedInNextRelease" as "ResolvedInRelease" when there is a release
  2066. that was created after the last release associated with the group being resolved
  2067. """
  2068. release_1 = self.create_release(
  2069. date_added=timezone.now() - timedelta(minutes=45), version="foobar 1"
  2070. )
  2071. release_2 = self.create_release(version="foobar 2")
  2072. self.create_release(version="foobar 3")
  2073. group = self.store_event(
  2074. data={
  2075. "timestamp": iso_format(before_now(seconds=12)),
  2076. "fingerprint": ["group-1"],
  2077. "release": release_1.version,
  2078. },
  2079. project_id=self.project.id,
  2080. ).group
  2081. self.login_as(user=self.user)
  2082. response = self.get_success_response(
  2083. qs_params={"id": group.id}, status="resolvedInNextRelease"
  2084. )
  2085. assert response.data["status"] == "resolved"
  2086. assert response.data["statusDetails"]["inNextRelease"]
  2087. grp_resolution = GroupResolution.objects.filter(group=group)
  2088. assert len(grp_resolution) == 1
  2089. grp_resolution = grp_resolution[0]
  2090. assert grp_resolution.current_release_version == release_1.version
  2091. assert grp_resolution.release.id == release_2.id
  2092. assert grp_resolution.type == GroupResolution.Type.in_release
  2093. assert grp_resolution.status == GroupResolution.Status.resolved
  2094. activity = Activity.objects.filter(
  2095. group=grp_resolution.group,
  2096. type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
  2097. ident=grp_resolution.id,
  2098. ).first()
  2099. assert activity.data["version"] == release_2.version
  2100. def test_selective_status_update(self):
  2101. group1 = self.create_group(status=GroupStatus.RESOLVED)
  2102. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  2103. group3 = self.create_group(status=GroupStatus.IGNORED)
  2104. group4 = self.create_group(
  2105. project=self.create_project(slug="foo"),
  2106. status=GroupStatus.UNRESOLVED,
  2107. )
  2108. self.login_as(user=self.user)
  2109. with self.feature("organizations:global-views"):
  2110. response = self.get_success_response(
  2111. qs_params={"id": [group1.id, group2.id], "group4": group4.id}, status="resolved"
  2112. )
  2113. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  2114. new_group1 = Group.objects.get(id=group1.id)
  2115. assert new_group1.resolved_at is not None
  2116. assert new_group1.status == GroupStatus.RESOLVED
  2117. new_group2 = Group.objects.get(id=group2.id)
  2118. assert new_group2.resolved_at is not None
  2119. assert new_group2.status == GroupStatus.RESOLVED
  2120. assert GroupSubscription.objects.filter(
  2121. user=self.user, group=new_group2, is_active=True
  2122. ).exists()
  2123. new_group3 = Group.objects.get(id=group3.id)
  2124. assert new_group3.resolved_at is None
  2125. assert new_group3.status == GroupStatus.IGNORED
  2126. new_group4 = Group.objects.get(id=group4.id)
  2127. assert new_group4.resolved_at is None
  2128. assert new_group4.status == GroupStatus.UNRESOLVED
  2129. def test_set_resolved_in_current_release(self):
  2130. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  2131. release.add_project(self.project)
  2132. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2133. self.login_as(user=self.user)
  2134. response = self.get_success_response(
  2135. qs_params={"id": group.id}, status="resolved", statusDetails={"inRelease": "latest"}
  2136. )
  2137. assert response.data["status"] == "resolved"
  2138. assert response.data["statusDetails"]["inRelease"] == release.version
  2139. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2140. group = Group.objects.get(id=group.id)
  2141. assert group.status == GroupStatus.RESOLVED
  2142. resolution = GroupResolution.objects.get(group=group)
  2143. assert resolution.release == release
  2144. assert resolution.type == GroupResolution.Type.in_release
  2145. assert resolution.status == GroupResolution.Status.resolved
  2146. assert resolution.actor_id == self.user.id
  2147. assert GroupSubscription.objects.filter(
  2148. user=self.user, group=group, is_active=True
  2149. ).exists()
  2150. activity = Activity.objects.get(
  2151. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  2152. )
  2153. assert activity.data["version"] == release.version
  2154. assert GroupHistory.objects.filter(
  2155. group=group, status=GroupHistoryStatus.SET_RESOLVED_IN_RELEASE
  2156. ).exists()
  2157. def test_set_resolved_in_explicit_release(self):
  2158. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  2159. release.add_project(self.project)
  2160. release2 = Release.objects.create(organization_id=self.project.organization_id, version="b")
  2161. release2.add_project(self.project)
  2162. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2163. self.login_as(user=self.user)
  2164. response = self.get_success_response(
  2165. qs_params={"id": group.id},
  2166. status="resolved",
  2167. statusDetails={"inRelease": release.version},
  2168. )
  2169. assert response.data["status"] == "resolved"
  2170. assert response.data["statusDetails"]["inRelease"] == release.version
  2171. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2172. assert "activity" in response.data
  2173. group = Group.objects.get(id=group.id)
  2174. assert group.status == GroupStatus.RESOLVED
  2175. resolution = GroupResolution.objects.get(group=group)
  2176. assert resolution.release == release
  2177. assert resolution.type == GroupResolution.Type.in_release
  2178. assert resolution.status == GroupResolution.Status.resolved
  2179. assert resolution.actor_id == self.user.id
  2180. assert GroupSubscription.objects.filter(
  2181. user=self.user, group=group, is_active=True
  2182. ).exists()
  2183. activity = Activity.objects.get(
  2184. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  2185. )
  2186. assert activity.data["version"] == release.version
  2187. def test_in_semver_projects_set_resolved_in_explicit_release(self):
  2188. release_1 = self.create_release(version="fake_package@3.0.0")
  2189. release_2 = self.create_release(version="fake_package@2.0.0")
  2190. release_3 = self.create_release(version="fake_package@3.0.1")
  2191. group = self.store_event(
  2192. data={
  2193. "timestamp": iso_format(before_now(seconds=10)),
  2194. "fingerprint": ["group-1"],
  2195. "release": release_1.version,
  2196. },
  2197. project_id=self.project.id,
  2198. ).group
  2199. self.login_as(user=self.user)
  2200. response = self.get_success_response(
  2201. qs_params={"id": group.id},
  2202. status="resolved",
  2203. statusDetails={"inRelease": release_1.version},
  2204. )
  2205. assert response.data["status"] == "resolved"
  2206. assert response.data["statusDetails"]["inRelease"] == release_1.version
  2207. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2208. assert "activity" in response.data
  2209. group = Group.objects.get(id=group.id)
  2210. assert group.status == GroupStatus.RESOLVED
  2211. resolution = GroupResolution.objects.get(group=group)
  2212. assert resolution.release == release_1
  2213. assert resolution.type == GroupResolution.Type.in_release
  2214. assert resolution.status == GroupResolution.Status.resolved
  2215. assert resolution.actor_id == self.user.id
  2216. assert GroupSubscription.objects.filter(
  2217. user=self.user, group=group, is_active=True
  2218. ).exists()
  2219. activity = Activity.objects.get(
  2220. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  2221. )
  2222. assert activity.data["version"] == release_1.version
  2223. assert GroupResolution.has_resolution(group=group, release=release_2)
  2224. assert not GroupResolution.has_resolution(group=group, release=release_3)
  2225. def test_set_resolved_in_next_release(self):
  2226. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  2227. release.add_project(self.project)
  2228. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2229. self.login_as(user=self.user)
  2230. response = self.get_success_response(
  2231. qs_params={"id": group.id}, status="resolved", statusDetails={"inNextRelease": True}
  2232. )
  2233. assert response.data["status"] == "resolved"
  2234. assert response.data["statusDetails"]["inNextRelease"]
  2235. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2236. assert "activity" in response.data
  2237. group = Group.objects.get(id=group.id)
  2238. assert group.status == GroupStatus.RESOLVED
  2239. resolution = GroupResolution.objects.get(group=group)
  2240. assert resolution.release == release
  2241. assert resolution.type == GroupResolution.Type.in_next_release
  2242. assert resolution.status == GroupResolution.Status.pending
  2243. assert resolution.actor_id == self.user.id
  2244. assert GroupSubscription.objects.filter(
  2245. user=self.user, group=group, is_active=True
  2246. ).exists()
  2247. activity = Activity.objects.get(
  2248. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  2249. )
  2250. assert activity.data["version"] == ""
  2251. def test_set_resolved_in_next_release_legacy(self):
  2252. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  2253. release.add_project(self.project)
  2254. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2255. self.login_as(user=self.user)
  2256. response = self.get_success_response(
  2257. qs_params={"id": group.id}, status="resolvedInNextRelease"
  2258. )
  2259. assert response.data["status"] == "resolved"
  2260. assert response.data["statusDetails"]["inNextRelease"]
  2261. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2262. assert "activity" in response.data
  2263. group = Group.objects.get(id=group.id)
  2264. assert group.status == GroupStatus.RESOLVED
  2265. resolution = GroupResolution.objects.get(group=group)
  2266. assert resolution.release == release
  2267. assert resolution.type == GroupResolution.Type.in_next_release
  2268. assert resolution.status == GroupResolution.Status.pending
  2269. assert resolution.actor_id == self.user.id
  2270. assert GroupSubscription.objects.filter(
  2271. user=self.user, group=group, is_active=True
  2272. ).exists()
  2273. assert GroupHistory.objects.filter(
  2274. group=group, status=GroupHistoryStatus.SET_RESOLVED_IN_RELEASE
  2275. ).exists()
  2276. activity = Activity.objects.get(
  2277. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  2278. )
  2279. assert activity.data["version"] == ""
  2280. def test_set_resolved_in_explicit_commit_unreleased(self):
  2281. repo = self.create_repo(project=self.project, name=self.project.name)
  2282. commit = self.create_commit(project=self.project, repo=repo)
  2283. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2284. self.login_as(user=self.user)
  2285. response = self.get_success_response(
  2286. qs_params={"id": group.id},
  2287. status="resolved",
  2288. statusDetails={"inCommit": {"commit": commit.key, "repository": repo.name}},
  2289. )
  2290. assert response.data["status"] == "resolved"
  2291. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  2292. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2293. assert "activity" not in response.data
  2294. group = Group.objects.get(id=group.id)
  2295. assert group.status == GroupStatus.RESOLVED
  2296. link = GroupLink.objects.get(group_id=group.id)
  2297. assert link.linked_type == GroupLink.LinkedType.commit
  2298. assert link.relationship == GroupLink.Relationship.resolves
  2299. assert link.linked_id == commit.id
  2300. assert GroupSubscription.objects.filter(
  2301. user=self.user, group=group, is_active=True
  2302. ).exists()
  2303. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  2304. assert activity.data["commit"] == commit.id
  2305. assert GroupHistory.objects.filter(
  2306. group=group, status=GroupHistoryStatus.SET_RESOLVED_IN_COMMIT
  2307. ).exists()
  2308. def test_set_resolved_in_explicit_commit_released(self):
  2309. release = self.create_release(project=self.project)
  2310. repo = self.create_repo(project=self.project, name=self.project.name)
  2311. commit = self.create_commit(project=self.project, repo=repo, release=release)
  2312. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2313. self.login_as(user=self.user)
  2314. response = self.get_success_response(
  2315. qs_params={"id": group.id},
  2316. status="resolved",
  2317. statusDetails={"inCommit": {"commit": commit.key, "repository": repo.name}},
  2318. )
  2319. assert response.data["status"] == "resolved"
  2320. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  2321. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2322. assert "activity" in response.data
  2323. group = Group.objects.get(id=group.id)
  2324. assert group.status == GroupStatus.RESOLVED
  2325. link = GroupLink.objects.get(group_id=group.id)
  2326. assert link.project_id == self.project.id
  2327. assert link.linked_type == GroupLink.LinkedType.commit
  2328. assert link.relationship == GroupLink.Relationship.resolves
  2329. assert link.linked_id == commit.id
  2330. assert GroupSubscription.objects.filter(
  2331. user=self.user, group=group, is_active=True
  2332. ).exists()
  2333. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  2334. assert activity.data["commit"] == commit.id
  2335. resolution = GroupResolution.objects.get(group=group)
  2336. assert resolution.type == GroupResolution.Type.in_release
  2337. assert resolution.status == GroupResolution.Status.resolved
  2338. assert GroupHistory.objects.filter(
  2339. group=group, status=GroupHistoryStatus.SET_RESOLVED_IN_COMMIT
  2340. ).exists()
  2341. def test_set_resolved_in_explicit_commit_missing(self):
  2342. repo = self.create_repo(project=self.project, name=self.project.name)
  2343. group = self.create_group(status=GroupStatus.UNRESOLVED)
  2344. self.login_as(user=self.user)
  2345. response = self.get_response(
  2346. qs_params={"id": group.id},
  2347. status="resolved",
  2348. statusDetails={"inCommit": {"commit": "a" * 40, "repository": repo.name}},
  2349. )
  2350. assert response.status_code == 400
  2351. assert (
  2352. response.data["statusDetails"]["inCommit"]["commit"][0]
  2353. == "Unable to find the given commit."
  2354. )
  2355. assert not GroupHistory.objects.filter(
  2356. group=group, status=GroupHistoryStatus.SET_RESOLVED_IN_COMMIT
  2357. ).exists()
  2358. def test_set_unresolved(self):
  2359. release = self.create_release(project=self.project, version="abc")
  2360. group = self.create_group(status=GroupStatus.RESOLVED)
  2361. GroupResolution.objects.create(group=group, release=release)
  2362. self.login_as(user=self.user)
  2363. response = self.get_success_response(qs_params={"id": group.id}, status="unresolved")
  2364. assert response.data == {"status": "unresolved", "statusDetails": {}}
  2365. group = Group.objects.get(id=group.id)
  2366. assert group.status == GroupStatus.UNRESOLVED
  2367. assert GroupHistory.objects.filter(
  2368. group=group, status=GroupHistoryStatus.UNRESOLVED
  2369. ).exists()
  2370. self.assertNoResolution(group)
  2371. assert GroupSubscription.objects.filter(
  2372. user=self.user, group=group, is_active=True
  2373. ).exists()
  2374. def test_set_unresolved_on_snooze(self):
  2375. group = self.create_group(status=GroupStatus.IGNORED)
  2376. GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(days=1))
  2377. self.login_as(user=self.user)
  2378. response = self.get_success_response(qs_params={"id": group.id}, status="unresolved")
  2379. assert response.data == {"status": "unresolved", "statusDetails": {}}
  2380. group = Group.objects.get(id=group.id)
  2381. assert group.status == GroupStatus.UNRESOLVED
  2382. assert GroupHistory.objects.filter(
  2383. group=group, status=GroupHistoryStatus.UNRESOLVED
  2384. ).exists()
  2385. def test_basic_ignore(self):
  2386. group = self.create_group(status=GroupStatus.RESOLVED)
  2387. snooze = GroupSnooze.objects.create(group=group, until=timezone.now())
  2388. self.login_as(user=self.user)
  2389. assert not GroupHistory.objects.filter(
  2390. group=group, status=GroupHistoryStatus.IGNORED
  2391. ).exists()
  2392. response = self.get_success_response(qs_params={"id": group.id}, status="ignored")
  2393. # existing snooze objects should be cleaned up
  2394. assert not GroupSnooze.objects.filter(id=snooze.id).exists()
  2395. group = Group.objects.get(id=group.id)
  2396. assert group.status == GroupStatus.IGNORED
  2397. assert GroupHistory.objects.filter(group=group, status=GroupHistoryStatus.IGNORED).exists()
  2398. assert response.data == {"status": "ignored", "statusDetails": {}, "inbox": None}
  2399. def test_snooze_duration(self):
  2400. group = self.create_group(status=GroupStatus.RESOLVED)
  2401. self.login_as(user=self.user)
  2402. response = self.get_success_response(
  2403. qs_params={"id": group.id}, status="ignored", ignoreDuration=30
  2404. )
  2405. snooze = GroupSnooze.objects.get(group=group)
  2406. snooze.until = snooze.until
  2407. now = timezone.now()
  2408. assert snooze.count is None
  2409. assert snooze.until > now + timedelta(minutes=29)
  2410. assert snooze.until < now + timedelta(minutes=31)
  2411. assert snooze.user_count is None
  2412. assert snooze.user_window is None
  2413. assert snooze.window is None
  2414. response.data["statusDetails"]["ignoreUntil"] = response.data["statusDetails"][
  2415. "ignoreUntil"
  2416. ]
  2417. assert response.data["status"] == "ignored"
  2418. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  2419. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  2420. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  2421. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  2422. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  2423. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2424. def test_snooze_count(self):
  2425. group = self.create_group(status=GroupStatus.RESOLVED, times_seen=1)
  2426. self.login_as(user=self.user)
  2427. response = self.get_success_response(
  2428. qs_params={"id": group.id}, status="ignored", ignoreCount=100
  2429. )
  2430. snooze = GroupSnooze.objects.get(group=group)
  2431. assert snooze.count == 100
  2432. assert snooze.until is None
  2433. assert snooze.user_count is None
  2434. assert snooze.user_window is None
  2435. assert snooze.window is None
  2436. assert snooze.state["times_seen"] == 1
  2437. assert response.data["status"] == "ignored"
  2438. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  2439. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  2440. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  2441. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  2442. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  2443. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2444. def test_snooze_user_count(self):
  2445. event = {}
  2446. for i in range(10):
  2447. event = self.store_event(
  2448. data={
  2449. "fingerprint": ["put-me-in-group-1"],
  2450. "user": {"id": str(i)},
  2451. "timestamp": iso_format(self.min_ago + timedelta(seconds=i)),
  2452. },
  2453. project_id=self.project.id,
  2454. )
  2455. group = Group.objects.get(id=event.group.id)
  2456. group.status = GroupStatus.RESOLVED
  2457. group.save()
  2458. self.login_as(user=self.user)
  2459. response = self.get_success_response(
  2460. qs_params={"id": group.id}, status="ignored", ignoreUserCount=10
  2461. )
  2462. snooze = GroupSnooze.objects.get(group=group)
  2463. assert snooze.count is None
  2464. assert snooze.until is None
  2465. assert snooze.user_count == 10
  2466. assert snooze.user_window is None
  2467. assert snooze.window is None
  2468. assert snooze.state["users_seen"] == 10
  2469. assert response.data["status"] == "ignored"
  2470. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  2471. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  2472. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  2473. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  2474. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  2475. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  2476. def test_set_bookmarked(self):
  2477. group1 = self.create_group(status=GroupStatus.RESOLVED)
  2478. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  2479. group3 = self.create_group(status=GroupStatus.IGNORED)
  2480. group4 = self.create_group(
  2481. project=self.create_project(slug="foo"),
  2482. status=GroupStatus.UNRESOLVED,
  2483. )
  2484. self.login_as(user=self.user)
  2485. with self.feature("organizations:global-views"):
  2486. response = self.get_success_response(
  2487. qs_params={"id": [group1.id, group2.id], "group4": group4.id}, isBookmarked="true"
  2488. )
  2489. assert response.data == {"isBookmarked": True}
  2490. bookmark1 = GroupBookmark.objects.filter(group=group1, user=self.user)
  2491. assert bookmark1.exists()
  2492. assert GroupSubscription.objects.filter(
  2493. user=self.user, group=group1, is_active=True
  2494. ).exists()
  2495. bookmark2 = GroupBookmark.objects.filter(group=group2, user=self.user)
  2496. assert bookmark2.exists()
  2497. assert GroupSubscription.objects.filter(
  2498. user=self.user, group=group2, is_active=True
  2499. ).exists()
  2500. bookmark3 = GroupBookmark.objects.filter(group=group3, user=self.user)
  2501. assert not bookmark3.exists()
  2502. bookmark4 = GroupBookmark.objects.filter(group=group4, user=self.user)
  2503. assert not bookmark4.exists()
  2504. def test_subscription(self):
  2505. group1 = self.create_group()
  2506. group2 = self.create_group()
  2507. group3 = self.create_group()
  2508. group4 = self.create_group(project=self.create_project(slug="foo"))
  2509. self.login_as(user=self.user)
  2510. with self.feature("organizations:global-views"):
  2511. response = self.get_success_response(
  2512. qs_params={"id": [group1.id, group2.id], "group4": group4.id}, isSubscribed="true"
  2513. )
  2514. assert response.data == {"isSubscribed": True, "subscriptionDetails": {"reason": "unknown"}}
  2515. assert GroupSubscription.objects.filter(
  2516. group=group1, user=self.user, is_active=True
  2517. ).exists()
  2518. assert GroupSubscription.objects.filter(
  2519. group=group2, user=self.user, is_active=True
  2520. ).exists()
  2521. assert not GroupSubscription.objects.filter(group=group3, user=self.user).exists()
  2522. assert not GroupSubscription.objects.filter(group=group4, user=self.user).exists()
  2523. def test_set_public(self):
  2524. group1 = self.create_group()
  2525. group2 = self.create_group()
  2526. self.login_as(user=self.user)
  2527. response = self.get_success_response(
  2528. qs_params={"id": [group1.id, group2.id]}, isPublic="true"
  2529. )
  2530. assert response.data["isPublic"] is True
  2531. assert "shareId" in response.data
  2532. new_group1 = Group.objects.get(id=group1.id)
  2533. assert bool(new_group1.get_share_id())
  2534. new_group2 = Group.objects.get(id=group2.id)
  2535. assert bool(new_group2.get_share_id())
  2536. def test_set_private(self):
  2537. group1 = self.create_group()
  2538. group2 = self.create_group()
  2539. # Manually mark them as shared
  2540. for g in group1, group2:
  2541. GroupShare.objects.create(project_id=g.project_id, group=g)
  2542. assert bool(g.get_share_id())
  2543. self.login_as(user=self.user)
  2544. response = self.get_success_response(
  2545. qs_params={"id": [group1.id, group2.id]}, isPublic="false"
  2546. )
  2547. assert response.data == {"isPublic": False, "shareId": None}
  2548. new_group1 = Group.objects.get(id=group1.id)
  2549. assert not bool(new_group1.get_share_id())
  2550. new_group2 = Group.objects.get(id=group2.id)
  2551. assert not bool(new_group2.get_share_id())
  2552. def test_set_has_seen(self):
  2553. group1 = self.create_group(status=GroupStatus.RESOLVED)
  2554. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  2555. group3 = self.create_group(status=GroupStatus.IGNORED)
  2556. group4 = self.create_group(
  2557. project=self.create_project(slug="foo"),
  2558. status=GroupStatus.UNRESOLVED,
  2559. )
  2560. self.login_as(user=self.user)
  2561. with self.feature("organizations:global-views"):
  2562. response = self.get_success_response(
  2563. qs_params={"id": [group1.id, group2.id], "group4": group4.id}, hasSeen="true"
  2564. )
  2565. assert response.data == {"hasSeen": True}
  2566. r1 = GroupSeen.objects.filter(group=group1, user=self.user)
  2567. assert r1.exists()
  2568. r2 = GroupSeen.objects.filter(group=group2, user=self.user)
  2569. assert r2.exists()
  2570. r3 = GroupSeen.objects.filter(group=group3, user=self.user)
  2571. assert not r3.exists()
  2572. r4 = GroupSeen.objects.filter(group=group4, user=self.user)
  2573. assert not r4.exists()
  2574. @patch("sentry.api.helpers.group_index.update.uuid4")
  2575. @patch("sentry.api.helpers.group_index.update.merge_groups")
  2576. @patch("sentry.api.helpers.group_index.update.eventstream")
  2577. def test_merge(self, mock_eventstream, merge_groups, mock_uuid4):
  2578. eventstream_state = object()
  2579. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  2580. mock_uuid4.return_value = self.get_mock_uuid()
  2581. group1 = self.create_group(times_seen=1)
  2582. group2 = self.create_group(times_seen=50)
  2583. group3 = self.create_group(times_seen=2)
  2584. self.create_group()
  2585. self.login_as(user=self.user)
  2586. response = self.get_success_response(
  2587. qs_params={"id": [group1.id, group2.id, group3.id]}, merge="1"
  2588. )
  2589. assert response.data["merge"]["parent"] == str(group2.id)
  2590. assert sorted(response.data["merge"]["children"]) == sorted(
  2591. [str(group1.id), str(group3.id)]
  2592. )
  2593. mock_eventstream.start_merge.assert_called_once_with(
  2594. group1.project_id, [group3.id, group1.id], group2.id
  2595. )
  2596. assert len(merge_groups.mock_calls) == 1
  2597. merge_groups.delay.assert_any_call(
  2598. from_object_ids=[group3.id, group1.id],
  2599. to_object_id=group2.id,
  2600. transaction_id="abc123",
  2601. eventstream_state=eventstream_state,
  2602. )
  2603. def test_assign(self):
  2604. group1 = self.create_group(is_public=True)
  2605. group2 = self.create_group(is_public=True)
  2606. user = self.user
  2607. self.login_as(user=user)
  2608. response = self.get_success_response(qs_params={"id": group1.id}, assignedTo=user.username)
  2609. assert response.data["assignedTo"]["id"] == str(user.id)
  2610. assert response.data["assignedTo"]["type"] == "user"
  2611. assert GroupAssignee.objects.filter(group=group1, user=user).exists()
  2612. assert GroupHistory.objects.filter(
  2613. group=group1, status=GroupHistoryStatus.ASSIGNED
  2614. ).exists()
  2615. assert not GroupAssignee.objects.filter(group=group2, user=user).exists()
  2616. assert (
  2617. Activity.objects.filter(
  2618. group=group1, user=user, type=ActivityType.ASSIGNED.value
  2619. ).count()
  2620. == 1
  2621. )
  2622. assert GroupSubscription.objects.filter(user=user, group=group1, is_active=True).exists()
  2623. response = self.get_success_response(qs_params={"id": group1.id}, assignedTo="")
  2624. assert response.data["assignedTo"] is None
  2625. assert not GroupAssignee.objects.filter(group=group1, user=user).exists()
  2626. assert GroupHistory.objects.filter(
  2627. group=group1, status=GroupHistoryStatus.UNASSIGNED
  2628. ).exists()
  2629. def test_assign_non_member(self):
  2630. group = self.create_group(is_public=True)
  2631. member = self.user
  2632. non_member = self.create_user("bar@example.com")
  2633. self.login_as(user=member)
  2634. response = self.get_response(qs_params={"id": group.id}, assignedTo=non_member.username)
  2635. assert not GroupHistory.objects.filter(
  2636. group=group, status=GroupHistoryStatus.ASSIGNED
  2637. ).exists()
  2638. assert response.status_code == 400, response.content
  2639. def test_assign_team(self):
  2640. self.login_as(user=self.user)
  2641. group = self.create_group()
  2642. other_member = self.create_user("bar@example.com")
  2643. team = self.create_team(
  2644. organization=group.project.organization, members=[self.user, other_member]
  2645. )
  2646. group.project.add_team(team)
  2647. assert not GroupHistory.objects.filter(
  2648. group=group, status=GroupHistoryStatus.ASSIGNED
  2649. ).exists()
  2650. response = self.get_success_response(
  2651. qs_params={"id": group.id}, assignedTo=f"team:{team.id}"
  2652. )
  2653. assert response.data["assignedTo"]["id"] == str(team.id)
  2654. assert response.data["assignedTo"]["type"] == "team"
  2655. assert GroupHistory.objects.filter(group=group, status=GroupHistoryStatus.ASSIGNED).exists()
  2656. assert GroupAssignee.objects.filter(group=group, team=team).exists()
  2657. assert Activity.objects.filter(group=group, type=ActivityType.ASSIGNED.value).count() == 1
  2658. assert GroupSubscription.objects.filter(group=group, is_active=True).count() == 2
  2659. response = self.get_success_response(qs_params={"id": group.id}, assignedTo="")
  2660. assert response.data["assignedTo"] is None
  2661. assert GroupHistory.objects.filter(
  2662. group=group, status=GroupHistoryStatus.UNASSIGNED
  2663. ).exists()
  2664. def test_discard(self):
  2665. group1 = self.create_group(is_public=True)
  2666. group2 = self.create_group(is_public=True)
  2667. group_hash = GroupHash.objects.create(hash="x" * 32, project=group1.project, group=group1)
  2668. user = self.user
  2669. self.login_as(user=user)
  2670. with self.tasks():
  2671. with self.feature("projects:discard-groups"):
  2672. response = self.get_response(qs_params={"id": group1.id}, discard=True)
  2673. assert response.status_code == 204
  2674. assert not Group.objects.filter(id=group1.id).exists()
  2675. assert Group.objects.filter(id=group2.id).exists()
  2676. assert GroupHash.objects.filter(id=group_hash.id).exists()
  2677. tombstone = GroupTombstone.objects.get(
  2678. id=GroupHash.objects.get(id=group_hash.id).group_tombstone_id
  2679. )
  2680. assert tombstone.message == group1.message
  2681. assert tombstone.culprit == group1.culprit
  2682. assert tombstone.project == group1.project
  2683. assert tombstone.data == group1.data
  2684. @override_settings(SENTRY_SELF_HOSTED=False)
  2685. def test_ratelimit(self):
  2686. self.login_as(user=self.user)
  2687. with freeze_time("2000-01-01"):
  2688. for i in range(5):
  2689. self.get_success_response()
  2690. self.get_error_response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
  2691. def test_set_inbox(self):
  2692. group1 = self.create_group()
  2693. group2 = self.create_group()
  2694. self.login_as(user=self.user)
  2695. response = self.get_success_response(qs_params={"id": [group1.id, group2.id]}, inbox="true")
  2696. assert response.data == {"inbox": True}
  2697. assert GroupInbox.objects.filter(group=group1).exists()
  2698. assert GroupInbox.objects.filter(group=group2).exists()
  2699. assert not GroupHistory.objects.filter(
  2700. group=group1, status=GroupHistoryStatus.REVIEWED
  2701. ).exists()
  2702. assert not GroupHistory.objects.filter(
  2703. group=group2, status=GroupHistoryStatus.REVIEWED
  2704. ).exists()
  2705. response = self.get_success_response(qs_params={"id": [group2.id]}, inbox="false")
  2706. assert response.data == {"inbox": False}
  2707. assert GroupInbox.objects.filter(group=group1).exists()
  2708. assert not GroupHistory.objects.filter(
  2709. group=group1, status=GroupHistoryStatus.REVIEWED
  2710. ).exists()
  2711. assert GroupHistory.objects.filter(
  2712. group=group2, status=GroupHistoryStatus.REVIEWED
  2713. ).exists()
  2714. assert not GroupInbox.objects.filter(group=group2).exists()
  2715. def test_set_resolved_inbox(self):
  2716. group1 = self.create_group()
  2717. group2 = self.create_group()
  2718. self.login_as(user=self.user)
  2719. response = self.get_success_response(
  2720. qs_params={"id": [group1.id, group2.id]}, status="resolved"
  2721. )
  2722. assert response.data["inbox"] is None
  2723. assert not GroupInbox.objects.filter(group=group1).exists()
  2724. assert not GroupInbox.objects.filter(group=group2).exists()
  2725. self.get_success_response(qs_params={"id": [group2.id]}, status="unresolved")
  2726. assert not GroupInbox.objects.filter(group=group1).exists()
  2727. assert not GroupInbox.objects.filter(group=group2).exists()
  2728. assert not GroupHistory.objects.filter(
  2729. group=group1, status=GroupHistoryStatus.UNRESOLVED
  2730. ).exists()
  2731. assert GroupHistory.objects.filter(
  2732. group=group2, status=GroupHistoryStatus.UNRESOLVED
  2733. ).exists()
  2734. class GroupDeleteTest(APITestCase, SnubaTestCase):
  2735. endpoint = "sentry-api-0-organization-group-index"
  2736. method = "delete"
  2737. def get_response(self, *args, **kwargs):
  2738. if not args:
  2739. org = self.project.organization.slug
  2740. else:
  2741. org = args[0]
  2742. return super().get_response(org, **kwargs)
  2743. @patch("sentry.api.helpers.group_index.delete.eventstream")
  2744. @patch("sentry.eventstream")
  2745. def test_delete_by_id(self, mock_eventstream_task, mock_eventstream_api):
  2746. eventstream_state = {"event_stream_state": uuid4()}
  2747. mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
  2748. group1 = self.create_group(status=GroupStatus.RESOLVED)
  2749. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  2750. group3 = self.create_group(status=GroupStatus.IGNORED)
  2751. group4 = self.create_group(
  2752. project=self.create_project(slug="foo"),
  2753. status=GroupStatus.UNRESOLVED,
  2754. )
  2755. hashes = []
  2756. for g in group1, group2, group3, group4:
  2757. hash = uuid4().hex
  2758. hashes.append(hash)
  2759. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  2760. self.login_as(user=self.user)
  2761. with self.feature("organizations:global-views"):
  2762. response = self.get_response(
  2763. qs_params={"id": [group1.id, group2.id], "group4": group4.id}
  2764. )
  2765. mock_eventstream_api.start_delete_groups.assert_called_once_with(
  2766. group1.project_id, [group1.id, group2.id]
  2767. )
  2768. assert response.status_code == 204
  2769. assert Group.objects.get(id=group1.id).status == GroupStatus.PENDING_DELETION
  2770. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  2771. assert Group.objects.get(id=group2.id).status == GroupStatus.PENDING_DELETION
  2772. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  2773. assert Group.objects.get(id=group3.id).status != GroupStatus.PENDING_DELETION
  2774. assert GroupHash.objects.filter(group_id=group3.id).exists()
  2775. assert Group.objects.get(id=group4.id).status != GroupStatus.PENDING_DELETION
  2776. assert GroupHash.objects.filter(group_id=group4.id).exists()
  2777. Group.objects.filter(id__in=(group1.id, group2.id)).update(status=GroupStatus.UNRESOLVED)
  2778. with self.tasks():
  2779. with self.feature("organizations:global-views"):
  2780. response = self.get_response(
  2781. qs_params={"id": [group1.id, group2.id], "group4": group4.id}
  2782. )
  2783. mock_eventstream_task.end_delete_groups.assert_called_once_with(eventstream_state)
  2784. assert response.status_code == 204
  2785. assert not Group.objects.filter(id=group1.id).exists()
  2786. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  2787. assert not Group.objects.filter(id=group2.id).exists()
  2788. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  2789. assert Group.objects.filter(id=group3.id).exists()
  2790. assert GroupHash.objects.filter(group_id=group3.id).exists()
  2791. assert Group.objects.filter(id=group4.id).exists()
  2792. assert GroupHash.objects.filter(group_id=group4.id).exists()
  2793. def test_bulk_delete(self):
  2794. groups = []
  2795. for i in range(10, 41):
  2796. groups.append(
  2797. self.create_group(
  2798. project=self.project,
  2799. status=GroupStatus.RESOLVED,
  2800. )
  2801. )
  2802. hashes = []
  2803. for group in groups:
  2804. hash = uuid4().hex
  2805. hashes.append(hash)
  2806. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  2807. self.login_as(user=self.user)
  2808. # if query is '' it defaults to is:unresolved
  2809. response = self.get_response(qs_params={"query": ""})
  2810. assert response.status_code == 204
  2811. for group in groups:
  2812. assert Group.objects.get(id=group.id).status == GroupStatus.PENDING_DELETION
  2813. assert not GroupHash.objects.filter(group_id=group.id).exists()
  2814. Group.objects.filter(id__in=[group.id for group in groups]).update(
  2815. status=GroupStatus.UNRESOLVED
  2816. )
  2817. with self.tasks():
  2818. response = self.get_response(qs_params={"query": ""})
  2819. assert response.status_code == 204
  2820. for group in groups:
  2821. assert not Group.objects.filter(id=group.id).exists()
  2822. assert not GroupHash.objects.filter(group_id=group.id).exists()
  2823. @override_settings(SENTRY_SELF_HOSTED=False)
  2824. def test_ratelimit(self):
  2825. self.login_as(user=self.user)
  2826. with freeze_time("2000-01-01"):
  2827. for i in range(5):
  2828. self.get_success_response()
  2829. self.get_error_response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)