test_organization_group_index.py 134 KB

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