test_organization_group_index.py 150 KB

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