test_organization_group_index.py 146 KB

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