test_organization_group_index.py 140 KB

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