test_organization_group_index.py 133 KB

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