test_organization_group_index.py 134 KB

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