test_organization_group_index.py 126 KB

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