test_organization_group_index.py 140 KB

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