test_organization_group_index.py 142 KB

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