test_organization_events_stats.py 129 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445
  1. from __future__ import annotations
  2. import uuid
  3. from datetime import timedelta
  4. from typing import Any, TypedDict
  5. from unittest import mock
  6. from uuid import uuid4
  7. import pytest
  8. from dateutil.parser import parse as parse_date
  9. from django.urls import reverse
  10. from snuba_sdk import Entity
  11. from snuba_sdk.column import Column
  12. from snuba_sdk.conditions import Condition, Op
  13. from snuba_sdk.function import Function
  14. from sentry.constants import MAX_TOP_EVENTS
  15. from sentry.issues.grouptype import ProfileFileIOGroupType
  16. from sentry.models.project import Project
  17. from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric
  18. from sentry.snuba.discover import OTHER_KEY
  19. from sentry.testutils.cases import APITestCase, ProfilesSnubaTestCase, SnubaTestCase
  20. from sentry.testutils.helpers.datetime import before_now
  21. from sentry.utils.samples import load_data
  22. from tests.sentry.issues.test_utils import SearchIssueTestMixin
  23. pytestmark = pytest.mark.sentry_metrics
  24. class _EventDataDict(TypedDict):
  25. data: dict[str, Any]
  26. project: Project
  27. count: int
  28. class OrganizationEventsStatsEndpointTest(APITestCase, SnubaTestCase, SearchIssueTestMixin):
  29. endpoint = "sentry-api-0-organization-events-stats"
  30. def setUp(self):
  31. super().setUp()
  32. self.login_as(user=self.user)
  33. self.authed_user = self.user
  34. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  35. self.project = self.create_project()
  36. self.project2 = self.create_project()
  37. self.user = self.create_user()
  38. self.user2 = self.create_user()
  39. self.store_event(
  40. data={
  41. "event_id": "a" * 32,
  42. "message": "very bad",
  43. "timestamp": (self.day_ago + timedelta(minutes=1)).isoformat(),
  44. "fingerprint": ["group1"],
  45. "tags": {"sentry:user": self.user.email},
  46. },
  47. project_id=self.project.id,
  48. )
  49. self.store_event(
  50. data={
  51. "event_id": "b" * 32,
  52. "message": "oh my",
  53. "timestamp": (self.day_ago + timedelta(hours=1, minutes=1)).isoformat(),
  54. "fingerprint": ["group2"],
  55. "tags": {"sentry:user": self.user2.email},
  56. },
  57. project_id=self.project2.id,
  58. )
  59. self.store_event(
  60. data={
  61. "event_id": "c" * 32,
  62. "message": "very bad",
  63. "timestamp": (self.day_ago + timedelta(hours=1, minutes=2)).isoformat(),
  64. "fingerprint": ["group2"],
  65. "tags": {"sentry:user": self.user2.email},
  66. },
  67. project_id=self.project2.id,
  68. )
  69. self.url = reverse(
  70. "sentry-api-0-organization-events-stats",
  71. kwargs={"organization_id_or_slug": self.project.organization.slug},
  72. )
  73. self.features = {}
  74. def do_request(self, data, url=None, features=None):
  75. if features is None:
  76. features = {"organizations:discover-basic": True}
  77. features.update(self.features)
  78. with self.feature(features):
  79. return self.client.get(self.url if url is None else url, data=data, format="json")
  80. @pytest.mark.querybuilder
  81. def test_simple(self):
  82. response = self.do_request(
  83. {
  84. "start": self.day_ago,
  85. "end": self.day_ago + timedelta(hours=2),
  86. "interval": "1h",
  87. },
  88. )
  89. assert response.status_code == 200, response.content
  90. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  91. def test_generic_issue(self):
  92. _, _, group_info = self.store_search_issue(
  93. self.project.id,
  94. self.user.id,
  95. [f"{ProfileFileIOGroupType.type_id}-group1"],
  96. "prod",
  97. self.day_ago,
  98. )
  99. assert group_info is not None
  100. self.store_search_issue(
  101. self.project.id,
  102. self.user.id,
  103. [f"{ProfileFileIOGroupType.type_id}-group1"],
  104. "prod",
  105. self.day_ago + timedelta(hours=1, minutes=1),
  106. )
  107. self.store_search_issue(
  108. self.project.id,
  109. self.user.id,
  110. [f"{ProfileFileIOGroupType.type_id}-group1"],
  111. "prod",
  112. self.day_ago + timedelta(hours=1, minutes=2),
  113. )
  114. with self.feature(
  115. [
  116. "organizations:profiling",
  117. ]
  118. ):
  119. response = self.do_request(
  120. {
  121. "start": self.day_ago,
  122. "end": self.day_ago + timedelta(hours=2),
  123. "interval": "1h",
  124. "query": f"issue:{group_info.group.qualified_short_id}",
  125. "dataset": "issuePlatform",
  126. },
  127. )
  128. assert response.status_code == 200, response.content
  129. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  130. def test_generic_issue_calculated_interval(self):
  131. """Test that a 4h interval returns the correct generic event stats.
  132. This follows a different code path than 1h or 1d as the IssuePlatformTimeSeriesQueryBuilder
  133. does some calculation to create the time column."""
  134. _, _, group_info = self.store_search_issue(
  135. self.project.id,
  136. self.user.id,
  137. [f"{ProfileFileIOGroupType.type_id}-group1"],
  138. "prod",
  139. self.day_ago + timedelta(minutes=1),
  140. )
  141. assert group_info is not None
  142. self.store_search_issue(
  143. self.project.id,
  144. self.user.id,
  145. [f"{ProfileFileIOGroupType.type_id}-group1"],
  146. "prod",
  147. self.day_ago + timedelta(minutes=1),
  148. )
  149. self.store_search_issue(
  150. self.project.id,
  151. self.user.id,
  152. [f"{ProfileFileIOGroupType.type_id}-group1"],
  153. "prod",
  154. self.day_ago + timedelta(minutes=2),
  155. )
  156. with self.feature(
  157. [
  158. "organizations:profiling",
  159. ]
  160. ):
  161. response = self.do_request(
  162. {
  163. "start": self.day_ago,
  164. "end": self.day_ago + timedelta(hours=4),
  165. "interval": "4h",
  166. "query": f"issue:{group_info.group.qualified_short_id}",
  167. "dataset": "issuePlatform",
  168. },
  169. )
  170. assert response.status_code == 200, response.content
  171. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 3}], [{"count": 0}]]
  172. def test_errors_dataset(self):
  173. response = self.do_request(
  174. {
  175. "start": self.day_ago,
  176. "end": self.day_ago + timedelta(hours=2),
  177. "interval": "1h",
  178. "dataset": "errors",
  179. "query": "is:unresolved",
  180. },
  181. )
  182. assert response.status_code == 200, response.content
  183. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  184. def test_errors_dataset_no_query(self):
  185. response = self.do_request(
  186. {
  187. "start": self.day_ago,
  188. "end": self.day_ago + timedelta(hours=2),
  189. "interval": "1h",
  190. "dataset": "errors",
  191. },
  192. )
  193. assert response.status_code == 200, response.content
  194. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  195. def test_misaligned_last_bucket(self):
  196. response = self.do_request(
  197. data={
  198. "start": self.day_ago - timedelta(minutes=30),
  199. "end": self.day_ago + timedelta(hours=1, minutes=30),
  200. "interval": "1h",
  201. "partial": "1",
  202. },
  203. )
  204. assert response.status_code == 200, response.content
  205. assert [attrs for time, attrs in response.data["data"]] == [
  206. [{"count": 0}],
  207. [{"count": 1}],
  208. [{"count": 2}],
  209. ]
  210. def test_no_projects(self):
  211. org = self.create_organization(owner=self.user)
  212. self.login_as(user=self.user)
  213. url = reverse(
  214. "sentry-api-0-organization-events-stats", kwargs={"organization_id_or_slug": org.slug}
  215. )
  216. response = self.do_request({}, url)
  217. assert response.status_code == 200, response.content
  218. assert len(response.data["data"]) == 0
  219. def test_user_count(self):
  220. self.store_event(
  221. data={
  222. "event_id": "d" * 32,
  223. "message": "something",
  224. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  225. "tags": {"sentry:user": self.user2.email},
  226. "fingerprint": ["group2"],
  227. },
  228. project_id=self.project2.id,
  229. )
  230. response = self.do_request(
  231. data={
  232. "start": self.day_ago,
  233. "end": self.day_ago + timedelta(hours=2),
  234. "interval": "1h",
  235. "yAxis": "user_count",
  236. },
  237. )
  238. assert response.status_code == 200, response.content
  239. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 2}], [{"count": 1}]]
  240. def test_discover2_backwards_compatibility(self):
  241. response = self.do_request(
  242. data={
  243. "project": self.project.id,
  244. "start": self.day_ago,
  245. "end": self.day_ago + timedelta(hours=2),
  246. "interval": "1h",
  247. "yAxis": "user_count",
  248. },
  249. )
  250. assert response.status_code == 200, response.content
  251. assert len(response.data["data"]) > 0
  252. response = self.do_request(
  253. data={
  254. "project": self.project.id,
  255. "start": self.day_ago,
  256. "end": self.day_ago + timedelta(hours=2),
  257. "interval": "1h",
  258. "yAxis": "event_count",
  259. },
  260. )
  261. assert response.status_code == 200, response.content
  262. assert len(response.data["data"]) > 0
  263. def test_with_event_count_flag(self):
  264. response = self.do_request(
  265. data={
  266. "start": self.day_ago,
  267. "end": self.day_ago + timedelta(hours=2),
  268. "interval": "1h",
  269. "yAxis": "event_count",
  270. },
  271. )
  272. assert response.status_code == 200, response.content
  273. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  274. def test_performance_view_feature(self):
  275. response = self.do_request(
  276. data={
  277. "end": before_now(),
  278. "start": before_now(hours=2),
  279. "query": "project_id:1",
  280. "interval": "30m",
  281. "yAxis": "count()",
  282. },
  283. features={
  284. "organizations:performance-view": True,
  285. "organizations:discover-basic": False,
  286. },
  287. )
  288. assert response.status_code == 200, response.content
  289. def test_apdex_divide_by_zero(self):
  290. ProjectTransactionThreshold.objects.create(
  291. project=self.project,
  292. organization=self.project.organization,
  293. threshold=600,
  294. metric=TransactionMetric.LCP.value,
  295. )
  296. # Shouldn't count towards apdex
  297. data = load_data(
  298. "transaction",
  299. start_timestamp=self.day_ago + timedelta(minutes=(1)),
  300. timestamp=self.day_ago + timedelta(minutes=(3)),
  301. )
  302. data["transaction"] = "/apdex/new/"
  303. data["user"] = {"email": "1@example.com"}
  304. data["measurements"] = {}
  305. self.store_event(data, project_id=self.project.id)
  306. response = self.do_request(
  307. data={
  308. "start": self.day_ago,
  309. "end": self.day_ago + timedelta(hours=2),
  310. "interval": "1h",
  311. "yAxis": "apdex()",
  312. "project": [self.project.id],
  313. },
  314. )
  315. assert response.status_code == 200, response.content
  316. assert len(response.data["data"]) == 2
  317. data = response.data["data"]
  318. # 0 transactions with LCP 0/0
  319. assert [attrs for time, attrs in response.data["data"]] == [
  320. [{"count": 0}],
  321. [{"count": 0}],
  322. ]
  323. def test_aggregate_function_apdex(self):
  324. project1 = self.create_project()
  325. project2 = self.create_project()
  326. events = [
  327. ("one", 400, project1.id),
  328. ("one", 400, project1.id),
  329. ("two", 3000, project2.id),
  330. ("two", 1000, project2.id),
  331. ("three", 3000, project2.id),
  332. ]
  333. for idx, event in enumerate(events):
  334. data = load_data(
  335. "transaction",
  336. start_timestamp=self.day_ago + timedelta(minutes=(1 + idx)),
  337. timestamp=self.day_ago + timedelta(minutes=(1 + idx), milliseconds=event[1]),
  338. )
  339. data["event_id"] = f"{idx}" * 32
  340. data["transaction"] = f"/apdex/new/{event[0]}"
  341. data["user"] = {"email": f"{idx}@example.com"}
  342. self.store_event(data, project_id=event[2])
  343. response = self.do_request(
  344. data={
  345. "start": self.day_ago,
  346. "end": self.day_ago + timedelta(hours=2),
  347. "interval": "1h",
  348. "yAxis": "apdex()",
  349. },
  350. )
  351. assert response.status_code == 200, response.content
  352. assert [attrs for time, attrs in response.data["data"]] == [
  353. [{"count": 0.3}],
  354. [{"count": 0}],
  355. ]
  356. ProjectTransactionThreshold.objects.create(
  357. project=project1,
  358. organization=project1.organization,
  359. threshold=100,
  360. metric=TransactionMetric.DURATION.value,
  361. )
  362. ProjectTransactionThreshold.objects.create(
  363. project=project2,
  364. organization=project1.organization,
  365. threshold=100,
  366. metric=TransactionMetric.DURATION.value,
  367. )
  368. response = self.do_request(
  369. data={
  370. "start": self.day_ago,
  371. "end": self.day_ago + timedelta(hours=2),
  372. "interval": "1h",
  373. "yAxis": "apdex()",
  374. },
  375. )
  376. assert response.status_code == 200, response.content
  377. assert [attrs for time, attrs in response.data["data"]] == [
  378. [{"count": 0.2}],
  379. [{"count": 0}],
  380. ]
  381. response = self.do_request(
  382. data={
  383. "start": self.day_ago,
  384. "end": self.day_ago + timedelta(hours=2),
  385. "interval": "1h",
  386. "yAxis": ["user_count", "apdex()"],
  387. },
  388. )
  389. assert response.status_code == 200, response.content
  390. assert response.data["user_count"]["order"] == 0
  391. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  392. [{"count": 5}],
  393. [{"count": 0}],
  394. ]
  395. assert response.data["apdex()"]["order"] == 1
  396. assert [attrs for time, attrs in response.data["apdex()"]["data"]] == [
  397. [{"count": 0.2}],
  398. [{"count": 0}],
  399. ]
  400. def test_aggregate_function_count(self):
  401. response = self.do_request(
  402. data={
  403. "start": self.day_ago,
  404. "end": self.day_ago + timedelta(hours=2),
  405. "interval": "1h",
  406. "yAxis": "count()",
  407. },
  408. )
  409. assert response.status_code == 200, response.content
  410. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  411. def test_invalid_aggregate(self):
  412. response = self.do_request(
  413. data={
  414. "start": self.day_ago,
  415. "end": self.day_ago + timedelta(hours=2),
  416. "interval": "1h",
  417. "yAxis": "rubbish",
  418. },
  419. )
  420. assert response.status_code == 400, response.content
  421. def test_aggregate_function_user_count(self):
  422. response = self.do_request(
  423. data={
  424. "start": self.day_ago,
  425. "end": self.day_ago + timedelta(hours=2),
  426. "interval": "1h",
  427. "yAxis": "count_unique(user)",
  428. },
  429. )
  430. assert response.status_code == 200, response.content
  431. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 1}]]
  432. def test_aggregate_invalid(self):
  433. response = self.do_request(
  434. data={
  435. "start": self.day_ago,
  436. "end": self.day_ago + timedelta(hours=2),
  437. "interval": "1h",
  438. "yAxis": "nope(lol)",
  439. },
  440. )
  441. assert response.status_code == 400, response.content
  442. def test_throughput_meta(self):
  443. project = self.create_project()
  444. # Each of these denotes how many events to create in each hour
  445. event_counts = [6, 0, 6, 3, 0, 3]
  446. for hour, count in enumerate(event_counts):
  447. for minute in range(count):
  448. self.store_event(
  449. data={
  450. "event_id": str(uuid.uuid1()),
  451. "message": "very bad",
  452. "timestamp": (
  453. self.day_ago + timedelta(hours=hour, minutes=minute)
  454. ).isoformat(),
  455. "fingerprint": ["group1"],
  456. "tags": {"sentry:user": self.user.email},
  457. },
  458. project_id=project.id,
  459. )
  460. for axis in ["epm()", "tpm()"]:
  461. response = self.do_request(
  462. data={
  463. "transformAliasToInputFormat": 1,
  464. "start": self.day_ago,
  465. "end": self.day_ago + timedelta(hours=6),
  466. "interval": "1h",
  467. "yAxis": axis,
  468. "project": project.id,
  469. },
  470. )
  471. meta = response.data["meta"]
  472. assert meta["fields"] == {
  473. "time": "date",
  474. axis: "rate",
  475. }
  476. assert meta["units"] == {"time": None, axis: "1/minute"}
  477. data = response.data["data"]
  478. assert len(data) == 6
  479. rows = data[0:6]
  480. for test in zip(event_counts, rows):
  481. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  482. for axis in ["eps()", "tps()"]:
  483. response = self.do_request(
  484. data={
  485. "transformAliasToInputFormat": 1,
  486. "start": self.day_ago,
  487. "end": self.day_ago + timedelta(hours=6),
  488. "interval": "1h",
  489. "yAxis": axis,
  490. "project": project.id,
  491. },
  492. )
  493. meta = response.data["meta"]
  494. assert meta["fields"] == {
  495. "time": "date",
  496. axis: "rate",
  497. }
  498. assert meta["units"] == {"time": None, axis: "1/second"}
  499. def test_throughput_epm_hour_rollup(self):
  500. project = self.create_project()
  501. # Each of these denotes how many events to create in each hour
  502. event_counts = [6, 0, 6, 3, 0, 3]
  503. for hour, count in enumerate(event_counts):
  504. for minute in range(count):
  505. self.store_event(
  506. data={
  507. "event_id": str(uuid.uuid1()),
  508. "message": "very bad",
  509. "timestamp": (
  510. self.day_ago + timedelta(hours=hour, minutes=minute)
  511. ).isoformat(),
  512. "fingerprint": ["group1"],
  513. "tags": {"sentry:user": self.user.email},
  514. },
  515. project_id=project.id,
  516. )
  517. for axis in ["epm()", "tpm()"]:
  518. response = self.do_request(
  519. data={
  520. "start": self.day_ago,
  521. "end": self.day_ago + timedelta(hours=6),
  522. "interval": "1h",
  523. "yAxis": axis,
  524. "project": project.id,
  525. },
  526. )
  527. assert response.status_code == 200, response.content
  528. data = response.data["data"]
  529. assert len(data) == 6
  530. rows = data[0:6]
  531. for test in zip(event_counts, rows):
  532. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  533. def test_throughput_epm_day_rollup(self):
  534. project = self.create_project()
  535. # Each of these denotes how many events to create in each minute
  536. event_counts = [6, 0, 6, 3, 0, 3]
  537. for hour, count in enumerate(event_counts):
  538. for minute in range(count):
  539. self.store_event(
  540. data={
  541. "event_id": str(uuid.uuid1()),
  542. "message": "very bad",
  543. "timestamp": (
  544. self.day_ago + timedelta(hours=hour, minutes=minute)
  545. ).isoformat(),
  546. "fingerprint": ["group1"],
  547. "tags": {"sentry:user": self.user.email},
  548. },
  549. project_id=project.id,
  550. )
  551. for axis in ["epm()", "tpm()"]:
  552. response = self.do_request(
  553. data={
  554. "start": self.day_ago,
  555. "end": self.day_ago + timedelta(hours=24),
  556. "interval": "24h",
  557. "yAxis": axis,
  558. "project": project.id,
  559. },
  560. )
  561. assert response.status_code == 200, response.content
  562. data = response.data["data"]
  563. assert len(data) == 2
  564. assert data[0][1][0]["count"] == sum(event_counts) / (86400.0 / 60.0)
  565. def test_throughput_eps_minute_rollup(self):
  566. project = self.create_project()
  567. # Each of these denotes how many events to create in each minute
  568. event_counts = [6, 0, 6, 3, 0, 3]
  569. for minute, count in enumerate(event_counts):
  570. for second in range(count):
  571. self.store_event(
  572. data={
  573. "event_id": str(uuid.uuid1()),
  574. "message": "very bad",
  575. "timestamp": (
  576. self.day_ago + timedelta(minutes=minute, seconds=second)
  577. ).isoformat(),
  578. "fingerprint": ["group1"],
  579. "tags": {"sentry:user": self.user.email},
  580. },
  581. project_id=project.id,
  582. )
  583. for axis in ["eps()", "tps()"]:
  584. response = self.do_request(
  585. data={
  586. "start": self.day_ago,
  587. "end": self.day_ago + timedelta(minutes=6),
  588. "interval": "1m",
  589. "yAxis": axis,
  590. "project": project.id,
  591. },
  592. )
  593. assert response.status_code == 200, response.content
  594. data = response.data["data"]
  595. assert len(data) == 6
  596. rows = data[0:6]
  597. for test in zip(event_counts, rows):
  598. assert test[1][1][0]["count"] == test[0] / 60.0
  599. def test_throughput_eps_no_rollup(self):
  600. project = self.create_project()
  601. # Each of these denotes how many events to create in each minute
  602. event_counts = [6, 0, 6, 3, 0, 3]
  603. for minute, count in enumerate(event_counts):
  604. for second in range(count):
  605. self.store_event(
  606. data={
  607. "event_id": str(uuid.uuid1()),
  608. "message": "very bad",
  609. "timestamp": (
  610. self.day_ago + timedelta(minutes=minute, seconds=second)
  611. ).isoformat(),
  612. "fingerprint": ["group1"],
  613. "tags": {"sentry:user": self.user.email},
  614. },
  615. project_id=project.id,
  616. )
  617. response = self.do_request(
  618. data={
  619. "start": self.day_ago,
  620. "end": self.day_ago + timedelta(minutes=1),
  621. "interval": "1s",
  622. "yAxis": "eps()",
  623. "project": project.id,
  624. },
  625. )
  626. assert response.status_code == 200, response.content
  627. data = response.data["data"]
  628. # expect 60 data points between time span of 0 and 60 seconds
  629. assert len(data) == 60
  630. rows = data[0:6]
  631. for row in rows:
  632. assert row[1][0]["count"] == 1
  633. def test_transaction_events(self):
  634. prototype = {
  635. "type": "transaction",
  636. "transaction": "api.issue.delete",
  637. "spans": [],
  638. "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
  639. "tags": {"important": "yes"},
  640. }
  641. fixtures = (
  642. ("d" * 32, before_now(minutes=32)),
  643. ("e" * 32, before_now(hours=1, minutes=2)),
  644. ("f" * 32, before_now(hours=1, minutes=35)),
  645. )
  646. for fixture in fixtures:
  647. data = prototype.copy()
  648. data["event_id"] = fixture[0]
  649. data["timestamp"] = fixture[1].isoformat()
  650. data["start_timestamp"] = (fixture[1] - timedelta(seconds=1)).isoformat()
  651. self.store_event(data=data, project_id=self.project.id)
  652. for dataset in ["discover", "transactions"]:
  653. response = self.do_request(
  654. data={
  655. "project": self.project.id,
  656. "end": before_now(),
  657. "start": before_now(hours=2),
  658. "query": "event.type:transaction",
  659. "interval": "30m",
  660. "yAxis": "count()",
  661. "dataset": dataset,
  662. },
  663. )
  664. assert response.status_code == 200, response.content
  665. items = [item for time, item in response.data["data"] if item]
  666. # We could get more results depending on where the 30 min
  667. # windows land.
  668. assert len(items) >= 3
  669. def test_project_id_query_filter(self):
  670. response = self.do_request(
  671. data={
  672. "end": before_now(),
  673. "start": before_now(hours=2),
  674. "query": "project_id:1",
  675. "interval": "30m",
  676. "yAxis": "count()",
  677. },
  678. )
  679. assert response.status_code == 200
  680. def test_latest_release_query_filter(self):
  681. response = self.do_request(
  682. data={
  683. "project": self.project.id,
  684. "end": before_now(),
  685. "start": before_now(hours=2),
  686. "query": "release:latest",
  687. "interval": "30m",
  688. "yAxis": "count()",
  689. },
  690. )
  691. assert response.status_code == 200
  692. def test_conditional_filter(self):
  693. response = self.do_request(
  694. data={
  695. "start": self.day_ago,
  696. "end": self.day_ago + timedelta(hours=2),
  697. "query": "id:{} OR id:{}".format("a" * 32, "b" * 32),
  698. "interval": "30m",
  699. "yAxis": "count()",
  700. },
  701. )
  702. assert response.status_code == 200, response.content
  703. data = response.data["data"]
  704. assert len(data) == 4
  705. assert data[0][1][0]["count"] == 1
  706. assert data[2][1][0]["count"] == 1
  707. def test_simple_multiple_yaxis(self):
  708. response = self.do_request(
  709. data={
  710. "start": self.day_ago,
  711. "end": self.day_ago + timedelta(hours=2),
  712. "interval": "1h",
  713. "yAxis": ["user_count", "event_count"],
  714. },
  715. )
  716. assert response.status_code == 200, response.content
  717. assert response.data["user_count"]["order"] == 0
  718. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  719. [{"count": 1}],
  720. [{"count": 1}],
  721. ]
  722. assert response.data["event_count"]["order"] == 1
  723. assert [attrs for time, attrs in response.data["event_count"]["data"]] == [
  724. [{"count": 1}],
  725. [{"count": 2}],
  726. ]
  727. def test_equation_yaxis(self):
  728. response = self.do_request(
  729. data={
  730. "start": self.day_ago,
  731. "end": self.day_ago + timedelta(hours=2),
  732. "interval": "1h",
  733. "yAxis": ["equation|count() / 100"],
  734. },
  735. )
  736. assert response.status_code == 200, response.content
  737. assert len(response.data["data"]) == 2
  738. assert [attrs for time, attrs in response.data["data"]] == [
  739. [{"count": 0.01}],
  740. [{"count": 0.02}],
  741. ]
  742. def test_eps_equation(self):
  743. response = self.do_request(
  744. data={
  745. "start": self.day_ago,
  746. "end": self.day_ago + timedelta(hours=2),
  747. "interval": "1h",
  748. "yAxis": ["equation|eps() * 2"],
  749. },
  750. )
  751. assert response.status_code == 200, response.content
  752. assert len(response.data["data"]) == 2
  753. assert pytest.approx(0.000556, abs=0.0001) == response.data["data"][0][1][0]["count"]
  754. assert pytest.approx(0.001112, abs=0.0001) == response.data["data"][1][1][0]["count"]
  755. def test_epm_equation(self):
  756. response = self.do_request(
  757. data={
  758. "start": self.day_ago,
  759. "end": self.day_ago + timedelta(hours=2),
  760. "interval": "1h",
  761. "yAxis": ["equation|epm() * 2"],
  762. },
  763. )
  764. assert response.status_code == 200, response.content
  765. assert len(response.data["data"]) == 2
  766. assert pytest.approx(0.03334, abs=0.01) == response.data["data"][0][1][0]["count"]
  767. assert pytest.approx(0.06667, abs=0.01) == response.data["data"][1][1][0]["count"]
  768. def test_equation_mixed_multi_yaxis(self):
  769. response = self.do_request(
  770. data={
  771. "start": self.day_ago,
  772. "end": self.day_ago + timedelta(hours=2),
  773. "interval": "1h",
  774. "yAxis": ["count()", "equation|count() * 100"],
  775. },
  776. )
  777. assert response.status_code == 200, response.content
  778. assert response.data["count()"]["order"] == 0
  779. assert [attrs for time, attrs in response.data["count()"]["data"]] == [
  780. [{"count": 1}],
  781. [{"count": 2}],
  782. ]
  783. assert response.data["equation|count() * 100"]["order"] == 1
  784. assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
  785. [{"count": 100}],
  786. [{"count": 200}],
  787. ]
  788. def test_equation_multi_yaxis(self):
  789. response = self.do_request(
  790. data={
  791. "start": self.day_ago,
  792. "end": self.day_ago + timedelta(hours=2),
  793. "interval": "1h",
  794. "yAxis": ["equation|count() / 100", "equation|count() * 100"],
  795. },
  796. )
  797. assert response.status_code == 200, response.content
  798. assert response.data["equation|count() / 100"]["order"] == 0
  799. assert [attrs for time, attrs in response.data["equation|count() / 100"]["data"]] == [
  800. [{"count": 0.01}],
  801. [{"count": 0.02}],
  802. ]
  803. assert response.data["equation|count() * 100"]["order"] == 1
  804. assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
  805. [{"count": 100}],
  806. [{"count": 200}],
  807. ]
  808. def test_large_interval_no_drop_values(self):
  809. self.store_event(
  810. data={
  811. "event_id": "d" * 32,
  812. "message": "not good",
  813. "timestamp": (self.day_ago - timedelta(minutes=10)).isoformat(),
  814. "fingerprint": ["group3"],
  815. },
  816. project_id=self.project.id,
  817. )
  818. response = self.do_request(
  819. data={
  820. "project": self.project.id,
  821. "end": self.day_ago,
  822. "start": self.day_ago - timedelta(hours=24),
  823. "query": 'message:"not good"',
  824. "interval": "1d",
  825. "yAxis": "count()",
  826. },
  827. )
  828. assert response.status_code == 200
  829. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 0}], [{"count": 1}]]
  830. @mock.patch("sentry.snuba.discover.timeseries_query", return_value={})
  831. def test_multiple_yaxis_only_one_query(self, mock_query):
  832. self.do_request(
  833. data={
  834. "project": self.project.id,
  835. "start": self.day_ago,
  836. "end": self.day_ago + timedelta(hours=2),
  837. "interval": "1h",
  838. "yAxis": ["user_count", "event_count", "epm()", "eps()"],
  839. },
  840. )
  841. assert mock_query.call_count == 1
  842. @mock.patch("sentry.snuba.discover.bulk_snuba_queries", return_value=[{"data": []}])
  843. def test_invalid_interval(self, mock_query):
  844. self.do_request(
  845. data={
  846. "end": before_now(),
  847. "start": before_now(hours=24),
  848. "query": "",
  849. "interval": "1s",
  850. "yAxis": "count()",
  851. },
  852. )
  853. assert mock_query.call_count == 1
  854. # Should've reset to the default for 24h
  855. assert mock_query.mock_calls[0].args[0][0].query.granularity.granularity == 300
  856. self.do_request(
  857. data={
  858. "end": before_now(),
  859. "start": before_now(hours=24),
  860. "query": "",
  861. "interval": "0d",
  862. "yAxis": "count()",
  863. },
  864. )
  865. assert mock_query.call_count == 2
  866. # Should've reset to the default for 24h
  867. assert mock_query.mock_calls[1].args[0][0].query.granularity.granularity == 300
  868. def test_out_of_retention(self):
  869. with self.options({"system.event-retention-days": 10}):
  870. response = self.do_request(
  871. data={
  872. "start": before_now(days=20),
  873. "end": before_now(days=15),
  874. "query": "",
  875. "interval": "30m",
  876. "yAxis": "count()",
  877. },
  878. )
  879. assert response.status_code == 400
  880. @mock.patch("sentry.utils.snuba.quantize_time")
  881. def test_quantize_dates(self, mock_quantize):
  882. mock_quantize.return_value = before_now(days=1)
  883. # Don't quantize short time periods
  884. self.do_request(
  885. data={"statsPeriod": "1h", "query": "", "interval": "30m", "yAxis": "count()"},
  886. )
  887. # Don't quantize absolute date periods
  888. self.do_request(
  889. data={
  890. "start": before_now(days=20),
  891. "end": before_now(days=15),
  892. "query": "",
  893. "interval": "30m",
  894. "yAxis": "count()",
  895. },
  896. )
  897. assert len(mock_quantize.mock_calls) == 0
  898. # Quantize long date periods
  899. self.do_request(
  900. data={"statsPeriod": "90d", "query": "", "interval": "30m", "yAxis": "count()"},
  901. )
  902. assert len(mock_quantize.mock_calls) == 2
  903. def test_with_zerofill(self):
  904. response = self.do_request(
  905. data={
  906. "start": self.day_ago,
  907. "end": self.day_ago + timedelta(hours=2),
  908. "interval": "30m",
  909. },
  910. )
  911. assert response.status_code == 200, response.content
  912. assert [attrs for time, attrs in response.data["data"]] == [
  913. [{"count": 1}],
  914. [{"count": 0}],
  915. [{"count": 2}],
  916. [{"count": 0}],
  917. ]
  918. def test_without_zerofill(self):
  919. start = self.day_ago.isoformat()
  920. end = (self.day_ago + timedelta(hours=2)).isoformat()
  921. response = self.do_request(
  922. data={
  923. "start": start,
  924. "end": end,
  925. "interval": "30m",
  926. "withoutZerofill": "1",
  927. },
  928. features={
  929. "organizations:performance-chart-interpolation": True,
  930. "organizations:discover-basic": True,
  931. },
  932. )
  933. assert response.status_code == 200, response.content
  934. assert [attrs for time, attrs in response.data["data"]] == [
  935. [{"count": 1}],
  936. [{"count": 2}],
  937. ]
  938. assert response.data["start"] == parse_date(start).timestamp()
  939. assert response.data["end"] == parse_date(end).timestamp()
  940. def test_comparison_error_dataset(self):
  941. self.store_event(
  942. data={
  943. "timestamp": (self.day_ago + timedelta(days=-1, minutes=1)).isoformat(),
  944. },
  945. project_id=self.project.id,
  946. )
  947. self.store_event(
  948. data={
  949. "timestamp": (self.day_ago + timedelta(days=-1, minutes=2)).isoformat(),
  950. },
  951. project_id=self.project.id,
  952. )
  953. self.store_event(
  954. data={
  955. "timestamp": (self.day_ago + timedelta(days=-1, hours=1, minutes=1)).isoformat(),
  956. },
  957. project_id=self.project2.id,
  958. )
  959. response = self.do_request(
  960. data={
  961. "start": self.day_ago,
  962. "end": self.day_ago + timedelta(hours=2),
  963. "interval": "1h",
  964. "comparisonDelta": int(timedelta(days=1).total_seconds()),
  965. "dataset": "errors",
  966. }
  967. )
  968. assert response.status_code == 200, response.content
  969. assert [attrs for time, attrs in response.data["data"]] == [
  970. [{"count": 1, "comparisonCount": 2}],
  971. [{"count": 2, "comparisonCount": 1}],
  972. ]
  973. def test_comparison(self):
  974. self.store_event(
  975. data={
  976. "timestamp": (self.day_ago + timedelta(days=-1, minutes=1)).isoformat(),
  977. },
  978. project_id=self.project.id,
  979. )
  980. self.store_event(
  981. data={
  982. "timestamp": (self.day_ago + timedelta(days=-1, minutes=2)).isoformat(),
  983. },
  984. project_id=self.project.id,
  985. )
  986. self.store_event(
  987. data={
  988. "timestamp": (self.day_ago + timedelta(days=-1, hours=1, minutes=1)).isoformat(),
  989. },
  990. project_id=self.project2.id,
  991. )
  992. response = self.do_request(
  993. data={
  994. "start": self.day_ago,
  995. "end": self.day_ago + timedelta(hours=2),
  996. "interval": "1h",
  997. "comparisonDelta": int(timedelta(days=1).total_seconds()),
  998. }
  999. )
  1000. assert response.status_code == 200, response.content
  1001. assert [attrs for time, attrs in response.data["data"]] == [
  1002. [{"count": 1, "comparisonCount": 2}],
  1003. [{"count": 2, "comparisonCount": 1}],
  1004. ]
  1005. def test_comparison_invalid(self):
  1006. response = self.do_request(
  1007. data={
  1008. "start": self.day_ago,
  1009. "end": self.day_ago + timedelta(hours=2),
  1010. "interval": "1h",
  1011. "comparisonDelta": "17h",
  1012. },
  1013. )
  1014. assert response.status_code == 400, response.content
  1015. assert response.data["detail"] == "comparisonDelta must be an integer"
  1016. start = before_now(days=85)
  1017. end = start + timedelta(days=7)
  1018. with self.options({"system.event-retention-days": 90}):
  1019. response = self.do_request(
  1020. data={
  1021. "start": start,
  1022. "end": end,
  1023. "interval": "1h",
  1024. "comparisonDelta": int(timedelta(days=7).total_seconds()),
  1025. }
  1026. )
  1027. assert response.status_code == 400, response.content
  1028. assert response.data["detail"] == "Comparison period is outside retention window"
  1029. def test_equations_divide_by_zero(self):
  1030. response = self.do_request(
  1031. data={
  1032. "start": self.day_ago,
  1033. "end": self.day_ago + timedelta(hours=2),
  1034. "interval": "1h",
  1035. # force a 0 in the denominator by doing 1 - 1
  1036. # since a 0 literal is illegal as the denominator
  1037. "yAxis": ["equation|count() / (1-1)"],
  1038. },
  1039. )
  1040. assert response.status_code == 200, response.content
  1041. assert len(response.data["data"]) == 2
  1042. assert [attrs for time, attrs in response.data["data"]] == [
  1043. [{"count": None}],
  1044. [{"count": None}],
  1045. ]
  1046. @mock.patch("sentry.search.events.builder.base.raw_snql_query")
  1047. def test_profiles_dataset_simple(self, mock_snql_query):
  1048. mock_snql_query.side_effect = [{"meta": {}, "data": []}]
  1049. query = {
  1050. "yAxis": [
  1051. "count()",
  1052. "p75()",
  1053. "p95()",
  1054. "p99()",
  1055. "p75(profile.duration)",
  1056. "p95(profile.duration)",
  1057. "p99(profile.duration)",
  1058. ],
  1059. "project": [self.project.id],
  1060. "dataset": "profiles",
  1061. }
  1062. response = self.do_request(query, features={"organizations:profiling": True})
  1063. assert response.status_code == 200, response.content
  1064. def test_tag_with_conflicting_function_alias_simple(self):
  1065. for _ in range(7):
  1066. self.store_event(
  1067. data={
  1068. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1069. "tags": {"count": "9001"},
  1070. },
  1071. project_id=self.project2.id,
  1072. )
  1073. # Query for count and count()
  1074. data = {
  1075. "start": self.day_ago.isoformat(),
  1076. "end": (self.day_ago + timedelta(minutes=3)).isoformat(),
  1077. "interval": "1h",
  1078. "yAxis": "count()",
  1079. "orderby": ["-count()"],
  1080. "field": ["count()", "count"],
  1081. "partial": "1",
  1082. }
  1083. response = self.client.get(self.url, data, format="json")
  1084. assert response.status_code == 200
  1085. # Expect a count of 8 because one event from setUp
  1086. assert response.data["data"][0][1] == [{"count": 8}]
  1087. data["query"] = "count:9001"
  1088. response = self.client.get(self.url, data, format="json")
  1089. assert response.status_code == 200
  1090. assert response.data["data"][0][1] == [{"count": 7}]
  1091. data["query"] = "count:abc"
  1092. response = self.client.get(self.url, data, format="json")
  1093. assert response.status_code == 200
  1094. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  1095. def test_group_id_tag_simple(self):
  1096. event_data: _EventDataDict = {
  1097. "data": {
  1098. "message": "poof",
  1099. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1100. "user": {"email": self.user.email},
  1101. "tags": {"group_id": "testing"},
  1102. "fingerprint": ["group1"],
  1103. },
  1104. "project": self.project2,
  1105. "count": 7,
  1106. }
  1107. for i in range(event_data["count"]):
  1108. event_data["data"]["event_id"] = f"a{i}" * 16
  1109. self.store_event(event_data["data"], project_id=event_data["project"].id)
  1110. data = {
  1111. "start": self.day_ago.isoformat(),
  1112. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1113. "interval": "1h",
  1114. "yAxis": "count()",
  1115. "orderby": ["-count()"],
  1116. "field": ["count()", "group_id"],
  1117. "partial": "1",
  1118. }
  1119. response = self.client.get(self.url, data, format="json")
  1120. assert response.status_code == 200
  1121. assert response.data["data"][0][1] == [{"count": 8}]
  1122. data["query"] = "group_id:testing"
  1123. response = self.client.get(self.url, data, format="json")
  1124. assert response.status_code == 200
  1125. assert response.data["data"][0][1] == [{"count": 7}]
  1126. data["query"] = "group_id:abc"
  1127. response = self.client.get(self.url, data, format="json")
  1128. assert response.status_code == 200
  1129. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  1130. class OrganizationEventsStatsTopNEvents(APITestCase, SnubaTestCase):
  1131. def setUp(self):
  1132. super().setUp()
  1133. self.login_as(user=self.user)
  1134. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  1135. self.project = self.create_project()
  1136. self.project2 = self.create_project()
  1137. self.user2 = self.create_user()
  1138. transaction_data = load_data("transaction")
  1139. transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat()
  1140. transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=4)).isoformat()
  1141. transaction_data["tags"] = {"shared-tag": "yup"}
  1142. self.event_data: list[_EventDataDict] = [
  1143. {
  1144. "data": {
  1145. "message": "poof",
  1146. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1147. "user": {"email": self.user.email},
  1148. "tags": {"shared-tag": "yup"},
  1149. "fingerprint": ["group1"],
  1150. },
  1151. "project": self.project2,
  1152. "count": 7,
  1153. },
  1154. {
  1155. "data": {
  1156. "message": "voof",
  1157. "timestamp": (self.day_ago + timedelta(hours=1, minutes=2)).isoformat(),
  1158. "fingerprint": ["group2"],
  1159. "user": {"email": self.user2.email},
  1160. "tags": {"shared-tag": "yup"},
  1161. },
  1162. "project": self.project2,
  1163. "count": 6,
  1164. },
  1165. {
  1166. "data": {
  1167. "message": "very bad",
  1168. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1169. "fingerprint": ["group3"],
  1170. "user": {"email": "foo@example.com"},
  1171. "tags": {"shared-tag": "yup"},
  1172. },
  1173. "project": self.project,
  1174. "count": 5,
  1175. },
  1176. {
  1177. "data": {
  1178. "message": "oh no",
  1179. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1180. "fingerprint": ["group4"],
  1181. "user": {"email": "bar@example.com"},
  1182. "tags": {"shared-tag": "yup"},
  1183. },
  1184. "project": self.project,
  1185. "count": 4,
  1186. },
  1187. {"data": transaction_data, "project": self.project, "count": 3},
  1188. # Not in the top 5
  1189. {
  1190. "data": {
  1191. "message": "sorta bad",
  1192. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1193. "fingerprint": ["group5"],
  1194. "user": {"email": "bar@example.com"},
  1195. "tags": {"shared-tag": "yup"},
  1196. },
  1197. "project": self.project,
  1198. "count": 2,
  1199. },
  1200. {
  1201. "data": {
  1202. "message": "not so bad",
  1203. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1204. "fingerprint": ["group6"],
  1205. "user": {"email": "bar@example.com"},
  1206. "tags": {"shared-tag": "yup"},
  1207. },
  1208. "project": self.project,
  1209. "count": 1,
  1210. },
  1211. ]
  1212. self.events = []
  1213. for index, event_data in enumerate(self.event_data):
  1214. data = event_data["data"].copy()
  1215. for i in range(event_data["count"]):
  1216. data["event_id"] = f"{index}{i}" * 16
  1217. event = self.store_event(data, project_id=event_data["project"].id)
  1218. self.events.append(event)
  1219. self.transaction = self.events[4]
  1220. self.enabled_features = {
  1221. "organizations:discover-basic": True,
  1222. }
  1223. self.url = reverse(
  1224. "sentry-api-0-organization-events-stats",
  1225. kwargs={"organization_id_or_slug": self.project.organization.slug},
  1226. )
  1227. def test_no_top_events_with_project_field(self):
  1228. project = self.create_project()
  1229. with self.feature(self.enabled_features):
  1230. response = self.client.get(
  1231. self.url,
  1232. data={
  1233. # make sure to query the project with 0 events
  1234. "project": str(project.id),
  1235. "start": self.day_ago.isoformat(),
  1236. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1237. "interval": "1h",
  1238. "yAxis": "count()",
  1239. "orderby": ["-count()"],
  1240. "field": ["count()", "project"],
  1241. "topEvents": "5",
  1242. },
  1243. format="json",
  1244. )
  1245. assert response.status_code == 200, response.content
  1246. # When there are no top events, we do not return an empty dict.
  1247. # Instead, we return a single zero-filled series for an empty graph.
  1248. data = response.data["data"]
  1249. assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
  1250. def test_no_top_events(self):
  1251. project = self.create_project()
  1252. with self.feature(self.enabled_features):
  1253. response = self.client.get(
  1254. self.url,
  1255. data={
  1256. # make sure to query the project with 0 events
  1257. "project": str(project.id),
  1258. "start": self.day_ago.isoformat(),
  1259. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1260. "interval": "1h",
  1261. "yAxis": "count()",
  1262. "orderby": ["-count()"],
  1263. "field": ["count()", "message", "user.email"],
  1264. "topEvents": "5",
  1265. },
  1266. format="json",
  1267. )
  1268. data = response.data["data"]
  1269. assert response.status_code == 200, response.content
  1270. # When there are no top events, we do not return an empty dict.
  1271. # Instead, we return a single zero-filled series for an empty graph.
  1272. assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
  1273. def test_no_top_events_with_multi_axis(self):
  1274. project = self.create_project()
  1275. with self.feature(self.enabled_features):
  1276. response = self.client.get(
  1277. self.url,
  1278. data={
  1279. # make sure to query the project with 0 events
  1280. "project": str(project.id),
  1281. "start": self.day_ago.isoformat(),
  1282. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1283. "interval": "1h",
  1284. "yAxis": ["count()", "count_unique(user)"],
  1285. "orderby": ["-count()"],
  1286. "field": ["count()", "count_unique(user)", "message", "user.email"],
  1287. "topEvents": "5",
  1288. },
  1289. format="json",
  1290. )
  1291. assert response.status_code == 200
  1292. data = response.data[""]
  1293. assert [attrs for time, attrs in data["count()"]["data"]] == [
  1294. [{"count": 0}],
  1295. [{"count": 0}],
  1296. ]
  1297. assert [attrs for time, attrs in data["count_unique(user)"]["data"]] == [
  1298. [{"count": 0}],
  1299. [{"count": 0}],
  1300. ]
  1301. def test_simple_top_events(self):
  1302. with self.feature(self.enabled_features):
  1303. response = self.client.get(
  1304. self.url,
  1305. data={
  1306. "start": self.day_ago.isoformat(),
  1307. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1308. "interval": "1h",
  1309. "yAxis": "count()",
  1310. "orderby": ["-count()"],
  1311. "field": ["count()", "message", "user.email"],
  1312. "topEvents": "5",
  1313. },
  1314. format="json",
  1315. )
  1316. data = response.data
  1317. assert response.status_code == 200, response.content
  1318. assert len(data) == 6
  1319. for index, event in enumerate(self.events[:5]):
  1320. message = event.message or event.transaction
  1321. results = data[
  1322. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1323. ]
  1324. assert results["order"] == index
  1325. assert [{"count": self.event_data[index]["count"]}] in [
  1326. attrs for _, attrs in results["data"]
  1327. ]
  1328. other = data["Other"]
  1329. assert other["order"] == 5
  1330. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1331. def test_simple_top_events_meta(self):
  1332. with self.feature(self.enabled_features):
  1333. response = self.client.get(
  1334. self.url,
  1335. data={
  1336. "start": self.day_ago.isoformat(),
  1337. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1338. "interval": "1h",
  1339. "yAxis": "sum(transaction.duration)",
  1340. "orderby": ["-sum(transaction.duration)"],
  1341. "field": ["transaction", "sum(transaction.duration)"],
  1342. "topEvents": "5",
  1343. },
  1344. format="json",
  1345. )
  1346. data = response.data
  1347. assert response.status_code == 200, response.content
  1348. for transaction, transaction_data in data.items():
  1349. assert transaction_data["meta"]["fields"] == {
  1350. "time": "date",
  1351. "transaction": "string",
  1352. "sum_transaction_duration": "duration",
  1353. }
  1354. assert transaction_data["meta"]["units"] == {
  1355. "time": None,
  1356. "transaction": None,
  1357. "sum_transaction_duration": "millisecond",
  1358. }
  1359. def test_simple_top_events_meta_no_alias(self):
  1360. with self.feature(self.enabled_features):
  1361. response = self.client.get(
  1362. self.url,
  1363. data={
  1364. "transformAliasToInputFormat": "1",
  1365. "start": self.day_ago.isoformat(),
  1366. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1367. "interval": "1h",
  1368. "yAxis": "sum(transaction.duration)",
  1369. "orderby": ["-sum(transaction.duration)"],
  1370. "field": ["transaction", "sum(transaction.duration)"],
  1371. "topEvents": "5",
  1372. },
  1373. format="json",
  1374. )
  1375. data = response.data
  1376. assert response.status_code == 200, response.content
  1377. for transaction, transaction_data in data.items():
  1378. assert transaction_data["meta"]["fields"] == {
  1379. "time": "date",
  1380. "transaction": "string",
  1381. "sum(transaction.duration)": "duration",
  1382. }
  1383. assert transaction_data["meta"]["units"] == {
  1384. "time": None,
  1385. "transaction": None,
  1386. "sum(transaction.duration)": "millisecond",
  1387. }
  1388. def test_top_events_with_projects_other(self):
  1389. with self.feature(self.enabled_features):
  1390. response = self.client.get(
  1391. self.url,
  1392. data={
  1393. "start": self.day_ago.isoformat(),
  1394. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1395. "interval": "1h",
  1396. "yAxis": "count()",
  1397. "orderby": ["-count()"],
  1398. "field": ["count()", "project"],
  1399. "topEvents": "1",
  1400. },
  1401. format="json",
  1402. )
  1403. data = response.data
  1404. assert response.status_code == 200, response.content
  1405. assert set(data.keys()) == {"Other", self.project.slug}
  1406. assert data[self.project.slug]["order"] == 0
  1407. assert [attrs[0]["count"] for _, attrs in data[self.project.slug]["data"]] == [15, 0]
  1408. assert data["Other"]["order"] == 1
  1409. assert [attrs[0]["count"] for _, attrs in data["Other"]["data"]] == [7, 6]
  1410. def test_top_events_with_projects_fields(self):
  1411. # We need to handle the project name fields differently
  1412. for project_field in ["project", "project.name"]:
  1413. with self.feature(self.enabled_features):
  1414. response = self.client.get(
  1415. self.url,
  1416. data={
  1417. "start": self.day_ago.isoformat(),
  1418. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1419. "interval": "1h",
  1420. "yAxis": "count()",
  1421. "orderby": ["-count()"],
  1422. "field": ["count()", project_field],
  1423. "topEvents": "5",
  1424. },
  1425. format="json",
  1426. )
  1427. data = response.data
  1428. assert response.status_code == 200, response.content
  1429. assert data[self.project.slug]["order"] == 0, project_field
  1430. assert [attrs[0]["count"] for _, attrs in data[self.project.slug]["data"]] == [
  1431. 15,
  1432. 0,
  1433. ], project_field
  1434. assert data[self.project2.slug]["order"] == 1, project_field
  1435. assert [attrs[0]["count"] for _, attrs in data[self.project2.slug]["data"]] == [
  1436. 7,
  1437. 6,
  1438. ], project_field
  1439. def test_tag_with_conflicting_function_alias_simple(self):
  1440. event_data: _EventDataDict = {
  1441. "data": {
  1442. "message": "poof",
  1443. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1444. "user": {"email": self.user.email},
  1445. "tags": {"count": "9001"},
  1446. "fingerprint": ["group1"],
  1447. },
  1448. "project": self.project2,
  1449. "count": 7,
  1450. }
  1451. for i in range(event_data["count"]):
  1452. event_data["data"]["event_id"] = f"a{i}" * 16
  1453. self.store_event(event_data["data"], project_id=event_data["project"].id)
  1454. # Query for count and count()
  1455. data = {
  1456. "start": self.day_ago.isoformat(),
  1457. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1458. "interval": "1h",
  1459. "yAxis": "count()",
  1460. "orderby": ["-count()"],
  1461. "field": ["count()", "count"],
  1462. "topEvents": "5",
  1463. "partial": "1",
  1464. }
  1465. with self.feature(self.enabled_features):
  1466. response = self.client.get(self.url, data, format="json")
  1467. assert response.status_code == 200
  1468. assert response.data["9001"]["data"][0][1] == [{"count": 7}]
  1469. data["query"] = "count:9001"
  1470. with self.feature(self.enabled_features):
  1471. response = self.client.get(self.url, data, format="json")
  1472. assert response.status_code == 200
  1473. assert response.data["9001"]["data"][0][1] == [{"count": 7}]
  1474. data["query"] = "count:abc"
  1475. with self.feature(self.enabled_features):
  1476. response = self.client.get(self.url, data, format="json")
  1477. assert response.status_code == 200
  1478. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  1479. @pytest.mark.xfail(
  1480. reason="The response.data[Other] returns 15 locally and returns 16 or 15 remotely."
  1481. )
  1482. def test_tag_with_conflicting_function_alias_with_other_single_grouping(self):
  1483. event_data: list[_EventDataDict] = [
  1484. {
  1485. "data": {
  1486. "message": "poof",
  1487. "timestamp": self.day_ago + timedelta(minutes=2),
  1488. "user": {"email": self.user.email},
  1489. "tags": {"count": "9001"},
  1490. "fingerprint": ["group1"],
  1491. },
  1492. "project": self.project2,
  1493. "count": 7,
  1494. },
  1495. {
  1496. "data": {
  1497. "message": "poof2",
  1498. "timestamp": self.day_ago + timedelta(minutes=2),
  1499. "user": {"email": self.user.email},
  1500. "tags": {"count": "abc"},
  1501. "fingerprint": ["group1"],
  1502. },
  1503. "project": self.project2,
  1504. "count": 3,
  1505. },
  1506. ]
  1507. for index, event in enumerate(event_data):
  1508. for i in range(event["count"]):
  1509. event["data"]["event_id"] = f"{index}{i}" * 16
  1510. self.store_event(event["data"], project_id=event["project"].id)
  1511. # Query for count and count()
  1512. data = {
  1513. "start": self.day_ago.isoformat(),
  1514. "end": (self.day_ago + timedelta(hours=1)).isoformat(),
  1515. "interval": "1h",
  1516. "yAxis": "count()",
  1517. "orderby": ["-count"],
  1518. "field": ["count()", "count"],
  1519. "topEvents": "2",
  1520. "partial": "1",
  1521. }
  1522. with self.feature(self.enabled_features):
  1523. response = self.client.get(self.url, data, format="json")
  1524. assert response.status_code == 200
  1525. assert response.data["9001"]["data"][0][1] == [{"count": 7}]
  1526. assert response.data["abc"]["data"][0][1] == [{"count": 3}]
  1527. assert response.data["Other"]["data"][0][1] == [{"count": 16}]
  1528. def test_tag_with_conflicting_function_alias_with_other_multiple_groupings(self):
  1529. event_data: list[_EventDataDict] = [
  1530. {
  1531. "data": {
  1532. "message": "abc",
  1533. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1534. "user": {"email": self.user.email},
  1535. "tags": {"count": "2"},
  1536. "fingerprint": ["group1"],
  1537. },
  1538. "project": self.project2,
  1539. "count": 3,
  1540. },
  1541. {
  1542. "data": {
  1543. "message": "def",
  1544. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1545. "user": {"email": self.user.email},
  1546. "tags": {"count": "9001"},
  1547. "fingerprint": ["group1"],
  1548. },
  1549. "project": self.project2,
  1550. "count": 7,
  1551. },
  1552. ]
  1553. for index, event in enumerate(event_data):
  1554. for i in range(event["count"]):
  1555. event["data"]["event_id"] = f"{index}{i}" * 16
  1556. self.store_event(event["data"], project_id=event["project"].id)
  1557. # Query for count and count()
  1558. data = {
  1559. "start": self.day_ago.isoformat(),
  1560. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1561. "interval": "2d",
  1562. "yAxis": "count()",
  1563. "orderby": ["-count"],
  1564. "field": ["count()", "count", "message"],
  1565. "topEvents": "2",
  1566. "partial": "1",
  1567. }
  1568. with self.feature(self.enabled_features):
  1569. response = self.client.get(self.url, data, format="json")
  1570. assert response.status_code == 200
  1571. assert response.data["abc,2"]["data"][0][1] == [{"count": 3}]
  1572. assert response.data["def,9001"]["data"][0][1] == [{"count": 7}]
  1573. assert response.data["Other"]["data"][0][1] == [{"count": 25}]
  1574. def test_group_id_tag_simple(self):
  1575. event_data: _EventDataDict = {
  1576. "data": {
  1577. "message": "poof",
  1578. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  1579. "user": {"email": self.user.email},
  1580. "tags": {"group_id": "the tag"},
  1581. "fingerprint": ["group1"],
  1582. },
  1583. "project": self.project2,
  1584. "count": 7,
  1585. }
  1586. for i in range(event_data["count"]):
  1587. event_data["data"]["event_id"] = f"a{i}" * 16
  1588. self.store_event(event_data["data"], project_id=event_data["project"].id)
  1589. data = {
  1590. "start": self.day_ago.isoformat(),
  1591. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1592. "interval": "1h",
  1593. "yAxis": "count()",
  1594. "orderby": ["-count()"],
  1595. "field": ["count()", "group_id"],
  1596. "topEvents": "5",
  1597. "partial": "1",
  1598. }
  1599. with self.feature(self.enabled_features):
  1600. response = self.client.get(self.url, data, format="json")
  1601. assert response.status_code == 200, response.content
  1602. assert response.data["the tag"]["data"][0][1] == [{"count": 7}]
  1603. data["query"] = 'group_id:"the tag"'
  1604. with self.feature(self.enabled_features):
  1605. response = self.client.get(self.url, data, format="json")
  1606. assert response.status_code == 200
  1607. assert response.data["the tag"]["data"][0][1] == [{"count": 7}]
  1608. data["query"] = "group_id:abc"
  1609. with self.feature(self.enabled_features):
  1610. response = self.client.get(self.url, data, format="json")
  1611. assert response.status_code == 200
  1612. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  1613. def test_top_events_limits(self):
  1614. data = {
  1615. "start": self.day_ago.isoformat(),
  1616. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1617. "interval": "1h",
  1618. "yAxis": "count()",
  1619. "orderby": ["-count()"],
  1620. "field": ["count()", "message", "user.email"],
  1621. }
  1622. with self.feature(self.enabled_features):
  1623. data["topEvents"] = str(MAX_TOP_EVENTS + 1)
  1624. response = self.client.get(self.url, data, format="json")
  1625. assert response.status_code == 400
  1626. data["topEvents"] = "0"
  1627. response = self.client.get(self.url, data, format="json")
  1628. assert response.status_code == 400
  1629. data["topEvents"] = "a"
  1630. response = self.client.get(self.url, data, format="json")
  1631. assert response.status_code == 400
  1632. @pytest.mark.xfail(
  1633. reason="The response is wrong whenever we have a top events timeseries on project + any other field + aggregation"
  1634. )
  1635. def test_top_events_with_projects(self):
  1636. with self.feature(self.enabled_features):
  1637. response = self.client.get(
  1638. self.url,
  1639. data={
  1640. "start": self.day_ago.isoformat(),
  1641. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1642. "interval": "1h",
  1643. "yAxis": "count()",
  1644. "orderby": ["-count()"],
  1645. "field": ["count()", "message", "project"],
  1646. "topEvents": "5",
  1647. },
  1648. format="json",
  1649. )
  1650. data = response.data
  1651. assert response.status_code == 200, response.content
  1652. assert len(data) == 6
  1653. for index, event in enumerate(self.events[:5]):
  1654. message = event.message or event.transaction
  1655. results = data[",".join([message, event.project.slug])]
  1656. assert results["order"] == index
  1657. assert [{"count": self.event_data[index]["count"]}] in [
  1658. attrs for time, attrs in results["data"]
  1659. ]
  1660. other = data["Other"]
  1661. assert other["order"] == 5
  1662. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1663. def test_top_events_with_issue(self):
  1664. # delete a group to make sure if this happens the value becomes unknown
  1665. event_group = self.events[0].group
  1666. event_group.delete()
  1667. with self.feature(self.enabled_features):
  1668. response = self.client.get(
  1669. self.url,
  1670. data={
  1671. "start": self.day_ago.isoformat(),
  1672. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1673. "interval": "1h",
  1674. "yAxis": "count()",
  1675. "orderby": ["-count()"],
  1676. "field": ["count()", "message", "issue"],
  1677. "topEvents": "5",
  1678. "query": "!event.type:transaction",
  1679. },
  1680. format="json",
  1681. )
  1682. data = response.data
  1683. assert response.status_code == 200, response.content
  1684. assert len(data) == 6
  1685. for index, event in enumerate(self.events[:4]):
  1686. message = event.message
  1687. # Because we deleted the group for event 0
  1688. if index == 0 or event.group is None:
  1689. issue = "unknown"
  1690. else:
  1691. issue = event.group.qualified_short_id
  1692. results = data[",".join([issue, message])]
  1693. assert results["order"] == index
  1694. assert [{"count": self.event_data[index]["count"]}] in [
  1695. attrs for time, attrs in results["data"]
  1696. ]
  1697. other = data["Other"]
  1698. assert other["order"] == 5
  1699. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1700. def test_transactions_top_events_with_issue(self):
  1701. # delete a group to make sure if this happens the value becomes unknown
  1702. event_group = self.events[0].group
  1703. event_group.delete()
  1704. with self.feature(self.enabled_features):
  1705. response = self.client.get(
  1706. self.url,
  1707. data={
  1708. "start": self.day_ago.isoformat(),
  1709. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1710. "interval": "1h",
  1711. "yAxis": "count()",
  1712. "orderby": ["-count()"],
  1713. "field": ["count()", "message", "issue"],
  1714. "topEvents": "5",
  1715. "query": "!event.type:transaction",
  1716. "dataset": "transactions",
  1717. },
  1718. format="json",
  1719. )
  1720. assert response.status_code == 200, response.content
  1721. # Just asserting that this doesn't fail, issue on transactions dataset doesn't mean anything
  1722. def test_top_events_with_transaction_status(self):
  1723. with self.feature(self.enabled_features):
  1724. response = self.client.get(
  1725. self.url,
  1726. data={
  1727. "start": self.day_ago.isoformat(),
  1728. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1729. "interval": "1h",
  1730. "yAxis": "count()",
  1731. "orderby": ["-count()"],
  1732. "field": ["count()", "transaction.status"],
  1733. "topEvents": "5",
  1734. },
  1735. format="json",
  1736. )
  1737. data = response.data
  1738. assert response.status_code == 200, response.content
  1739. assert len(data) == 1
  1740. assert "ok" in data
  1741. @mock.patch("sentry.models.GroupManager.get_issues_mapping")
  1742. def test_top_events_with_unknown_issue(self, mock_issues_mapping):
  1743. event = self.events[0]
  1744. event_data = self.event_data[0]
  1745. # ensure that the issue mapping returns None for the issue
  1746. mock_issues_mapping.return_value = {event.group.id: None}
  1747. with self.feature(self.enabled_features):
  1748. response = self.client.get(
  1749. self.url,
  1750. data={
  1751. "start": self.day_ago.isoformat(),
  1752. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1753. "interval": "1h",
  1754. "yAxis": "count()",
  1755. "orderby": ["-count()"],
  1756. "field": ["count()", "issue"],
  1757. "topEvents": "5",
  1758. # narrow the search to just one issue
  1759. "query": f"issue.id:{event.group.id}",
  1760. },
  1761. format="json",
  1762. )
  1763. assert response.status_code == 200, response.content
  1764. data = response.data
  1765. assert len(data) == 1
  1766. results = data["unknown"]
  1767. assert results["order"] == 0
  1768. assert [{"count": event_data["count"]}] in [attrs for time, attrs in results["data"]]
  1769. @mock.patch(
  1770. "sentry.search.events.builder.base.raw_snql_query",
  1771. side_effect=[{"data": [{"issue.id": 1}], "meta": []}, {"data": [], "meta": []}],
  1772. )
  1773. def test_top_events_with_issue_check_query_conditions(self, mock_query):
  1774. """ "Intentionally separate from test_top_events_with_issue
  1775. This is to test against a bug where the condition for issues wasn't included and we'd be missing data for
  1776. the interval since we'd cap out the max rows. This was not caught by the previous test since the results
  1777. would still be correct given the smaller interval & lack of data
  1778. """
  1779. with self.feature(self.enabled_features):
  1780. self.client.get(
  1781. self.url,
  1782. data={
  1783. "start": self.day_ago.isoformat(),
  1784. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1785. "interval": "1h",
  1786. "yAxis": "count()",
  1787. "orderby": ["-count()"],
  1788. "field": ["count()", "message", "issue"],
  1789. "topEvents": "5",
  1790. "query": "!event.type:transaction",
  1791. },
  1792. format="json",
  1793. )
  1794. assert (
  1795. Condition(Function("coalesce", [Column("group_id"), 0], "issue.id"), Op.IN, [1])
  1796. in mock_query.mock_calls[1].args[0].query.where
  1797. )
  1798. def test_top_events_with_functions(self):
  1799. for dataset in ["transactions", "discover"]:
  1800. with self.feature(self.enabled_features):
  1801. response = self.client.get(
  1802. self.url,
  1803. data={
  1804. "start": self.day_ago.isoformat(),
  1805. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1806. "interval": "1h",
  1807. "yAxis": "count()",
  1808. "orderby": ["-p99()"],
  1809. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1810. "topEvents": "5",
  1811. "dataset": dataset,
  1812. },
  1813. format="json",
  1814. )
  1815. data = response.data
  1816. assert response.status_code == 200, response.content
  1817. assert len(data) == 1
  1818. results = data[self.transaction.transaction]
  1819. assert results["order"] == 0
  1820. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1821. def test_top_events_with_functions_on_different_transactions(self):
  1822. """Transaction2 has less events, but takes longer so order should be self.transaction then transaction2"""
  1823. transaction_data = load_data("transaction")
  1824. transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat()
  1825. transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=6)).isoformat()
  1826. transaction_data["transaction"] = "/foo_bar/"
  1827. transaction2 = self.store_event(transaction_data, project_id=self.project.id)
  1828. with self.feature(self.enabled_features):
  1829. response = self.client.get(
  1830. self.url,
  1831. data={
  1832. "start": self.day_ago.isoformat(),
  1833. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1834. "interval": "1h",
  1835. "yAxis": "count()",
  1836. "orderby": ["-p90()"],
  1837. "field": ["transaction", "avg(transaction.duration)", "p90()"],
  1838. "topEvents": "5",
  1839. },
  1840. format="json",
  1841. )
  1842. data = response.data
  1843. assert response.status_code == 200, response.content
  1844. assert len(data) == 2
  1845. results = data[self.transaction.transaction]
  1846. assert results["order"] == 1
  1847. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1848. results = data[transaction2.transaction]
  1849. assert results["order"] == 0
  1850. assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
  1851. def test_top_events_with_query(self):
  1852. transaction_data = load_data("transaction")
  1853. transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat()
  1854. transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=6)).isoformat()
  1855. transaction_data["transaction"] = "/foo_bar/"
  1856. self.store_event(transaction_data, project_id=self.project.id)
  1857. with self.feature(self.enabled_features):
  1858. response = self.client.get(
  1859. self.url,
  1860. data={
  1861. "start": self.day_ago.isoformat(),
  1862. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1863. "interval": "1h",
  1864. "yAxis": "count()",
  1865. "orderby": ["-p99()"],
  1866. "query": "transaction:/foo_bar/",
  1867. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1868. "topEvents": "5",
  1869. },
  1870. format="json",
  1871. )
  1872. data = response.data
  1873. assert response.status_code == 200, response.content
  1874. assert len(data) == 1
  1875. transaction2_data = data["/foo_bar/"]
  1876. assert transaction2_data["order"] == 0
  1877. assert [attrs for time, attrs in transaction2_data["data"]] == [
  1878. [{"count": 1}],
  1879. [{"count": 0}],
  1880. ]
  1881. def test_top_events_with_negated_condition(self):
  1882. with self.feature(self.enabled_features):
  1883. response = self.client.get(
  1884. self.url,
  1885. data={
  1886. "start": self.day_ago.isoformat(),
  1887. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1888. "interval": "1h",
  1889. "yAxis": "count()",
  1890. "orderby": ["-count()"],
  1891. "query": f"!message:{self.events[0].message}",
  1892. "field": ["message", "count()"],
  1893. "topEvents": "5",
  1894. },
  1895. format="json",
  1896. )
  1897. data = response.data
  1898. assert response.status_code == 200, response.content
  1899. assert len(data) == 6
  1900. for index, event in enumerate(self.events[1:5]):
  1901. message = event.message or event.transaction
  1902. results = data[message]
  1903. assert results["order"] == index
  1904. assert [{"count": self.event_data[index + 1]["count"]}] in [
  1905. attrs for _, attrs in results["data"]
  1906. ]
  1907. other = data["Other"]
  1908. assert other["order"] == 5
  1909. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1910. def test_top_events_with_epm(self):
  1911. with self.feature(self.enabled_features):
  1912. response = self.client.get(
  1913. self.url,
  1914. data={
  1915. "start": self.day_ago.isoformat(),
  1916. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1917. "interval": "1h",
  1918. "yAxis": "epm()",
  1919. "orderby": ["-count()"],
  1920. "field": ["message", "user.email", "count()"],
  1921. "topEvents": "5",
  1922. },
  1923. format="json",
  1924. )
  1925. data = response.data
  1926. assert response.status_code == 200, response.content
  1927. assert len(data) == 6
  1928. for index, event in enumerate(self.events[:5]):
  1929. message = event.message or event.transaction
  1930. results = data[
  1931. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1932. ]
  1933. assert results["order"] == index
  1934. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  1935. attrs for time, attrs in results["data"]
  1936. ]
  1937. other = data["Other"]
  1938. assert other["order"] == 5
  1939. assert [{"count": 0.05}] in [attrs for _, attrs in other["data"]]
  1940. def test_top_events_with_multiple_yaxis(self):
  1941. with self.feature(self.enabled_features):
  1942. response = self.client.get(
  1943. self.url,
  1944. data={
  1945. "start": self.day_ago.isoformat(),
  1946. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1947. "interval": "1h",
  1948. "yAxis": ["epm()", "count()"],
  1949. "orderby": ["-count()"],
  1950. "field": ["message", "user.email", "count()"],
  1951. "topEvents": "5",
  1952. },
  1953. format="json",
  1954. )
  1955. data = response.data
  1956. assert response.status_code == 200, response.content
  1957. assert len(data) == 6
  1958. for index, event in enumerate(self.events[:5]):
  1959. message = event.message or event.transaction
  1960. results = data[
  1961. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1962. ]
  1963. assert results["order"] == index
  1964. assert results["epm()"]["order"] == 0
  1965. assert results["count()"]["order"] == 1
  1966. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  1967. attrs for time, attrs in results["epm()"]["data"]
  1968. ]
  1969. assert [{"count": self.event_data[index]["count"]}] in [
  1970. attrs for time, attrs in results["count()"]["data"]
  1971. ]
  1972. other = data["Other"]
  1973. assert other["order"] == 5
  1974. assert other["epm()"]["order"] == 0
  1975. assert other["count()"]["order"] == 1
  1976. assert [{"count": 0.05}] in [attrs for _, attrs in other["epm()"]["data"]]
  1977. assert [{"count": 3}] in [attrs for _, attrs in other["count()"]["data"]]
  1978. def test_top_events_with_boolean(self):
  1979. with self.feature(self.enabled_features):
  1980. response = self.client.get(
  1981. self.url,
  1982. data={
  1983. "start": self.day_ago.isoformat(),
  1984. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  1985. "interval": "1h",
  1986. "yAxis": "count()",
  1987. "orderby": ["-count()"],
  1988. "field": ["count()", "message", "device.charging"],
  1989. "topEvents": "5",
  1990. },
  1991. format="json",
  1992. )
  1993. data = response.data
  1994. assert response.status_code == 200, response.content
  1995. assert len(data) == 6
  1996. for index, event in enumerate(self.events[:5]):
  1997. message = event.message or event.transaction
  1998. results = data[",".join(["False", message])]
  1999. assert results["order"] == index
  2000. assert [{"count": self.event_data[index]["count"]}] in [
  2001. attrs for time, attrs in results["data"]
  2002. ]
  2003. other = data["Other"]
  2004. assert other["order"] == 5
  2005. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  2006. def test_top_events_with_error_unhandled(self):
  2007. self.login_as(user=self.user)
  2008. project = self.create_project()
  2009. prototype = load_data("android-ndk")
  2010. prototype["event_id"] = "f" * 32
  2011. prototype["logentry"] = {"formatted": "not handled"}
  2012. prototype["exception"]["values"][0]["value"] = "not handled"
  2013. prototype["exception"]["values"][0]["mechanism"]["handled"] = False
  2014. prototype["timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat()
  2015. self.store_event(data=prototype, project_id=project.id)
  2016. with self.feature(self.enabled_features):
  2017. response = self.client.get(
  2018. self.url,
  2019. data={
  2020. "start": self.day_ago.isoformat(),
  2021. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2022. "interval": "1h",
  2023. "yAxis": "count()",
  2024. "orderby": ["-count()"],
  2025. "field": ["count()", "error.unhandled"],
  2026. "topEvents": "5",
  2027. },
  2028. format="json",
  2029. )
  2030. data = response.data
  2031. assert response.status_code == 200, response.content
  2032. assert len(data) == 2
  2033. def test_top_events_with_timestamp(self):
  2034. with self.feature(self.enabled_features):
  2035. response = self.client.get(
  2036. self.url,
  2037. data={
  2038. "start": self.day_ago.isoformat(),
  2039. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2040. "interval": "1h",
  2041. "yAxis": "count()",
  2042. "orderby": ["-count()"],
  2043. "query": "event.type:default",
  2044. "field": ["count()", "message", "timestamp"],
  2045. "topEvents": "5",
  2046. },
  2047. format="json",
  2048. )
  2049. data = response.data
  2050. assert response.status_code == 200, response.content
  2051. assert len(data) == 6
  2052. # Transactions won't be in the results because of the query
  2053. del self.events[4]
  2054. del self.event_data[4]
  2055. for index, event in enumerate(self.events[:5]):
  2056. results = data[",".join([event.message, event.timestamp])]
  2057. assert results["order"] == index
  2058. assert [{"count": self.event_data[index]["count"]}] in [
  2059. attrs for time, attrs in results["data"]
  2060. ]
  2061. other = data["Other"]
  2062. assert other["order"] == 5
  2063. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  2064. def test_top_events_with_int(self):
  2065. with self.feature(self.enabled_features):
  2066. response = self.client.get(
  2067. self.url,
  2068. data={
  2069. "start": self.day_ago.isoformat(),
  2070. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2071. "interval": "1h",
  2072. "yAxis": "count()",
  2073. "orderby": ["-count()"],
  2074. "field": ["count()", "message", "transaction.duration"],
  2075. "topEvents": "5",
  2076. },
  2077. format="json",
  2078. )
  2079. data = response.data
  2080. assert response.status_code == 200, response.content
  2081. assert len(data) == 1
  2082. results = data[",".join([self.transaction.transaction, "120000"])]
  2083. assert results["order"] == 0
  2084. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  2085. def test_top_events_with_user(self):
  2086. with self.feature(self.enabled_features):
  2087. response = self.client.get(
  2088. self.url,
  2089. data={
  2090. "start": self.day_ago.isoformat(),
  2091. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2092. "interval": "1h",
  2093. "yAxis": "count()",
  2094. "orderby": ["-count()", "user"],
  2095. "field": ["user", "count()"],
  2096. "topEvents": "5",
  2097. },
  2098. format="json",
  2099. )
  2100. data = response.data
  2101. assert response.status_code == 200, response.content
  2102. assert len(data) == 5
  2103. assert data["email:bar@example.com"]["order"] == 1
  2104. assert [attrs for time, attrs in data["email:bar@example.com"]["data"]] == [
  2105. [{"count": 7}],
  2106. [{"count": 0}],
  2107. ]
  2108. assert [attrs for time, attrs in data["ip:127.0.0.1"]["data"]] == [
  2109. [{"count": 3}],
  2110. [{"count": 0}],
  2111. ]
  2112. def test_top_events_with_user_and_email(self):
  2113. with self.feature(self.enabled_features):
  2114. response = self.client.get(
  2115. self.url,
  2116. data={
  2117. "start": self.day_ago.isoformat(),
  2118. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2119. "interval": "1h",
  2120. "yAxis": "count()",
  2121. "orderby": ["-count()", "user"],
  2122. "field": ["user", "user.email", "count()"],
  2123. "topEvents": "5",
  2124. },
  2125. format="json",
  2126. )
  2127. data = response.data
  2128. assert response.status_code == 200, response.content
  2129. assert len(data) == 5
  2130. assert data["email:bar@example.com,bar@example.com"]["order"] == 1
  2131. assert [attrs for time, attrs in data["email:bar@example.com,bar@example.com"]["data"]] == [
  2132. [{"count": 7}],
  2133. [{"count": 0}],
  2134. ]
  2135. assert [attrs for time, attrs in data["ip:127.0.0.1,None"]["data"]] == [
  2136. [{"count": 3}],
  2137. [{"count": 0}],
  2138. ]
  2139. def test_top_events_with_user_display(self):
  2140. with self.feature(self.enabled_features):
  2141. response = self.client.get(
  2142. self.url,
  2143. data={
  2144. "start": self.day_ago.isoformat(),
  2145. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2146. "interval": "1h",
  2147. "yAxis": "count()",
  2148. "orderby": ["-count()"],
  2149. "field": ["message", "user.display", "count()"],
  2150. "topEvents": "5",
  2151. },
  2152. format="json",
  2153. )
  2154. data = response.data
  2155. assert response.status_code == 200, response.content
  2156. assert len(data) == 6
  2157. for index, event in enumerate(self.events[:5]):
  2158. message = event.message or event.transaction
  2159. user = self.event_data[index]["data"]["user"]
  2160. results = data[
  2161. ",".join([message, user.get("email", None) or user.get("ip_address", "None")])
  2162. ]
  2163. assert results["order"] == index
  2164. assert [{"count": self.event_data[index]["count"]}] in [
  2165. attrs for _, attrs in results["data"]
  2166. ]
  2167. other = data["Other"]
  2168. assert other["order"] == 5
  2169. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  2170. @pytest.mark.skip(reason="A query with group_id will not return transactions")
  2171. def test_top_events_none_filter(self):
  2172. """When a field is None in one of the top events, make sure we filter by it
  2173. In this case event[4] is a transaction and has no issue
  2174. """
  2175. with self.feature(self.enabled_features):
  2176. response = self.client.get(
  2177. self.url,
  2178. data={
  2179. "start": self.day_ago.isoformat(),
  2180. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2181. "interval": "1h",
  2182. "yAxis": "count()",
  2183. "orderby": ["-count()"],
  2184. "field": ["count()", "issue"],
  2185. "topEvents": "5",
  2186. },
  2187. format="json",
  2188. )
  2189. data = response.data
  2190. assert response.status_code == 200, response.content
  2191. assert len(data) == 5
  2192. for index, event in enumerate(self.events[:5]):
  2193. if event.group is None:
  2194. issue = "unknown"
  2195. else:
  2196. issue = event.group.qualified_short_id
  2197. results = data[issue]
  2198. assert results["order"] == index
  2199. assert [{"count": self.event_data[index]["count"]}] in [
  2200. attrs for time, attrs in results["data"]
  2201. ]
  2202. @pytest.mark.skip(reason="Invalid query - transaction events don't have group_id field")
  2203. def test_top_events_one_field_with_none(self):
  2204. with self.feature(self.enabled_features):
  2205. response = self.client.get(
  2206. self.url,
  2207. data={
  2208. "start": self.day_ago.isoformat(),
  2209. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2210. "interval": "1h",
  2211. "yAxis": "count()",
  2212. "orderby": ["-count()"],
  2213. "query": "event.type:transaction",
  2214. "field": ["count()", "issue"],
  2215. "topEvents": "5",
  2216. },
  2217. format="json",
  2218. )
  2219. data = response.data
  2220. assert response.status_code == 200, response.content
  2221. assert len(data) == 1
  2222. results = data["unknown"]
  2223. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  2224. assert results["order"] == 0
  2225. def test_top_events_with_error_handled(self):
  2226. data = self.event_data[0]
  2227. data["data"]["level"] = "error"
  2228. data["data"]["exception"] = {
  2229. "values": [
  2230. {
  2231. "type": "ValidationError",
  2232. "value": "Bad request",
  2233. "mechanism": {"handled": True, "type": "generic"},
  2234. }
  2235. ]
  2236. }
  2237. self.store_event(data["data"], project_id=data["project"].id)
  2238. data["data"]["exception"] = {
  2239. "values": [
  2240. {
  2241. "type": "ValidationError",
  2242. "value": "Bad request",
  2243. "mechanism": {"handled": False, "type": "generic"},
  2244. }
  2245. ]
  2246. }
  2247. self.store_event(data["data"], project_id=data["project"].id)
  2248. with self.feature(self.enabled_features):
  2249. response = self.client.get(
  2250. self.url,
  2251. data={
  2252. "start": self.day_ago.isoformat(),
  2253. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2254. "interval": "1h",
  2255. "yAxis": "count()",
  2256. "orderby": ["-count()"],
  2257. "field": ["count()", "error.handled"],
  2258. "topEvents": "5",
  2259. "query": "!event.type:transaction",
  2260. },
  2261. format="json",
  2262. )
  2263. assert response.status_code == 200, response.content
  2264. res_data = response.data
  2265. assert len(res_data) == 2
  2266. results = res_data["1"]
  2267. assert [attrs for time, attrs in results["data"]] == [[{"count": 20}], [{"count": 6}]]
  2268. results = res_data["0"]
  2269. assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
  2270. def test_top_events_with_aggregate_condition(self):
  2271. with self.feature(self.enabled_features):
  2272. response = self.client.get(
  2273. self.url,
  2274. data={
  2275. "start": self.day_ago.isoformat(),
  2276. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2277. "interval": "1h",
  2278. "yAxis": "count()",
  2279. "orderby": ["-count()"],
  2280. "field": ["message", "count()"],
  2281. "query": "count():>4",
  2282. "topEvents": "5",
  2283. },
  2284. format="json",
  2285. )
  2286. assert response.status_code == 200, response.content
  2287. data = response.data
  2288. assert len(data) == 3
  2289. for index, event in enumerate(self.events[:3]):
  2290. message = event.message or event.transaction
  2291. results = data[message]
  2292. assert results["order"] == index
  2293. assert [{"count": self.event_data[index]["count"]}] in [
  2294. attrs for time, attrs in results["data"]
  2295. ]
  2296. @pytest.mark.xfail(reason="There's only 2 rows total, which mean there shouldn't be other")
  2297. def test_top_events_with_to_other(self):
  2298. version = "version -@'\" 1.2,3+(4)"
  2299. version_escaped = "version -@'\\\" 1.2,3+(4)"
  2300. # every symbol is replaced with a underscore to make the alias
  2301. version_alias = "version_______1_2_3__4_"
  2302. # add an event in the current release
  2303. event = self.event_data[0]
  2304. event_data = event["data"].copy()
  2305. event_data["event_id"] = uuid4().hex
  2306. event_data["release"] = version
  2307. self.store_event(event_data, project_id=event["project"].id)
  2308. with self.feature(self.enabled_features):
  2309. response = self.client.get(
  2310. self.url,
  2311. data={
  2312. "start": self.day_ago.isoformat(),
  2313. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2314. "interval": "1h",
  2315. "yAxis": "count()",
  2316. # the double underscores around the version alias is because of a comma and quote
  2317. "orderby": [f"-to_other_release__{version_alias}__others_current"],
  2318. "field": [
  2319. "count()",
  2320. f'to_other(release,"{version_escaped}",others,current)',
  2321. ],
  2322. "topEvents": "2",
  2323. },
  2324. format="json",
  2325. )
  2326. assert response.status_code == 200, response.content
  2327. data = response.data
  2328. assert len(data) == 2
  2329. current = data["current"]
  2330. assert current["order"] == 1
  2331. assert sum(attrs[0]["count"] for _, attrs in current["data"]) == 1
  2332. others = data["others"]
  2333. assert others["order"] == 0
  2334. assert sum(attrs[0]["count"] for _, attrs in others["data"]) == sum(
  2335. event_data["count"] for event_data in self.event_data
  2336. )
  2337. def test_top_events_with_equations(self):
  2338. with self.feature(self.enabled_features):
  2339. response = self.client.get(
  2340. self.url,
  2341. data={
  2342. "start": self.day_ago.isoformat(),
  2343. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2344. "interval": "1h",
  2345. "yAxis": "equation|count() / 100",
  2346. "orderby": ["-count()"],
  2347. "field": ["count()", "message", "user.email", "equation|count() / 100"],
  2348. "topEvents": "5",
  2349. },
  2350. format="json",
  2351. )
  2352. data = response.data
  2353. assert response.status_code == 200, response.content
  2354. assert len(data) == 6
  2355. for index, event in enumerate(self.events[:5]):
  2356. message = event.message or event.transaction
  2357. results = data[
  2358. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  2359. ]
  2360. assert results["order"] == index
  2361. assert [{"count": self.event_data[index]["count"] / 100}] in [
  2362. attrs for time, attrs in results["data"]
  2363. ]
  2364. other = data["Other"]
  2365. assert other["order"] == 5
  2366. assert [{"count": 0.03}] in [attrs for _, attrs in other["data"]]
  2367. @mock.patch("sentry.snuba.discover.bulk_snuba_queries", return_value=[{"data": [], "meta": []}])
  2368. @mock.patch(
  2369. "sentry.search.events.builder.base.raw_snql_query",
  2370. return_value={"data": [], "meta": []},
  2371. )
  2372. def test_invalid_interval(self, mock_raw_query, mock_bulk_query):
  2373. with self.feature(self.enabled_features):
  2374. response = self.client.get(
  2375. self.url,
  2376. format="json",
  2377. data={
  2378. "end": before_now().isoformat(),
  2379. # 7,200 points for each event
  2380. "start": before_now(seconds=7200).isoformat(),
  2381. "field": ["count()", "issue"],
  2382. "query": "",
  2383. "interval": "1s",
  2384. "yAxis": "count()",
  2385. },
  2386. )
  2387. assert response.status_code == 200
  2388. assert mock_bulk_query.call_count == 1
  2389. with self.feature(self.enabled_features):
  2390. response = self.client.get(
  2391. self.url,
  2392. format="json",
  2393. data={
  2394. "end": before_now().isoformat(),
  2395. "start": before_now(seconds=7200).isoformat(),
  2396. "field": ["count()", "issue"],
  2397. "query": "",
  2398. "interval": "1s",
  2399. "yAxis": "count()",
  2400. # 7,200 points for each event * 2, should error
  2401. "topEvents": "2",
  2402. },
  2403. )
  2404. assert response.status_code == 200
  2405. assert mock_raw_query.call_count == 2
  2406. # Should've reset to the default for between 1 and 24h
  2407. assert mock_raw_query.mock_calls[1].args[0].query.granularity.granularity == 300
  2408. with self.feature(self.enabled_features):
  2409. response = self.client.get(
  2410. self.url,
  2411. format="json",
  2412. data={
  2413. "end": before_now().isoformat(),
  2414. # 1999 points * 5 events should just be enough to not error
  2415. "start": before_now(seconds=1999).isoformat(),
  2416. "field": ["count()", "issue"],
  2417. "query": "",
  2418. "interval": "1s",
  2419. "yAxis": "count()",
  2420. "topEvents": "5",
  2421. },
  2422. )
  2423. assert response.status_code == 200
  2424. assert mock_raw_query.call_count == 4
  2425. # Should've left the interval alone since we're just below the limit
  2426. assert mock_raw_query.mock_calls[3].args[0].query.granularity.granularity == 1
  2427. with self.feature(self.enabled_features):
  2428. response = self.client.get(
  2429. self.url,
  2430. format="json",
  2431. data={
  2432. "end": before_now().isoformat(),
  2433. "start": before_now(hours=24).isoformat(),
  2434. "field": ["count()", "issue"],
  2435. "query": "",
  2436. "interval": "0d",
  2437. "yAxis": "count()",
  2438. "topEvents": "5",
  2439. },
  2440. )
  2441. assert response.status_code == 200
  2442. assert mock_raw_query.call_count == 6
  2443. # Should've default to 24h's default of 5m
  2444. assert mock_raw_query.mock_calls[5].args[0].query.granularity.granularity == 300
  2445. def test_top_events_timestamp_fields(self):
  2446. with self.feature(self.enabled_features):
  2447. response = self.client.get(
  2448. self.url,
  2449. format="json",
  2450. data={
  2451. "start": self.day_ago.isoformat(),
  2452. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2453. "interval": "1h",
  2454. "yAxis": "count()",
  2455. "orderby": ["-count()"],
  2456. "field": ["count()", "timestamp", "timestamp.to_hour", "timestamp.to_day"],
  2457. "topEvents": "5",
  2458. },
  2459. )
  2460. assert response.status_code == 200
  2461. data = response.data
  2462. assert len(data) == 3
  2463. # these are the timestamps corresponding to the events stored
  2464. timestamps = [
  2465. self.day_ago + timedelta(minutes=2),
  2466. self.day_ago + timedelta(hours=1, minutes=2),
  2467. self.day_ago + timedelta(minutes=4),
  2468. ]
  2469. timestamp_hours = [timestamp.replace(minute=0, second=0) for timestamp in timestamps]
  2470. timestamp_days = [timestamp.replace(hour=0, minute=0, second=0) for timestamp in timestamps]
  2471. for ts, ts_hr, ts_day in zip(timestamps, timestamp_hours, timestamp_days):
  2472. key = f"{ts.isoformat()},{ts_day.isoformat()},{ts_hr.isoformat()}"
  2473. count = sum(e["count"] for e in self.event_data if e["data"]["timestamp"] == ts)
  2474. results = data[key]
  2475. assert [{"count": count}] in [attrs for time, attrs in results["data"]]
  2476. def test_top_events_other_with_matching_columns(self):
  2477. with self.feature(self.enabled_features):
  2478. response = self.client.get(
  2479. self.url,
  2480. data={
  2481. "start": self.day_ago.isoformat(),
  2482. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2483. "interval": "1h",
  2484. "yAxis": "count()",
  2485. "orderby": ["-count()"],
  2486. "field": ["count()", "tags[shared-tag]", "message"],
  2487. "topEvents": "5",
  2488. },
  2489. format="json",
  2490. )
  2491. data = response.data
  2492. assert response.status_code == 200, response.content
  2493. assert len(data) == 6
  2494. for index, event in enumerate(self.events[:5]):
  2495. message = event.message or event.transaction
  2496. results = data[",".join([message, "yup"])]
  2497. assert results["order"] == index
  2498. assert [{"count": self.event_data[index]["count"]}] in [
  2499. attrs for _, attrs in results["data"]
  2500. ]
  2501. other = data["Other"]
  2502. assert other["order"] == 5
  2503. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  2504. def test_top_events_with_field_overlapping_other_key(self):
  2505. transaction_data = load_data("transaction")
  2506. transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat()
  2507. transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=6)).isoformat()
  2508. transaction_data["transaction"] = OTHER_KEY
  2509. for i in range(5):
  2510. data = transaction_data.copy()
  2511. data["event_id"] = "ab" + f"{i}" * 30
  2512. data["contexts"]["trace"]["span_id"] = "ab" + f"{i}" * 14
  2513. self.store_event(data, project_id=self.project.id)
  2514. with self.feature(self.enabled_features):
  2515. response = self.client.get(
  2516. self.url,
  2517. data={
  2518. "start": self.day_ago.isoformat(),
  2519. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2520. "interval": "1h",
  2521. "yAxis": "count()",
  2522. "orderby": ["-count()"],
  2523. "field": ["count()", "message"],
  2524. "topEvents": "5",
  2525. },
  2526. format="json",
  2527. )
  2528. data = response.data
  2529. assert response.status_code == 200, response.content
  2530. assert len(data) == 6
  2531. assert f"{OTHER_KEY} (message)" in data
  2532. results = data[f"{OTHER_KEY} (message)"]
  2533. assert [{"count": 5}] in [attrs for _, attrs in results["data"]]
  2534. other = data["Other"]
  2535. assert other["order"] == 5
  2536. assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
  2537. def test_top_events_can_exclude_other_series(self):
  2538. with self.feature(self.enabled_features):
  2539. response = self.client.get(
  2540. self.url,
  2541. data={
  2542. "start": self.day_ago.isoformat(),
  2543. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2544. "interval": "1h",
  2545. "yAxis": "count()",
  2546. "orderby": ["count()"],
  2547. "field": ["count()", "message"],
  2548. "topEvents": "5",
  2549. "excludeOther": "1",
  2550. },
  2551. format="json",
  2552. )
  2553. data = response.data
  2554. assert response.status_code == 200, response.content
  2555. assert len(data) == 5
  2556. assert "Other" not in response.data
  2557. @pytest.mark.xfail(reason="Started failing on ClickHouse 21.8")
  2558. def test_top_events_with_equation_including_unselected_fields_passes_field_validation(self):
  2559. with self.feature(self.enabled_features):
  2560. response = self.client.get(
  2561. self.url,
  2562. data={
  2563. "start": self.day_ago.isoformat(),
  2564. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2565. "interval": "1h",
  2566. "yAxis": "count()",
  2567. "orderby": ["-equation[0]"],
  2568. "field": ["count()", "message", "equation|count_unique(user) * 2"],
  2569. "topEvents": "5",
  2570. },
  2571. format="json",
  2572. )
  2573. data = response.data
  2574. assert response.status_code == 200, response.content
  2575. assert len(data) == 6
  2576. other = data["Other"]
  2577. assert other["order"] == 5
  2578. assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
  2579. def test_top_events_boolean_condition_and_project_field(self):
  2580. with self.feature(self.enabled_features):
  2581. response = self.client.get(
  2582. self.url,
  2583. data={
  2584. "start": self.day_ago.isoformat(),
  2585. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2586. "interval": "1h",
  2587. "yAxis": "count()",
  2588. "orderby": ["-count()"],
  2589. "field": ["project", "count()"],
  2590. "topEvents": "5",
  2591. "query": "event.type:transaction (transaction:*a OR transaction:b*)",
  2592. },
  2593. format="json",
  2594. )
  2595. assert response.status_code == 200
  2596. class OrganizationEventsStatsProfileFunctionDatasetEndpointTest(
  2597. APITestCase, ProfilesSnubaTestCase, SearchIssueTestMixin
  2598. ):
  2599. endpoint = "sentry-api-0-organization-events-stats"
  2600. def setUp(self):
  2601. super().setUp()
  2602. self.login_as(user=self.user)
  2603. self.one_day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  2604. self.two_days_ago = before_now(days=2).replace(hour=10, minute=0, second=0, microsecond=0)
  2605. self.three_days_ago = before_now(days=3).replace(hour=10, minute=0, second=0, microsecond=0)
  2606. self.project = self.create_project()
  2607. self.url = reverse(
  2608. "sentry-api-0-organization-events-stats",
  2609. kwargs={"organization_id_or_slug": self.project.organization.slug},
  2610. )
  2611. def test_functions_dataset_simple(self):
  2612. transaction_function = self.store_functions(
  2613. [
  2614. {
  2615. "self_times_ns": [100_000_000 for _ in range(100)],
  2616. "package": "foo",
  2617. "function": "bar",
  2618. "in_app": True,
  2619. },
  2620. ],
  2621. project=self.project,
  2622. timestamp=self.two_days_ago - timedelta(hours=12),
  2623. )
  2624. continuous_timestamp = self.two_days_ago + timedelta(hours=12)
  2625. continuous_function = self.store_functions_chunk(
  2626. [
  2627. {
  2628. "self_times_ns": [200_000_000 for _ in range(100)],
  2629. "package": "bar",
  2630. "function": "bar",
  2631. "thread_id": "1",
  2632. "in_app": True,
  2633. },
  2634. ],
  2635. project=self.project,
  2636. timestamp=continuous_timestamp,
  2637. )
  2638. y_axes = [
  2639. "cpm()",
  2640. "p95(function.duration)",
  2641. "all_examples()",
  2642. ]
  2643. data = {
  2644. "dataset": "profileFunctions",
  2645. "start": self.three_days_ago.isoformat(),
  2646. "end": self.one_day_ago.isoformat(),
  2647. "interval": "1d",
  2648. "yAxis": y_axes,
  2649. }
  2650. response = self.client.get(self.url, data=data, format="json")
  2651. assert response.status_code == 200, response.content
  2652. assert sum(row[1][0]["count"] for row in response.data["cpm()"]["data"]) == pytest.approx(
  2653. 200 / ((self.one_day_ago - self.three_days_ago).total_seconds() / 60), rel=1e-3
  2654. )
  2655. assert any(
  2656. row[1][0]["count"] > 0 for row in response.data["p95(function.duration)"]["data"]
  2657. )
  2658. examples = [row[1][0]["count"] for row in response.data["all_examples()"]["data"]]
  2659. assert examples == [
  2660. [
  2661. {
  2662. "profile_id": transaction_function["transaction"]["contexts"]["profile"][
  2663. "profile_id"
  2664. ],
  2665. },
  2666. ],
  2667. [
  2668. {
  2669. "profiler_id": continuous_function["profiler_id"],
  2670. "thread_id": "1",
  2671. "start": continuous_timestamp.timestamp(),
  2672. "end": (continuous_timestamp + timedelta(microseconds=200_000)).timestamp(),
  2673. },
  2674. ],
  2675. ]
  2676. for y_axis in y_axes:
  2677. assert response.data[y_axis]["meta"]["fields"] == {
  2678. "time": "date",
  2679. "cpm": "number",
  2680. "p95_function_duration": "duration",
  2681. "all_examples": "string",
  2682. }
  2683. assert response.data[y_axis]["meta"]["units"] == {
  2684. "time": None,
  2685. "cpm": None,
  2686. "p95_function_duration": "nanosecond",
  2687. "all_examples": None,
  2688. }
  2689. class OrganizationEventsStatsTopNEventsProfileFunctionDatasetEndpointTest(
  2690. APITestCase, ProfilesSnubaTestCase, SearchIssueTestMixin
  2691. ):
  2692. endpoint = "sentry-api-0-organization-events-stats"
  2693. def setUp(self):
  2694. super().setUp()
  2695. self.login_as(user=self.user)
  2696. self.one_day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  2697. self.two_days_ago = before_now(days=2).replace(hour=10, minute=0, second=0, microsecond=0)
  2698. self.three_days_ago = before_now(days=3).replace(hour=10, minute=0, second=0, microsecond=0)
  2699. self.project = self.create_project()
  2700. self.url = reverse(
  2701. "sentry-api-0-organization-events-stats",
  2702. kwargs={"organization_id_or_slug": self.project.organization.slug},
  2703. )
  2704. def test_functions_dataset_simple(self):
  2705. self.store_functions(
  2706. [
  2707. {
  2708. "self_times_ns": [100 for _ in range(100)],
  2709. "package": "pkg",
  2710. "function": "foo",
  2711. "in_app": True,
  2712. },
  2713. {
  2714. "self_times_ns": [100 for _ in range(10)],
  2715. "package": "pkg",
  2716. "function": "bar",
  2717. "in_app": True,
  2718. },
  2719. ],
  2720. project=self.project,
  2721. timestamp=self.two_days_ago,
  2722. )
  2723. y_axes = [
  2724. "cpm()",
  2725. "p95(function.duration)",
  2726. "all_examples()",
  2727. ]
  2728. data = {
  2729. "dataset": "profileFunctions",
  2730. "field": ["function", "count()"],
  2731. "start": self.three_days_ago.isoformat(),
  2732. "end": self.one_day_ago.isoformat(),
  2733. "yAxis": y_axes,
  2734. "interval": "1d",
  2735. "topEvents": "2",
  2736. "excludeOther": "1",
  2737. }
  2738. response = self.client.get(self.url, data=data, format="json")
  2739. assert response.status_code == 200, response.content
  2740. assert sum(
  2741. row[1][0]["count"] for row in response.data["foo"]["cpm()"]["data"]
  2742. ) == pytest.approx(
  2743. 100 / ((self.one_day_ago - self.three_days_ago).total_seconds() / 60), rel=1e-3
  2744. )
  2745. assert sum(
  2746. row[1][0]["count"] for row in response.data["bar"]["cpm()"]["data"]
  2747. ) == pytest.approx(
  2748. 10 / ((self.one_day_ago - self.three_days_ago).total_seconds() / 60), rel=1e-3
  2749. )
  2750. assert any(
  2751. row[1][0]["count"] > 0 for row in response.data["foo"]["p95(function.duration)"]["data"]
  2752. )
  2753. assert any(
  2754. row[1][0]["count"] > 0 for row in response.data["bar"]["p95(function.duration)"]["data"]
  2755. )
  2756. for func in ["foo", "bar"]:
  2757. for y_axis in y_axes:
  2758. assert response.data[func][y_axis]["meta"]["units"] == {
  2759. "time": None,
  2760. "count": None,
  2761. "cpm": None,
  2762. "function": None,
  2763. "p95_function_duration": "nanosecond",
  2764. "all_examples": None,
  2765. }
  2766. class OrganizationEventsStatsTopNEventsErrors(APITestCase, SnubaTestCase):
  2767. def setUp(self):
  2768. super().setUp()
  2769. self.login_as(user=self.user)
  2770. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  2771. self.project = self.create_project()
  2772. self.project2 = self.create_project()
  2773. self.user2 = self.create_user()
  2774. self.event_data: list[_EventDataDict] = [
  2775. {
  2776. "data": {
  2777. "message": "poof",
  2778. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  2779. "user": {"email": self.user.email},
  2780. "tags": {"shared-tag": "yup"},
  2781. "fingerprint": ["group1"],
  2782. },
  2783. "project": self.project2,
  2784. "count": 7,
  2785. },
  2786. {
  2787. "data": {
  2788. "message": "voof",
  2789. "timestamp": (self.day_ago + timedelta(hours=1, minutes=2)).isoformat(),
  2790. "fingerprint": ["group2"],
  2791. "user": {"email": self.user2.email},
  2792. "tags": {"shared-tag": "yup"},
  2793. },
  2794. "project": self.project2,
  2795. "count": 6,
  2796. },
  2797. {
  2798. "data": {
  2799. "message": "very bad",
  2800. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  2801. "fingerprint": ["group3"],
  2802. "user": {"email": "foo@example.com"},
  2803. "tags": {"shared-tag": "yup"},
  2804. },
  2805. "project": self.project,
  2806. "count": 5,
  2807. },
  2808. {
  2809. "data": {
  2810. "message": "oh no",
  2811. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  2812. "fingerprint": ["group4"],
  2813. "user": {"email": "bar@example.com"},
  2814. "tags": {"shared-tag": "yup"},
  2815. },
  2816. "project": self.project,
  2817. "count": 4,
  2818. },
  2819. {
  2820. "data": {
  2821. "message": "kinda bad",
  2822. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  2823. "user": {"email": self.user.email},
  2824. "tags": {"shared-tag": "yup"},
  2825. "fingerprint": ["group7"],
  2826. },
  2827. "project": self.project,
  2828. "count": 3,
  2829. },
  2830. # Not in the top 5
  2831. {
  2832. "data": {
  2833. "message": "sorta bad",
  2834. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  2835. "fingerprint": ["group5"],
  2836. "user": {"email": "bar@example.com"},
  2837. "tags": {"shared-tag": "yup"},
  2838. },
  2839. "project": self.project,
  2840. "count": 2,
  2841. },
  2842. {
  2843. "data": {
  2844. "message": "not so bad",
  2845. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  2846. "fingerprint": ["group6"],
  2847. "user": {"email": "bar@example.com"},
  2848. "tags": {"shared-tag": "yup"},
  2849. },
  2850. "project": self.project,
  2851. "count": 1,
  2852. },
  2853. ]
  2854. self.events = []
  2855. for index, event_data in enumerate(self.event_data):
  2856. data = event_data["data"].copy()
  2857. for i in range(event_data["count"]):
  2858. data["event_id"] = f"{index}{i}" * 16
  2859. event = self.store_event(data, project_id=event_data["project"].id)
  2860. self.events.append(event)
  2861. self.enabled_features = {
  2862. "organizations:discover-basic": True,
  2863. }
  2864. self.url = reverse(
  2865. "sentry-api-0-organization-events-stats",
  2866. kwargs={"organization_id_or_slug": self.project.organization.slug},
  2867. )
  2868. def test_simple_top_events(self):
  2869. with self.feature(self.enabled_features):
  2870. response = self.client.get(
  2871. self.url,
  2872. data={
  2873. "start": self.day_ago.isoformat(),
  2874. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2875. "interval": "1h",
  2876. "yAxis": "count()",
  2877. "orderby": ["-count()"],
  2878. "field": ["count()", "message", "user.email"],
  2879. "dataset": "errors",
  2880. "topEvents": "5",
  2881. },
  2882. format="json",
  2883. )
  2884. data = response.data
  2885. assert response.status_code == 200, response.content
  2886. assert len(data) == 6
  2887. for index, event in enumerate(self.events[:5]):
  2888. message = event.message or event.transaction
  2889. results = data[
  2890. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  2891. ]
  2892. assert results["order"] == index
  2893. assert [{"count": self.event_data[index]["count"]}] in [
  2894. attrs for _, attrs in results["data"]
  2895. ]
  2896. other = data["Other"]
  2897. assert other["order"] == 5
  2898. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  2899. def test_top_events_with_projects_other(self):
  2900. with self.feature(self.enabled_features):
  2901. response = self.client.get(
  2902. self.url,
  2903. data={
  2904. "start": self.day_ago.isoformat(),
  2905. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2906. "interval": "1h",
  2907. "yAxis": "count()",
  2908. "orderby": ["-count()"],
  2909. "field": ["count()", "project"],
  2910. "dataset": "errors",
  2911. "topEvents": "1",
  2912. },
  2913. format="json",
  2914. )
  2915. data = response.data
  2916. assert response.status_code == 200, response.content
  2917. assert set(data.keys()) == {"Other", self.project.slug}
  2918. assert data[self.project.slug]["order"] == 0
  2919. assert [attrs[0]["count"] for _, attrs in data[self.project.slug]["data"]] == [15, 0]
  2920. assert data["Other"]["order"] == 1
  2921. assert [attrs[0]["count"] for _, attrs in data["Other"]["data"]] == [7, 6]
  2922. def test_top_events_with_issue(self):
  2923. # delete a group to make sure if this happens the value becomes unknown
  2924. event_group = self.events[0].group
  2925. event_group.delete()
  2926. with self.feature(self.enabled_features):
  2927. response = self.client.get(
  2928. self.url,
  2929. data={
  2930. "start": self.day_ago.isoformat(),
  2931. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2932. "interval": "1h",
  2933. "yAxis": "count()",
  2934. "orderby": ["-count()"],
  2935. "field": ["count()", "message", "issue"],
  2936. "topEvents": "5",
  2937. "query": "",
  2938. "dataset": "errors",
  2939. },
  2940. format="json",
  2941. )
  2942. data = response.data
  2943. assert response.status_code == 200, response.content
  2944. assert len(data) == 6
  2945. for index, event in enumerate(self.events[:4]):
  2946. message = event.message
  2947. # Because we deleted the group for event 0
  2948. if index == 0 or event.group is None:
  2949. issue = "unknown"
  2950. else:
  2951. issue = event.group.qualified_short_id
  2952. results = data[",".join([issue, message])]
  2953. assert results["order"] == index
  2954. assert [{"count": self.event_data[index]["count"]}] in [
  2955. attrs for time, attrs in results["data"]
  2956. ]
  2957. other = data["Other"]
  2958. assert other["order"] == 5
  2959. assert [attrs[0]["count"] for _, attrs in data["Other"]["data"]] == [3, 0]
  2960. @mock.patch("sentry.models.GroupManager.get_issues_mapping")
  2961. def test_top_events_with_unknown_issue(self, mock_issues_mapping):
  2962. event = self.events[0]
  2963. event_data = self.event_data[0]
  2964. # ensure that the issue mapping returns None for the issue
  2965. mock_issues_mapping.return_value = {event.group.id: None}
  2966. with self.feature(self.enabled_features):
  2967. response = self.client.get(
  2968. self.url,
  2969. data={
  2970. "start": self.day_ago.isoformat(),
  2971. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  2972. "interval": "1h",
  2973. "yAxis": "count()",
  2974. "orderby": ["-count()"],
  2975. "field": ["count()", "issue"],
  2976. "topEvents": "5",
  2977. # narrow the search to just one issue
  2978. "query": f"issue.id:{event.group.id}",
  2979. "dataset": "errors",
  2980. },
  2981. format="json",
  2982. )
  2983. assert response.status_code == 200, response.content
  2984. data = response.data
  2985. assert len(data) == 1
  2986. results = data["unknown"]
  2987. assert results["order"] == 0
  2988. assert [{"count": event_data["count"]}] in [attrs for time, attrs in results["data"]]
  2989. @mock.patch(
  2990. "sentry.search.events.builder.base.raw_snql_query",
  2991. side_effect=[{"data": [{"issue.id": 1}], "meta": []}, {"data": [], "meta": []}],
  2992. )
  2993. def test_top_events_with_issue_check_query_conditions(self, mock_query):
  2994. """ "Intentionally separate from test_top_events_with_issue
  2995. This is to test against a bug where the condition for issues wasn't included and we'd be missing data for
  2996. the interval since we'd cap out the max rows. This was not caught by the previous test since the results
  2997. would still be correct given the smaller interval & lack of data
  2998. """
  2999. with self.feature(self.enabled_features):
  3000. self.client.get(
  3001. self.url,
  3002. data={
  3003. "start": self.day_ago.isoformat(),
  3004. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  3005. "interval": "1h",
  3006. "yAxis": "count()",
  3007. "orderby": ["-count()"],
  3008. "field": ["count()", "message", "issue"],
  3009. "topEvents": "5",
  3010. "query": "!event.type:transaction",
  3011. "dataset": "errors",
  3012. },
  3013. format="json",
  3014. )
  3015. assert (
  3016. Condition(
  3017. Function(
  3018. "coalesce",
  3019. [Column("group_id", entity=Entity("events", alias="events")), 0],
  3020. "issue.id",
  3021. ),
  3022. Op.IN,
  3023. [1],
  3024. )
  3025. in mock_query.mock_calls[1].args[0].query.where
  3026. )
  3027. def test_group_id_tag_simple(self):
  3028. event_data: _EventDataDict = {
  3029. "data": {
  3030. "message": "poof",
  3031. "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(),
  3032. "user": {"email": self.user.email},
  3033. "tags": {"group_id": "the tag"},
  3034. "fingerprint": ["group1"],
  3035. },
  3036. "project": self.project2,
  3037. "count": 7,
  3038. }
  3039. for i in range(event_data["count"]):
  3040. event_data["data"]["event_id"] = f"a{i}" * 16
  3041. self.store_event(event_data["data"], project_id=event_data["project"].id)
  3042. data = {
  3043. "start": self.day_ago.isoformat(),
  3044. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  3045. "interval": "1h",
  3046. "yAxis": "count()",
  3047. "orderby": ["-count()"],
  3048. "field": ["count()", "group_id"],
  3049. "topEvents": "5",
  3050. "partial": "1",
  3051. }
  3052. with self.feature(self.enabled_features):
  3053. response = self.client.get(self.url, data, format="json")
  3054. assert response.status_code == 200, response.content
  3055. assert response.data["the tag"]["data"][0][1] == [{"count": 7}]
  3056. data["query"] = 'group_id:"the tag"'
  3057. with self.feature(self.enabled_features):
  3058. response = self.client.get(self.url, data, format="json")
  3059. assert response.status_code == 200
  3060. assert response.data["the tag"]["data"][0][1] == [{"count": 7}]
  3061. data["query"] = "group_id:abc"
  3062. with self.feature(self.enabled_features):
  3063. response = self.client.get(self.url, data, format="json")
  3064. assert response.status_code == 200
  3065. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  3066. def test_top_events_with_error_unhandled(self):
  3067. self.login_as(user=self.user)
  3068. project = self.create_project()
  3069. prototype = load_data("android-ndk")
  3070. prototype["event_id"] = "f" * 32
  3071. prototype["logentry"] = {"formatted": "not handled"}
  3072. prototype["exception"]["values"][0]["value"] = "not handled"
  3073. prototype["exception"]["values"][0]["mechanism"]["handled"] = False
  3074. prototype["timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat()
  3075. self.store_event(data=prototype, project_id=project.id)
  3076. with self.feature(self.enabled_features):
  3077. response = self.client.get(
  3078. self.url,
  3079. data={
  3080. "start": self.day_ago.isoformat(),
  3081. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  3082. "interval": "1h",
  3083. "yAxis": "count()",
  3084. "orderby": ["-count()"],
  3085. "field": ["count()", "error.unhandled"],
  3086. "topEvents": "5",
  3087. },
  3088. format="json",
  3089. )
  3090. data = response.data
  3091. assert response.status_code == 200, response.content
  3092. assert len(data) == 2