test_organization_group_index.py 126 KB

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