test_organization_events_stats.py 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207
  1. import uuid
  2. from datetime import timedelta
  3. from unittest import mock
  4. from uuid import uuid4
  5. import pytest
  6. from dateutil.parser import parse as parse_date
  7. from django.urls import reverse
  8. from pytz import utc
  9. from snuba_sdk.column import Column
  10. from snuba_sdk.conditions import Condition, Op
  11. from snuba_sdk.function import Function
  12. from sentry.constants import MAX_TOP_EVENTS
  13. from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric
  14. from sentry.snuba.discover import OTHER_KEY
  15. from sentry.testutils import APITestCase, SnubaTestCase
  16. from sentry.testutils.helpers.datetime import before_now, iso_format
  17. from sentry.testutils.silo import region_silo_test
  18. from sentry.utils.samples import load_data
  19. pytestmark = pytest.mark.sentry_metrics
  20. @region_silo_test
  21. class OrganizationEventsStatsEndpointTest(APITestCase, SnubaTestCase):
  22. endpoint = "sentry-api-0-organization-events-stats"
  23. def setUp(self):
  24. super().setUp()
  25. self.login_as(user=self.user)
  26. self.authed_user = self.user
  27. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  28. self.project = self.create_project()
  29. self.project2 = self.create_project()
  30. self.user = self.create_user()
  31. self.user2 = self.create_user()
  32. self.store_event(
  33. data={
  34. "event_id": "a" * 32,
  35. "message": "very bad",
  36. "timestamp": iso_format(self.day_ago + timedelta(minutes=1)),
  37. "fingerprint": ["group1"],
  38. "tags": {"sentry:user": self.user.email},
  39. },
  40. project_id=self.project.id,
  41. )
  42. self.store_event(
  43. data={
  44. "event_id": "b" * 32,
  45. "message": "oh my",
  46. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=1)),
  47. "fingerprint": ["group2"],
  48. "tags": {"sentry:user": self.user2.email},
  49. },
  50. project_id=self.project2.id,
  51. )
  52. self.store_event(
  53. data={
  54. "event_id": "c" * 32,
  55. "message": "very bad",
  56. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
  57. "fingerprint": ["group2"],
  58. "tags": {"sentry:user": self.user2.email},
  59. },
  60. project_id=self.project2.id,
  61. )
  62. self.url = reverse(
  63. "sentry-api-0-organization-events-stats",
  64. kwargs={"organization_slug": self.project.organization.slug},
  65. )
  66. self.features = {}
  67. def do_request(self, data, url=None, features=None):
  68. if features is None:
  69. features = {"organizations:discover-basic": True}
  70. features.update(self.features)
  71. with self.feature(features):
  72. return self.client.get(self.url if url is None else url, data=data, format="json")
  73. def test_simple(self):
  74. response = self.do_request(
  75. {
  76. "start": iso_format(self.day_ago),
  77. "end": iso_format(self.day_ago + timedelta(hours=2)),
  78. "interval": "1h",
  79. },
  80. )
  81. assert response.status_code == 200, response.content
  82. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  83. def test_misaligned_last_bucket(self):
  84. response = self.do_request(
  85. data={
  86. "start": iso_format(self.day_ago - timedelta(minutes=30)),
  87. "end": iso_format(self.day_ago + timedelta(hours=1, minutes=30)),
  88. "interval": "1h",
  89. "partial": "1",
  90. },
  91. )
  92. assert response.status_code == 200, response.content
  93. assert [attrs for time, attrs in response.data["data"]] == [
  94. [{"count": 0}],
  95. [{"count": 1}],
  96. [{"count": 2}],
  97. ]
  98. def test_no_projects(self):
  99. org = self.create_organization(owner=self.user)
  100. self.login_as(user=self.user)
  101. url = reverse(
  102. "sentry-api-0-organization-events-stats", kwargs={"organization_slug": org.slug}
  103. )
  104. response = self.do_request({}, url)
  105. assert response.status_code == 200, response.content
  106. assert len(response.data["data"]) == 0
  107. def test_user_count(self):
  108. self.store_event(
  109. data={
  110. "event_id": "d" * 32,
  111. "message": "something",
  112. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  113. "tags": {"sentry:user": self.user2.email},
  114. "fingerprint": ["group2"],
  115. },
  116. project_id=self.project2.id,
  117. )
  118. response = self.do_request(
  119. data={
  120. "start": iso_format(self.day_ago),
  121. "end": iso_format(self.day_ago + timedelta(hours=2)),
  122. "interval": "1h",
  123. "yAxis": "user_count",
  124. },
  125. )
  126. assert response.status_code == 200, response.content
  127. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 2}], [{"count": 1}]]
  128. def test_discover2_backwards_compatibility(self):
  129. response = self.do_request(
  130. data={
  131. "project": self.project.id,
  132. "start": iso_format(self.day_ago),
  133. "end": iso_format(self.day_ago + timedelta(hours=2)),
  134. "interval": "1h",
  135. "yAxis": "user_count",
  136. },
  137. )
  138. assert response.status_code == 200, response.content
  139. assert len(response.data["data"]) > 0
  140. response = self.do_request(
  141. data={
  142. "project": self.project.id,
  143. "start": iso_format(self.day_ago),
  144. "end": iso_format(self.day_ago + timedelta(hours=2)),
  145. "interval": "1h",
  146. "yAxis": "event_count",
  147. },
  148. )
  149. assert response.status_code == 200, response.content
  150. assert len(response.data["data"]) > 0
  151. def test_with_event_count_flag(self):
  152. response = self.do_request(
  153. data={
  154. "start": iso_format(self.day_ago),
  155. "end": iso_format(self.day_ago + timedelta(hours=2)),
  156. "interval": "1h",
  157. "yAxis": "event_count",
  158. },
  159. )
  160. assert response.status_code == 200, response.content
  161. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  162. def test_performance_view_feature(self):
  163. response = self.do_request(
  164. data={
  165. "end": iso_format(before_now()),
  166. "start": iso_format(before_now(hours=2)),
  167. "query": "project_id:1",
  168. "interval": "30m",
  169. "yAxis": "count()",
  170. },
  171. features={
  172. "organizations:performance-view": True,
  173. "organizations:discover-basic": False,
  174. },
  175. )
  176. assert response.status_code == 200, response.content
  177. def test_apdex_divide_by_zero(self):
  178. ProjectTransactionThreshold.objects.create(
  179. project=self.project,
  180. organization=self.project.organization,
  181. threshold=600,
  182. metric=TransactionMetric.LCP.value,
  183. )
  184. # Shouldn't count towards apdex
  185. data = load_data(
  186. "transaction",
  187. start_timestamp=self.day_ago + timedelta(minutes=(1)),
  188. timestamp=self.day_ago + timedelta(minutes=(3)),
  189. )
  190. data["transaction"] = "/apdex/new/"
  191. data["user"] = {"email": "1@example.com"}
  192. data["measurements"] = {}
  193. self.store_event(data, project_id=self.project.id)
  194. response = self.do_request(
  195. data={
  196. "start": iso_format(self.day_ago),
  197. "end": iso_format(self.day_ago + timedelta(hours=2)),
  198. "interval": "1h",
  199. "yAxis": "apdex()",
  200. "project": [self.project.id],
  201. },
  202. )
  203. assert response.status_code == 200, response.content
  204. assert len(response.data["data"]) == 2
  205. data = response.data["data"]
  206. # 0 transactions with LCP 0/0
  207. assert [attrs for time, attrs in response.data["data"]] == [
  208. [{"count": 0}],
  209. [{"count": 0}],
  210. ]
  211. def test_aggregate_function_apdex(self):
  212. project1 = self.create_project()
  213. project2 = self.create_project()
  214. events = [
  215. ("one", 400, project1.id),
  216. ("one", 400, project1.id),
  217. ("two", 3000, project2.id),
  218. ("two", 1000, project2.id),
  219. ("three", 3000, project2.id),
  220. ]
  221. for idx, event in enumerate(events):
  222. data = load_data(
  223. "transaction",
  224. start_timestamp=self.day_ago + timedelta(minutes=(1 + idx)),
  225. timestamp=self.day_ago + timedelta(minutes=(1 + idx), milliseconds=event[1]),
  226. )
  227. data["event_id"] = f"{idx}" * 32
  228. data["transaction"] = f"/apdex/new/{event[0]}"
  229. data["user"] = {"email": f"{idx}@example.com"}
  230. self.store_event(data, project_id=event[2])
  231. response = self.do_request(
  232. data={
  233. "start": iso_format(self.day_ago),
  234. "end": iso_format(self.day_ago + timedelta(hours=2)),
  235. "interval": "1h",
  236. "yAxis": "apdex()",
  237. },
  238. )
  239. assert response.status_code == 200, response.content
  240. assert [attrs for time, attrs in response.data["data"]] == [
  241. [{"count": 0.3}],
  242. [{"count": 0}],
  243. ]
  244. ProjectTransactionThreshold.objects.create(
  245. project=project1,
  246. organization=project1.organization,
  247. threshold=100,
  248. metric=TransactionMetric.DURATION.value,
  249. )
  250. ProjectTransactionThreshold.objects.create(
  251. project=project2,
  252. organization=project1.organization,
  253. threshold=100,
  254. metric=TransactionMetric.DURATION.value,
  255. )
  256. response = self.do_request(
  257. data={
  258. "start": iso_format(self.day_ago),
  259. "end": iso_format(self.day_ago + timedelta(hours=2)),
  260. "interval": "1h",
  261. "yAxis": "apdex()",
  262. },
  263. )
  264. assert response.status_code == 200, response.content
  265. assert [attrs for time, attrs in response.data["data"]] == [
  266. [{"count": 0.2}],
  267. [{"count": 0}],
  268. ]
  269. response = self.do_request(
  270. data={
  271. "start": iso_format(self.day_ago),
  272. "end": iso_format(self.day_ago + timedelta(hours=2)),
  273. "interval": "1h",
  274. "yAxis": ["user_count", "apdex()"],
  275. },
  276. )
  277. assert response.status_code == 200, response.content
  278. assert response.data["user_count"]["order"] == 0
  279. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  280. [{"count": 5}],
  281. [{"count": 0}],
  282. ]
  283. assert response.data["apdex()"]["order"] == 1
  284. assert [attrs for time, attrs in response.data["apdex()"]["data"]] == [
  285. [{"count": 0.2}],
  286. [{"count": 0}],
  287. ]
  288. def test_aggregate_function_count(self):
  289. response = self.do_request(
  290. data={
  291. "start": iso_format(self.day_ago),
  292. "end": iso_format(self.day_ago + timedelta(hours=2)),
  293. "interval": "1h",
  294. "yAxis": "count()",
  295. },
  296. )
  297. assert response.status_code == 200, response.content
  298. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  299. def test_invalid_aggregate(self):
  300. response = self.do_request(
  301. data={
  302. "start": iso_format(self.day_ago),
  303. "end": iso_format(self.day_ago + timedelta(hours=2)),
  304. "interval": "1h",
  305. "yAxis": "rubbish",
  306. },
  307. )
  308. assert response.status_code == 400, response.content
  309. def test_aggregate_function_user_count(self):
  310. response = self.do_request(
  311. data={
  312. "start": iso_format(self.day_ago),
  313. "end": iso_format(self.day_ago + timedelta(hours=2)),
  314. "interval": "1h",
  315. "yAxis": "count_unique(user)",
  316. },
  317. )
  318. assert response.status_code == 200, response.content
  319. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 1}]]
  320. def test_aggregate_invalid(self):
  321. response = self.do_request(
  322. data={
  323. "start": iso_format(self.day_ago),
  324. "end": iso_format(self.day_ago + timedelta(hours=2)),
  325. "interval": "1h",
  326. "yAxis": "nope(lol)",
  327. },
  328. )
  329. assert response.status_code == 400, response.content
  330. def test_throughput_epm_hour_rollup(self):
  331. project = self.create_project()
  332. # Each of these denotes how many events to create in each hour
  333. event_counts = [6, 0, 6, 3, 0, 3]
  334. for hour, count in enumerate(event_counts):
  335. for minute in range(count):
  336. self.store_event(
  337. data={
  338. "event_id": str(uuid.uuid1()),
  339. "message": "very bad",
  340. "timestamp": iso_format(
  341. self.day_ago + timedelta(hours=hour, minutes=minute)
  342. ),
  343. "fingerprint": ["group1"],
  344. "tags": {"sentry:user": self.user.email},
  345. },
  346. project_id=project.id,
  347. )
  348. for axis in ["epm()", "tpm()"]:
  349. response = self.do_request(
  350. data={
  351. "start": iso_format(self.day_ago),
  352. "end": iso_format(self.day_ago + timedelta(hours=6)),
  353. "interval": "1h",
  354. "yAxis": axis,
  355. "project": project.id,
  356. },
  357. )
  358. assert response.status_code == 200, response.content
  359. data = response.data["data"]
  360. assert len(data) == 6
  361. rows = data[0:6]
  362. for test in zip(event_counts, rows):
  363. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  364. def test_throughput_epm_day_rollup(self):
  365. project = self.create_project()
  366. # Each of these denotes how many events to create in each minute
  367. event_counts = [6, 0, 6, 3, 0, 3]
  368. for hour, count in enumerate(event_counts):
  369. for minute in range(count):
  370. self.store_event(
  371. data={
  372. "event_id": str(uuid.uuid1()),
  373. "message": "very bad",
  374. "timestamp": iso_format(
  375. self.day_ago + timedelta(hours=hour, minutes=minute)
  376. ),
  377. "fingerprint": ["group1"],
  378. "tags": {"sentry:user": self.user.email},
  379. },
  380. project_id=project.id,
  381. )
  382. for axis in ["epm()", "tpm()"]:
  383. response = self.do_request(
  384. data={
  385. "start": iso_format(self.day_ago),
  386. "end": iso_format(self.day_ago + timedelta(hours=24)),
  387. "interval": "24h",
  388. "yAxis": axis,
  389. "project": project.id,
  390. },
  391. )
  392. assert response.status_code == 200, response.content
  393. data = response.data["data"]
  394. assert len(data) == 2
  395. assert data[0][1][0]["count"] == sum(event_counts) / (86400.0 / 60.0)
  396. def test_throughput_eps_minute_rollup(self):
  397. project = self.create_project()
  398. # Each of these denotes how many events to create in each minute
  399. event_counts = [6, 0, 6, 3, 0, 3]
  400. for minute, count in enumerate(event_counts):
  401. for second in range(count):
  402. self.store_event(
  403. data={
  404. "event_id": str(uuid.uuid1()),
  405. "message": "very bad",
  406. "timestamp": iso_format(
  407. self.day_ago + timedelta(minutes=minute, seconds=second)
  408. ),
  409. "fingerprint": ["group1"],
  410. "tags": {"sentry:user": self.user.email},
  411. },
  412. project_id=project.id,
  413. )
  414. for axis in ["eps()", "tps()"]:
  415. response = self.do_request(
  416. data={
  417. "start": iso_format(self.day_ago),
  418. "end": iso_format(self.day_ago + timedelta(minutes=6)),
  419. "interval": "1m",
  420. "yAxis": axis,
  421. "project": project.id,
  422. },
  423. )
  424. assert response.status_code == 200, response.content
  425. data = response.data["data"]
  426. assert len(data) == 6
  427. rows = data[0:6]
  428. for test in zip(event_counts, rows):
  429. assert test[1][1][0]["count"] == test[0] / 60.0
  430. def test_throughput_eps_no_rollup(self):
  431. project = self.create_project()
  432. # Each of these denotes how many events to create in each minute
  433. event_counts = [6, 0, 6, 3, 0, 3]
  434. for minute, count in enumerate(event_counts):
  435. for second in range(count):
  436. self.store_event(
  437. data={
  438. "event_id": str(uuid.uuid1()),
  439. "message": "very bad",
  440. "timestamp": iso_format(
  441. self.day_ago + timedelta(minutes=minute, seconds=second)
  442. ),
  443. "fingerprint": ["group1"],
  444. "tags": {"sentry:user": self.user.email},
  445. },
  446. project_id=project.id,
  447. )
  448. response = self.do_request(
  449. data={
  450. "start": iso_format(self.day_ago),
  451. "end": iso_format(self.day_ago + timedelta(minutes=1)),
  452. "interval": "1s",
  453. "yAxis": "eps()",
  454. "project": project.id,
  455. },
  456. )
  457. assert response.status_code == 200, response.content
  458. data = response.data["data"]
  459. # expect 60 data points between time span of 0 and 60 seconds
  460. assert len(data) == 60
  461. rows = data[0:6]
  462. for row in rows:
  463. assert row[1][0]["count"] == 1
  464. def test_transaction_events(self):
  465. prototype = {
  466. "type": "transaction",
  467. "transaction": "api.issue.delete",
  468. "spans": [],
  469. "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
  470. "tags": {"important": "yes"},
  471. }
  472. fixtures = (
  473. ("d" * 32, before_now(minutes=32)),
  474. ("e" * 32, before_now(hours=1, minutes=2)),
  475. ("f" * 32, before_now(hours=1, minutes=35)),
  476. )
  477. for fixture in fixtures:
  478. data = prototype.copy()
  479. data["event_id"] = fixture[0]
  480. data["timestamp"] = iso_format(fixture[1])
  481. data["start_timestamp"] = iso_format(fixture[1] - timedelta(seconds=1))
  482. self.store_event(data=data, project_id=self.project.id)
  483. response = self.do_request(
  484. data={
  485. "project": self.project.id,
  486. "end": iso_format(before_now()),
  487. "start": iso_format(before_now(hours=2)),
  488. "query": "event.type:transaction",
  489. "interval": "30m",
  490. "yAxis": "count()",
  491. },
  492. )
  493. assert response.status_code == 200, response.content
  494. items = [item for time, item in response.data["data"] if item]
  495. # We could get more results depending on where the 30 min
  496. # windows land.
  497. assert len(items) >= 3
  498. def test_project_id_query_filter(self):
  499. response = self.do_request(
  500. data={
  501. "end": iso_format(before_now()),
  502. "start": iso_format(before_now(hours=2)),
  503. "query": "project_id:1",
  504. "interval": "30m",
  505. "yAxis": "count()",
  506. },
  507. )
  508. assert response.status_code == 200
  509. def test_latest_release_query_filter(self):
  510. response = self.do_request(
  511. data={
  512. "project": self.project.id,
  513. "end": iso_format(before_now()),
  514. "start": iso_format(before_now(hours=2)),
  515. "query": "release:latest",
  516. "interval": "30m",
  517. "yAxis": "count()",
  518. },
  519. )
  520. assert response.status_code == 200
  521. def test_conditional_filter(self):
  522. response = self.do_request(
  523. data={
  524. "start": iso_format(self.day_ago),
  525. "end": iso_format(self.day_ago + timedelta(hours=2)),
  526. "query": "id:{} OR id:{}".format("a" * 32, "b" * 32),
  527. "interval": "30m",
  528. "yAxis": "count()",
  529. },
  530. )
  531. assert response.status_code == 200, response.content
  532. data = response.data["data"]
  533. assert len(data) == 4
  534. assert data[0][1][0]["count"] == 1
  535. assert data[2][1][0]["count"] == 1
  536. def test_simple_multiple_yaxis(self):
  537. response = self.do_request(
  538. data={
  539. "start": iso_format(self.day_ago),
  540. "end": iso_format(self.day_ago + timedelta(hours=2)),
  541. "interval": "1h",
  542. "yAxis": ["user_count", "event_count"],
  543. },
  544. )
  545. assert response.status_code == 200, response.content
  546. assert response.data["user_count"]["order"] == 0
  547. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  548. [{"count": 1}],
  549. [{"count": 1}],
  550. ]
  551. assert response.data["event_count"]["order"] == 1
  552. assert [attrs for time, attrs in response.data["event_count"]["data"]] == [
  553. [{"count": 1}],
  554. [{"count": 2}],
  555. ]
  556. def test_equation_yaxis(self):
  557. response = self.do_request(
  558. data={
  559. "start": iso_format(self.day_ago),
  560. "end": iso_format(self.day_ago + timedelta(hours=2)),
  561. "interval": "1h",
  562. "yAxis": ["equation|count() / 100"],
  563. },
  564. )
  565. assert response.status_code == 200, response.content
  566. assert len(response.data["data"]) == 2
  567. assert [attrs for time, attrs in response.data["data"]] == [
  568. [{"count": 0.01}],
  569. [{"count": 0.02}],
  570. ]
  571. def test_equation_mixed_multi_yaxis(self):
  572. response = self.do_request(
  573. data={
  574. "start": iso_format(self.day_ago),
  575. "end": iso_format(self.day_ago + timedelta(hours=2)),
  576. "interval": "1h",
  577. "yAxis": ["count()", "equation|count() * 100"],
  578. },
  579. )
  580. assert response.status_code == 200, response.content
  581. assert response.data["count()"]["order"] == 0
  582. assert [attrs for time, attrs in response.data["count()"]["data"]] == [
  583. [{"count": 1}],
  584. [{"count": 2}],
  585. ]
  586. assert response.data["equation|count() * 100"]["order"] == 1
  587. assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
  588. [{"count": 100}],
  589. [{"count": 200}],
  590. ]
  591. def test_equation_multi_yaxis(self):
  592. response = self.do_request(
  593. data={
  594. "start": iso_format(self.day_ago),
  595. "end": iso_format(self.day_ago + timedelta(hours=2)),
  596. "interval": "1h",
  597. "yAxis": ["equation|count() / 100", "equation|count() * 100"],
  598. },
  599. )
  600. assert response.status_code == 200, response.content
  601. assert response.data["equation|count() / 100"]["order"] == 0
  602. assert [attrs for time, attrs in response.data["equation|count() / 100"]["data"]] == [
  603. [{"count": 0.01}],
  604. [{"count": 0.02}],
  605. ]
  606. assert response.data["equation|count() * 100"]["order"] == 1
  607. assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
  608. [{"count": 100}],
  609. [{"count": 200}],
  610. ]
  611. def test_large_interval_no_drop_values(self):
  612. self.store_event(
  613. data={
  614. "event_id": "d" * 32,
  615. "message": "not good",
  616. "timestamp": iso_format(self.day_ago - timedelta(minutes=10)),
  617. "fingerprint": ["group3"],
  618. },
  619. project_id=self.project.id,
  620. )
  621. response = self.do_request(
  622. data={
  623. "project": self.project.id,
  624. "end": iso_format(self.day_ago),
  625. "start": iso_format(self.day_ago - timedelta(hours=24)),
  626. "query": 'message:"not good"',
  627. "interval": "1d",
  628. "yAxis": "count()",
  629. },
  630. )
  631. assert response.status_code == 200
  632. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 0}], [{"count": 1}]]
  633. @mock.patch("sentry.snuba.discover.timeseries_query", return_value={})
  634. def test_multiple_yaxis_only_one_query(self, mock_query):
  635. self.do_request(
  636. data={
  637. "project": self.project.id,
  638. "start": iso_format(self.day_ago),
  639. "end": iso_format(self.day_ago + timedelta(hours=2)),
  640. "interval": "1h",
  641. "yAxis": ["user_count", "event_count", "epm()", "eps()"],
  642. },
  643. )
  644. assert mock_query.call_count == 1
  645. @mock.patch("sentry.snuba.discover.bulk_snql_query", return_value=[{"data": []}])
  646. def test_invalid_interval(self, mock_query):
  647. self.do_request(
  648. data={
  649. "end": iso_format(before_now()),
  650. "start": iso_format(before_now(hours=24)),
  651. "query": "",
  652. "interval": "1s",
  653. "yAxis": "count()",
  654. },
  655. )
  656. assert mock_query.call_count == 1
  657. # Should've reset to the default for 24h
  658. assert mock_query.mock_calls[0].args[0][0].query.granularity.granularity == 300
  659. self.do_request(
  660. data={
  661. "end": iso_format(before_now()),
  662. "start": iso_format(before_now(hours=24)),
  663. "query": "",
  664. "interval": "0d",
  665. "yAxis": "count()",
  666. },
  667. )
  668. assert mock_query.call_count == 2
  669. # Should've reset to the default for 24h
  670. assert mock_query.mock_calls[1].args[0][0].query.granularity.granularity == 300
  671. def test_out_of_retention(self):
  672. with self.options({"system.event-retention-days": 10}):
  673. response = self.do_request(
  674. data={
  675. "start": iso_format(before_now(days=20)),
  676. "end": iso_format(before_now(days=15)),
  677. "query": "",
  678. "interval": "30m",
  679. "yAxis": "count()",
  680. },
  681. )
  682. assert response.status_code == 400
  683. @mock.patch("sentry.utils.snuba.quantize_time")
  684. def test_quantize_dates(self, mock_quantize):
  685. mock_quantize.return_value = before_now(days=1).replace(tzinfo=utc)
  686. # Don't quantize short time periods
  687. self.do_request(
  688. data={"statsPeriod": "1h", "query": "", "interval": "30m", "yAxis": "count()"},
  689. )
  690. # Don't quantize absolute date periods
  691. self.do_request(
  692. data={
  693. "start": iso_format(before_now(days=20)),
  694. "end": iso_format(before_now(days=15)),
  695. "query": "",
  696. "interval": "30m",
  697. "yAxis": "count()",
  698. },
  699. )
  700. assert len(mock_quantize.mock_calls) == 0
  701. # Quantize long date periods
  702. self.do_request(
  703. data={"statsPeriod": "90d", "query": "", "interval": "30m", "yAxis": "count()"},
  704. )
  705. assert len(mock_quantize.mock_calls) == 2
  706. def test_with_zerofill(self):
  707. response = self.do_request(
  708. data={
  709. "start": iso_format(self.day_ago),
  710. "end": iso_format(self.day_ago + timedelta(hours=2)),
  711. "interval": "30m",
  712. },
  713. )
  714. assert response.status_code == 200, response.content
  715. assert [attrs for time, attrs in response.data["data"]] == [
  716. [{"count": 1}],
  717. [{"count": 0}],
  718. [{"count": 2}],
  719. [{"count": 0}],
  720. ]
  721. def test_without_zerofill(self):
  722. start = iso_format(self.day_ago)
  723. end = iso_format(self.day_ago + timedelta(hours=2))
  724. response = self.do_request(
  725. data={
  726. "start": start,
  727. "end": end,
  728. "interval": "30m",
  729. "withoutZerofill": "1",
  730. },
  731. features={
  732. "organizations:performance-chart-interpolation": True,
  733. "organizations:discover-basic": True,
  734. },
  735. )
  736. assert response.status_code == 200, response.content
  737. assert [attrs for time, attrs in response.data["data"]] == [
  738. [{"count": 1}],
  739. [{"count": 2}],
  740. ]
  741. assert response.data["start"] == parse_date(start).timestamp()
  742. assert response.data["end"] == parse_date(end).timestamp()
  743. def test_comparison(self):
  744. self.store_event(
  745. data={
  746. "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=1)),
  747. },
  748. project_id=self.project.id,
  749. )
  750. self.store_event(
  751. data={
  752. "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=2)),
  753. },
  754. project_id=self.project.id,
  755. )
  756. self.store_event(
  757. data={
  758. "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1, minutes=1)),
  759. },
  760. project_id=self.project2.id,
  761. )
  762. response = self.do_request(
  763. data={
  764. "start": iso_format(self.day_ago),
  765. "end": iso_format(self.day_ago + timedelta(hours=2)),
  766. "interval": "1h",
  767. "comparisonDelta": int(timedelta(days=1).total_seconds()),
  768. }
  769. )
  770. assert response.status_code == 200, response.content
  771. assert [attrs for time, attrs in response.data["data"]] == [
  772. [{"count": 1, "comparisonCount": 2}],
  773. [{"count": 2, "comparisonCount": 1}],
  774. ]
  775. def test_comparison_invalid(self):
  776. response = self.do_request(
  777. data={
  778. "start": iso_format(self.day_ago),
  779. "end": iso_format(self.day_ago + timedelta(hours=2)),
  780. "interval": "1h",
  781. "comparisonDelta": "17h",
  782. },
  783. )
  784. assert response.status_code == 400, response.content
  785. assert response.data["detail"] == "comparisonDelta must be an integer"
  786. start = before_now(days=85)
  787. end = start + timedelta(days=7)
  788. with self.options({"system.event-retention-days": 90}):
  789. response = self.do_request(
  790. data={
  791. "start": iso_format(start),
  792. "end": iso_format(end),
  793. "interval": "1h",
  794. "comparisonDelta": int(timedelta(days=7).total_seconds()),
  795. }
  796. )
  797. assert response.status_code == 400, response.content
  798. assert response.data["detail"] == "Comparison period is outside retention window"
  799. def test_equations_divide_by_zero(self):
  800. response = self.do_request(
  801. data={
  802. "start": iso_format(self.day_ago),
  803. "end": iso_format(self.day_ago + timedelta(hours=2)),
  804. "interval": "1h",
  805. # force a 0 in the denominator by doing 1 - 1
  806. # since a 0 literal is illegal as the denominator
  807. "yAxis": ["equation|count() / (1-1)"],
  808. },
  809. )
  810. assert response.status_code == 200, response.content
  811. assert len(response.data["data"]) == 2
  812. assert [attrs for time, attrs in response.data["data"]] == [
  813. [{"count": None}],
  814. [{"count": None}],
  815. ]
  816. @region_silo_test
  817. class OrganizationEventsStatsTopNEvents(APITestCase, SnubaTestCase):
  818. def setUp(self):
  819. super().setUp()
  820. self.login_as(user=self.user)
  821. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  822. self.project = self.create_project()
  823. self.project2 = self.create_project()
  824. self.user2 = self.create_user()
  825. transaction_data = load_data("transaction")
  826. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  827. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=4))
  828. transaction_data["tags"] = {"shared-tag": "yup"}
  829. self.event_data = [
  830. {
  831. "data": {
  832. "message": "poof",
  833. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  834. "user": {"email": self.user.email},
  835. "tags": {"shared-tag": "yup"},
  836. "fingerprint": ["group1"],
  837. },
  838. "project": self.project2,
  839. "count": 7,
  840. },
  841. {
  842. "data": {
  843. "message": "voof",
  844. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
  845. "fingerprint": ["group2"],
  846. "user": {"email": self.user2.email},
  847. "tags": {"shared-tag": "yup"},
  848. },
  849. "project": self.project2,
  850. "count": 6,
  851. },
  852. {
  853. "data": {
  854. "message": "very bad",
  855. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  856. "fingerprint": ["group3"],
  857. "user": {"email": "foo@example.com"},
  858. "tags": {"shared-tag": "yup"},
  859. },
  860. "project": self.project,
  861. "count": 5,
  862. },
  863. {
  864. "data": {
  865. "message": "oh no",
  866. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  867. "fingerprint": ["group4"],
  868. "user": {"email": "bar@example.com"},
  869. "tags": {"shared-tag": "yup"},
  870. },
  871. "project": self.project,
  872. "count": 4,
  873. },
  874. {"data": transaction_data, "project": self.project, "count": 3},
  875. # Not in the top 5
  876. {
  877. "data": {
  878. "message": "sorta bad",
  879. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  880. "fingerprint": ["group5"],
  881. "user": {"email": "bar@example.com"},
  882. "tags": {"shared-tag": "yup"},
  883. },
  884. "project": self.project,
  885. "count": 2,
  886. },
  887. {
  888. "data": {
  889. "message": "not so bad",
  890. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  891. "fingerprint": ["group6"],
  892. "user": {"email": "bar@example.com"},
  893. "tags": {"shared-tag": "yup"},
  894. },
  895. "project": self.project,
  896. "count": 1,
  897. },
  898. ]
  899. self.events = []
  900. for index, event_data in enumerate(self.event_data):
  901. data = event_data["data"].copy()
  902. event = {}
  903. for i in range(event_data["count"]):
  904. data["event_id"] = f"{index}{i}" * 16
  905. event = self.store_event(data, project_id=event_data["project"].id)
  906. self.events.append(event)
  907. self.transaction = self.events[4]
  908. self.enabled_features = {
  909. "organizations:discover-basic": True,
  910. }
  911. self.url = reverse(
  912. "sentry-api-0-organization-events-stats",
  913. kwargs={"organization_slug": self.project.organization.slug},
  914. )
  915. def test_no_top_events_with_project_field(self):
  916. project = self.create_project()
  917. with self.feature(self.enabled_features):
  918. response = self.client.get(
  919. self.url,
  920. data={
  921. # make sure to query the project with 0 events
  922. "project": project.id,
  923. "start": iso_format(self.day_ago),
  924. "end": iso_format(self.day_ago + timedelta(hours=2)),
  925. "interval": "1h",
  926. "yAxis": "count()",
  927. "orderby": ["-count()"],
  928. "field": ["count()", "project"],
  929. "topEvents": 5,
  930. },
  931. format="json",
  932. )
  933. assert response.status_code == 200, response.content
  934. # When there are no top events, we do not return an empty dict.
  935. # Instead, we return a single zero-filled series for an empty graph.
  936. data = response.data["data"]
  937. assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
  938. def test_no_top_events(self):
  939. project = self.create_project()
  940. with self.feature(self.enabled_features):
  941. response = self.client.get(
  942. self.url,
  943. data={
  944. # make sure to query the project with 0 events
  945. "project": project.id,
  946. "start": iso_format(self.day_ago),
  947. "end": iso_format(self.day_ago + timedelta(hours=2)),
  948. "interval": "1h",
  949. "yAxis": "count()",
  950. "orderby": ["-count()"],
  951. "field": ["count()", "message", "user.email"],
  952. "topEvents": 5,
  953. },
  954. format="json",
  955. )
  956. data = response.data["data"]
  957. assert response.status_code == 200, response.content
  958. # When there are no top events, we do not return an empty dict.
  959. # Instead, we return a single zero-filled series for an empty graph.
  960. assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
  961. def test_no_top_events_with_multi_axis(self):
  962. project = self.create_project()
  963. with self.feature(self.enabled_features):
  964. response = self.client.get(
  965. self.url,
  966. data={
  967. # make sure to query the project with 0 events
  968. "project": project.id,
  969. "start": iso_format(self.day_ago),
  970. "end": iso_format(self.day_ago + timedelta(hours=2)),
  971. "interval": "1h",
  972. "yAxis": ["count()", "count_unique(user)"],
  973. "orderby": ["-count()"],
  974. "field": ["count()", "count_unique(user)", "message", "user.email"],
  975. "topEvents": 5,
  976. },
  977. format="json",
  978. )
  979. assert response.status_code == 200
  980. data = response.data[""]
  981. assert [attrs for time, attrs in data["count()"]["data"]] == [
  982. [{"count": 0}],
  983. [{"count": 0}],
  984. ]
  985. assert [attrs for time, attrs in data["count_unique(user)"]["data"]] == [
  986. [{"count": 0}],
  987. [{"count": 0}],
  988. ]
  989. def test_simple_top_events(self):
  990. with self.feature(self.enabled_features):
  991. response = self.client.get(
  992. self.url,
  993. data={
  994. "start": iso_format(self.day_ago),
  995. "end": iso_format(self.day_ago + timedelta(hours=2)),
  996. "interval": "1h",
  997. "yAxis": "count()",
  998. "orderby": ["-count()"],
  999. "field": ["count()", "message", "user.email"],
  1000. "topEvents": 5,
  1001. },
  1002. format="json",
  1003. )
  1004. data = response.data
  1005. assert response.status_code == 200, response.content
  1006. assert len(data) == 6
  1007. for index, event in enumerate(self.events[:5]):
  1008. message = event.message or event.transaction
  1009. results = data[
  1010. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1011. ]
  1012. assert results["order"] == index
  1013. assert [{"count": self.event_data[index]["count"]}] in [
  1014. attrs for _, attrs in results["data"]
  1015. ]
  1016. other = data["Other"]
  1017. assert other["order"] == 5
  1018. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1019. def test_top_events_limits(self):
  1020. data = {
  1021. "start": iso_format(self.day_ago),
  1022. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1023. "interval": "1h",
  1024. "yAxis": "count()",
  1025. "orderby": ["-count()"],
  1026. "field": ["count()", "message", "user.email"],
  1027. }
  1028. with self.feature(self.enabled_features):
  1029. data["topEvents"] = MAX_TOP_EVENTS + 1
  1030. response = self.client.get(self.url, data, format="json")
  1031. assert response.status_code == 400
  1032. data["topEvents"] = 0
  1033. response = self.client.get(self.url, data, format="json")
  1034. assert response.status_code == 400
  1035. data["topEvents"] = "a"
  1036. response = self.client.get(self.url, data, format="json")
  1037. assert response.status_code == 400
  1038. def test_top_events_with_projects(self):
  1039. with self.feature(self.enabled_features):
  1040. response = self.client.get(
  1041. self.url,
  1042. data={
  1043. "start": iso_format(self.day_ago),
  1044. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1045. "interval": "1h",
  1046. "yAxis": "count()",
  1047. "orderby": ["-count()"],
  1048. "field": ["count()", "message", "project"],
  1049. "topEvents": 5,
  1050. },
  1051. format="json",
  1052. )
  1053. data = response.data
  1054. assert response.status_code == 200, response.content
  1055. assert len(data) == 6
  1056. for index, event in enumerate(self.events[:5]):
  1057. message = event.message or event.transaction
  1058. results = data[",".join([message, event.project.slug])]
  1059. assert results["order"] == index
  1060. assert [{"count": self.event_data[index]["count"]}] in [
  1061. attrs for time, attrs in results["data"]
  1062. ]
  1063. other = data["Other"]
  1064. assert other["order"] == 5
  1065. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1066. def test_top_events_with_issue(self):
  1067. # delete a group to make sure if this happens the value becomes unknown
  1068. event_group = self.events[0].group
  1069. event_group.delete()
  1070. with self.feature(self.enabled_features):
  1071. response = self.client.get(
  1072. self.url,
  1073. data={
  1074. "start": iso_format(self.day_ago),
  1075. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1076. "interval": "1h",
  1077. "yAxis": "count()",
  1078. "orderby": ["-count()"],
  1079. "field": ["count()", "message", "issue"],
  1080. "topEvents": 5,
  1081. "query": "!event.type:transaction",
  1082. },
  1083. format="json",
  1084. )
  1085. data = response.data
  1086. assert response.status_code == 200, response.content
  1087. assert len(data) == 6
  1088. for index, event in enumerate(self.events[:4]):
  1089. message = event.message
  1090. # Because we deleted the group for event 0
  1091. if index == 0 or event.group is None:
  1092. issue = "unknown"
  1093. else:
  1094. issue = event.group.qualified_short_id
  1095. results = data[",".join([issue, message])]
  1096. assert results["order"] == index
  1097. assert [{"count": self.event_data[index]["count"]}] in [
  1098. attrs for time, attrs in results["data"]
  1099. ]
  1100. other = data["Other"]
  1101. assert other["order"] == 5
  1102. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1103. def test_top_events_with_transaction_status(self):
  1104. with self.feature(self.enabled_features):
  1105. response = self.client.get(
  1106. self.url,
  1107. data={
  1108. "start": iso_format(self.day_ago),
  1109. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1110. "interval": "1h",
  1111. "yAxis": "count()",
  1112. "orderby": ["-count()"],
  1113. "field": ["count()", "transaction.status"],
  1114. "topEvents": 5,
  1115. },
  1116. format="json",
  1117. )
  1118. data = response.data
  1119. assert response.status_code == 200, response.content
  1120. assert len(data) == 1
  1121. assert "ok" in data
  1122. @mock.patch("sentry.models.GroupManager.get_issues_mapping")
  1123. def test_top_events_with_unknown_issue(self, mock_issues_mapping):
  1124. event = self.events[0]
  1125. event_data = self.event_data[0]
  1126. # ensure that the issue mapping returns None for the issue
  1127. mock_issues_mapping.return_value = {event.group.id: None}
  1128. with self.feature(self.enabled_features):
  1129. response = self.client.get(
  1130. self.url,
  1131. data={
  1132. "start": iso_format(self.day_ago),
  1133. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1134. "interval": "1h",
  1135. "yAxis": "count()",
  1136. "orderby": ["-count()"],
  1137. "field": ["count()", "issue"],
  1138. "topEvents": 5,
  1139. # narrow the search to just one issue
  1140. "query": f"issue.id:{event.group.id}",
  1141. },
  1142. format="json",
  1143. )
  1144. assert response.status_code == 200, response.content
  1145. data = response.data
  1146. assert len(data) == 1
  1147. results = data["unknown"]
  1148. assert results["order"] == 0
  1149. assert [{"count": event_data["count"]}] in [attrs for time, attrs in results["data"]]
  1150. @mock.patch(
  1151. "sentry.search.events.builder.raw_snql_query",
  1152. side_effect=[{"data": [{"issue.id": 1}], "meta": []}, {"data": [], "meta": []}],
  1153. )
  1154. def test_top_events_with_issue_check_query_conditions(self, mock_query):
  1155. """ "Intentionally separate from test_top_events_with_issue
  1156. This is to test against a bug where the condition for issues wasn't included and we'd be missing data for
  1157. the interval since we'd cap out the max rows. This was not caught by the previous test since the results
  1158. would still be correct given the smaller interval & lack of data
  1159. """
  1160. with self.feature(self.enabled_features):
  1161. self.client.get(
  1162. self.url,
  1163. data={
  1164. "start": iso_format(self.day_ago),
  1165. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1166. "interval": "1h",
  1167. "yAxis": "count()",
  1168. "orderby": ["-count()"],
  1169. "field": ["count()", "message", "issue"],
  1170. "topEvents": 5,
  1171. "query": "!event.type:transaction",
  1172. },
  1173. format="json",
  1174. )
  1175. assert (
  1176. Condition(Function("coalesce", [Column("group_id"), 0], "issue.id"), Op.IN, [1])
  1177. in mock_query.mock_calls[1].args[0].query.where
  1178. )
  1179. def test_top_events_with_functions(self):
  1180. with self.feature(self.enabled_features):
  1181. response = self.client.get(
  1182. self.url,
  1183. data={
  1184. "start": iso_format(self.day_ago),
  1185. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1186. "interval": "1h",
  1187. "yAxis": "count()",
  1188. "orderby": ["-p99()"],
  1189. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1190. "topEvents": 5,
  1191. },
  1192. format="json",
  1193. )
  1194. data = response.data
  1195. assert response.status_code == 200, response.content
  1196. assert len(data) == 1
  1197. results = data[self.transaction.transaction]
  1198. assert results["order"] == 0
  1199. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1200. def test_top_events_with_functions_on_different_transactions(self):
  1201. """Transaction2 has less events, but takes longer so order should be self.transaction then transaction2"""
  1202. transaction_data = load_data("transaction")
  1203. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1204. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  1205. transaction_data["transaction"] = "/foo_bar/"
  1206. transaction2 = self.store_event(transaction_data, project_id=self.project.id)
  1207. with self.feature(self.enabled_features):
  1208. response = self.client.get(
  1209. self.url,
  1210. data={
  1211. "start": iso_format(self.day_ago),
  1212. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1213. "interval": "1h",
  1214. "yAxis": "count()",
  1215. "orderby": ["-p99()"],
  1216. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1217. "topEvents": 5,
  1218. },
  1219. format="json",
  1220. )
  1221. data = response.data
  1222. assert response.status_code == 200, response.content
  1223. assert len(data) == 2
  1224. results = data[self.transaction.transaction]
  1225. assert results["order"] == 1
  1226. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1227. results = data[transaction2.transaction]
  1228. assert results["order"] == 0
  1229. assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
  1230. def test_top_events_with_query(self):
  1231. transaction_data = load_data("transaction")
  1232. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1233. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  1234. transaction_data["transaction"] = "/foo_bar/"
  1235. self.store_event(transaction_data, project_id=self.project.id)
  1236. with self.feature(self.enabled_features):
  1237. response = self.client.get(
  1238. self.url,
  1239. data={
  1240. "start": iso_format(self.day_ago),
  1241. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1242. "interval": "1h",
  1243. "yAxis": "count()",
  1244. "orderby": ["-p99()"],
  1245. "query": "transaction:/foo_bar/",
  1246. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1247. "topEvents": 5,
  1248. },
  1249. format="json",
  1250. )
  1251. data = response.data
  1252. assert response.status_code == 200, response.content
  1253. assert len(data) == 1
  1254. transaction2_data = data["/foo_bar/"]
  1255. assert transaction2_data["order"] == 0
  1256. assert [attrs for time, attrs in transaction2_data["data"]] == [
  1257. [{"count": 1}],
  1258. [{"count": 0}],
  1259. ]
  1260. def test_top_events_with_negated_condition(self):
  1261. with self.feature(self.enabled_features):
  1262. response = self.client.get(
  1263. self.url,
  1264. data={
  1265. "start": iso_format(self.day_ago),
  1266. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1267. "interval": "1h",
  1268. "yAxis": "count()",
  1269. "orderby": ["-count()"],
  1270. "query": f"!message:{self.events[0].message}",
  1271. "field": ["message", "count()"],
  1272. "topEvents": 5,
  1273. },
  1274. format="json",
  1275. )
  1276. data = response.data
  1277. assert response.status_code == 200, response.content
  1278. assert len(data) == 6
  1279. for index, event in enumerate(self.events[1:5]):
  1280. message = event.message or event.transaction
  1281. results = data[message]
  1282. assert results["order"] == index
  1283. assert [{"count": self.event_data[index + 1]["count"]}] in [
  1284. attrs for _, attrs in results["data"]
  1285. ]
  1286. other = data["Other"]
  1287. assert other["order"] == 5
  1288. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1289. def test_top_events_with_epm(self):
  1290. with self.feature(self.enabled_features):
  1291. response = self.client.get(
  1292. self.url,
  1293. data={
  1294. "start": iso_format(self.day_ago),
  1295. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1296. "interval": "1h",
  1297. "yAxis": "epm()",
  1298. "orderby": ["-count()"],
  1299. "field": ["message", "user.email", "count()"],
  1300. "topEvents": 5,
  1301. },
  1302. format="json",
  1303. )
  1304. data = response.data
  1305. assert response.status_code == 200, response.content
  1306. assert len(data) == 6
  1307. for index, event in enumerate(self.events[:5]):
  1308. message = event.message or event.transaction
  1309. results = data[
  1310. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1311. ]
  1312. assert results["order"] == index
  1313. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  1314. attrs for time, attrs in results["data"]
  1315. ]
  1316. other = data["Other"]
  1317. assert other["order"] == 5
  1318. assert [{"count": 0.05}] in [attrs for _, attrs in other["data"]]
  1319. def test_top_events_with_multiple_yaxis(self):
  1320. with self.feature(self.enabled_features):
  1321. response = self.client.get(
  1322. self.url,
  1323. data={
  1324. "start": iso_format(self.day_ago),
  1325. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1326. "interval": "1h",
  1327. "yAxis": ["epm()", "count()"],
  1328. "orderby": ["-count()"],
  1329. "field": ["message", "user.email", "count()"],
  1330. "topEvents": 5,
  1331. },
  1332. format="json",
  1333. )
  1334. data = response.data
  1335. assert response.status_code == 200, response.content
  1336. assert len(data) == 6
  1337. for index, event in enumerate(self.events[:5]):
  1338. message = event.message or event.transaction
  1339. results = data[
  1340. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1341. ]
  1342. assert results["order"] == index
  1343. assert results["epm()"]["order"] == 0
  1344. assert results["count()"]["order"] == 1
  1345. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  1346. attrs for time, attrs in results["epm()"]["data"]
  1347. ]
  1348. assert [{"count": self.event_data[index]["count"]}] in [
  1349. attrs for time, attrs in results["count()"]["data"]
  1350. ]
  1351. other = data["Other"]
  1352. assert other["order"] == 5
  1353. assert other["epm()"]["order"] == 0
  1354. assert other["count()"]["order"] == 1
  1355. assert [{"count": 0.05}] in [attrs for _, attrs in other["epm()"]["data"]]
  1356. assert [{"count": 3}] in [attrs for _, attrs in other["count()"]["data"]]
  1357. def test_top_events_with_boolean(self):
  1358. with self.feature(self.enabled_features):
  1359. response = self.client.get(
  1360. self.url,
  1361. data={
  1362. "start": iso_format(self.day_ago),
  1363. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1364. "interval": "1h",
  1365. "yAxis": "count()",
  1366. "orderby": ["-count()"],
  1367. "field": ["count()", "message", "device.charging"],
  1368. "topEvents": 5,
  1369. },
  1370. format="json",
  1371. )
  1372. data = response.data
  1373. assert response.status_code == 200, response.content
  1374. assert len(data) == 6
  1375. for index, event in enumerate(self.events[:5]):
  1376. message = event.message or event.transaction
  1377. results = data[",".join(["False", message])]
  1378. assert results["order"] == index
  1379. assert [{"count": self.event_data[index]["count"]}] in [
  1380. attrs for time, attrs in results["data"]
  1381. ]
  1382. other = data["Other"]
  1383. assert other["order"] == 5
  1384. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1385. def test_top_events_with_error_unhandled(self):
  1386. self.login_as(user=self.user)
  1387. project = self.create_project()
  1388. prototype = load_data("android-ndk")
  1389. prototype["event_id"] = "f" * 32
  1390. prototype["message"] = "not handled"
  1391. prototype["exception"]["values"][0]["value"] = "not handled"
  1392. prototype["exception"]["values"][0]["mechanism"]["handled"] = False
  1393. prototype["timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1394. self.store_event(data=prototype, project_id=project.id)
  1395. with self.feature(self.enabled_features):
  1396. response = self.client.get(
  1397. self.url,
  1398. data={
  1399. "start": iso_format(self.day_ago),
  1400. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1401. "interval": "1h",
  1402. "yAxis": "count()",
  1403. "orderby": ["-count()"],
  1404. "field": ["count()", "error.unhandled"],
  1405. "topEvents": 5,
  1406. },
  1407. format="json",
  1408. )
  1409. data = response.data
  1410. assert response.status_code == 200, response.content
  1411. assert len(data) == 2
  1412. def test_top_events_with_timestamp(self):
  1413. with self.feature(self.enabled_features):
  1414. response = self.client.get(
  1415. self.url,
  1416. data={
  1417. "start": iso_format(self.day_ago),
  1418. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1419. "interval": "1h",
  1420. "yAxis": "count()",
  1421. "orderby": ["-count()"],
  1422. "query": "event.type:default",
  1423. "field": ["count()", "message", "timestamp"],
  1424. "topEvents": 5,
  1425. },
  1426. format="json",
  1427. )
  1428. data = response.data
  1429. assert response.status_code == 200, response.content
  1430. assert len(data) == 6
  1431. # Transactions won't be in the results because of the query
  1432. del self.events[4]
  1433. del self.event_data[4]
  1434. for index, event in enumerate(self.events[:5]):
  1435. results = data[",".join([event.message, event.timestamp])]
  1436. assert results["order"] == index
  1437. assert [{"count": self.event_data[index]["count"]}] in [
  1438. attrs for time, attrs in results["data"]
  1439. ]
  1440. other = data["Other"]
  1441. assert other["order"] == 5
  1442. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1443. def test_top_events_with_int(self):
  1444. with self.feature(self.enabled_features):
  1445. response = self.client.get(
  1446. self.url,
  1447. data={
  1448. "start": iso_format(self.day_ago),
  1449. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1450. "interval": "1h",
  1451. "yAxis": "count()",
  1452. "orderby": ["-count()"],
  1453. "field": ["count()", "message", "transaction.duration"],
  1454. "topEvents": 5,
  1455. },
  1456. format="json",
  1457. )
  1458. data = response.data
  1459. assert response.status_code == 200, response.content
  1460. assert len(data) == 1
  1461. results = data[",".join([self.transaction.transaction, "120000"])]
  1462. assert results["order"] == 0
  1463. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1464. def test_top_events_with_user(self):
  1465. with self.feature(self.enabled_features):
  1466. response = self.client.get(
  1467. self.url,
  1468. data={
  1469. "start": iso_format(self.day_ago),
  1470. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1471. "interval": "1h",
  1472. "yAxis": "count()",
  1473. "orderby": ["-count()", "user"],
  1474. "field": ["user", "count()"],
  1475. "topEvents": 5,
  1476. },
  1477. format="json",
  1478. )
  1479. data = response.data
  1480. assert response.status_code == 200, response.content
  1481. assert len(data) == 5
  1482. assert data["email:bar@example.com"]["order"] == 1
  1483. assert [attrs for time, attrs in data["email:bar@example.com"]["data"]] == [
  1484. [{"count": 7}],
  1485. [{"count": 0}],
  1486. ]
  1487. assert [attrs for time, attrs in data["ip:127.0.0.1"]["data"]] == [
  1488. [{"count": 3}],
  1489. [{"count": 0}],
  1490. ]
  1491. def test_top_events_with_user_and_email(self):
  1492. with self.feature(self.enabled_features):
  1493. response = self.client.get(
  1494. self.url,
  1495. data={
  1496. "start": iso_format(self.day_ago),
  1497. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1498. "interval": "1h",
  1499. "yAxis": "count()",
  1500. "orderby": ["-count()", "user"],
  1501. "field": ["user", "user.email", "count()"],
  1502. "topEvents": 5,
  1503. },
  1504. format="json",
  1505. )
  1506. data = response.data
  1507. assert response.status_code == 200, response.content
  1508. assert len(data) == 5
  1509. assert data["email:bar@example.com,bar@example.com"]["order"] == 1
  1510. assert [attrs for time, attrs in data["email:bar@example.com,bar@example.com"]["data"]] == [
  1511. [{"count": 7}],
  1512. [{"count": 0}],
  1513. ]
  1514. assert [attrs for time, attrs in data["ip:127.0.0.1,None"]["data"]] == [
  1515. [{"count": 3}],
  1516. [{"count": 0}],
  1517. ]
  1518. def test_top_events_with_user_display(self):
  1519. with self.feature(self.enabled_features):
  1520. response = self.client.get(
  1521. self.url,
  1522. data={
  1523. "start": iso_format(self.day_ago),
  1524. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1525. "interval": "1h",
  1526. "yAxis": "count()",
  1527. "orderby": ["-count()"],
  1528. "field": ["message", "user.display", "count()"],
  1529. "topEvents": 5,
  1530. },
  1531. format="json",
  1532. )
  1533. data = response.data
  1534. assert response.status_code == 200, response.content
  1535. assert len(data) == 6
  1536. for index, event in enumerate(self.events[:5]):
  1537. message = event.message or event.transaction
  1538. user = self.event_data[index]["data"]["user"]
  1539. results = data[
  1540. ",".join([message, user.get("email", None) or user.get("ip_address", "None")])
  1541. ]
  1542. assert results["order"] == index
  1543. assert [{"count": self.event_data[index]["count"]}] in [
  1544. attrs for _, attrs in results["data"]
  1545. ]
  1546. other = data["Other"]
  1547. assert other["order"] == 5
  1548. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1549. @pytest.mark.skip(reason="A query with group_id will not return transactions")
  1550. def test_top_events_none_filter(self):
  1551. """When a field is None in one of the top events, make sure we filter by it
  1552. In this case event[4] is a transaction and has no issue
  1553. """
  1554. with self.feature(self.enabled_features):
  1555. response = self.client.get(
  1556. self.url,
  1557. data={
  1558. "start": iso_format(self.day_ago),
  1559. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1560. "interval": "1h",
  1561. "yAxis": "count()",
  1562. "orderby": ["-count()"],
  1563. "field": ["count()", "issue"],
  1564. "topEvents": 5,
  1565. },
  1566. format="json",
  1567. )
  1568. data = response.data
  1569. assert response.status_code == 200, response.content
  1570. assert len(data) == 5
  1571. for index, event in enumerate(self.events[:5]):
  1572. if event.group is None:
  1573. issue = "unknown"
  1574. else:
  1575. issue = event.group.qualified_short_id
  1576. results = data[issue]
  1577. assert results["order"] == index
  1578. assert [{"count": self.event_data[index]["count"]}] in [
  1579. attrs for time, attrs in results["data"]
  1580. ]
  1581. @pytest.mark.skip(reason="Invalid query - transaction events don't have group_id field")
  1582. def test_top_events_one_field_with_none(self):
  1583. with self.feature(self.enabled_features):
  1584. response = self.client.get(
  1585. self.url,
  1586. data={
  1587. "start": iso_format(self.day_ago),
  1588. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1589. "interval": "1h",
  1590. "yAxis": "count()",
  1591. "orderby": ["-count()"],
  1592. "query": "event.type:transaction",
  1593. "field": ["count()", "issue"],
  1594. "topEvents": 5,
  1595. },
  1596. format="json",
  1597. )
  1598. data = response.data
  1599. assert response.status_code == 200, response.content
  1600. assert len(data) == 1
  1601. results = data["unknown"]
  1602. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1603. assert results["order"] == 0
  1604. def test_top_events_with_error_handled(self):
  1605. data = self.event_data[0]
  1606. data["data"]["level"] = "error"
  1607. data["data"]["exception"] = {
  1608. "values": [
  1609. {
  1610. "type": "ValidationError",
  1611. "value": "Bad request",
  1612. "mechanism": {"handled": True, "type": "generic"},
  1613. }
  1614. ]
  1615. }
  1616. self.store_event(data["data"], project_id=data["project"].id)
  1617. data["data"]["exception"] = {
  1618. "values": [
  1619. {
  1620. "type": "ValidationError",
  1621. "value": "Bad request",
  1622. "mechanism": {"handled": False, "type": "generic"},
  1623. }
  1624. ]
  1625. }
  1626. self.store_event(data["data"], project_id=data["project"].id)
  1627. with self.feature(self.enabled_features):
  1628. response = self.client.get(
  1629. self.url,
  1630. data={
  1631. "start": iso_format(self.day_ago),
  1632. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1633. "interval": "1h",
  1634. "yAxis": "count()",
  1635. "orderby": ["-count()"],
  1636. "field": ["count()", "error.handled"],
  1637. "topEvents": 5,
  1638. "query": "!event.type:transaction",
  1639. },
  1640. format="json",
  1641. )
  1642. assert response.status_code == 200, response.content
  1643. data = response.data
  1644. assert len(data) == 2
  1645. results = data["1"]
  1646. assert [attrs for time, attrs in results["data"]] == [[{"count": 20}], [{"count": 6}]]
  1647. results = data["0"]
  1648. assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
  1649. def test_top_events_with_aggregate_condition(self):
  1650. with self.feature(self.enabled_features):
  1651. response = self.client.get(
  1652. self.url,
  1653. data={
  1654. "start": iso_format(self.day_ago),
  1655. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1656. "interval": "1h",
  1657. "yAxis": "count()",
  1658. "orderby": ["-count()"],
  1659. "field": ["message", "count()"],
  1660. "query": "count():>4",
  1661. "topEvents": 5,
  1662. },
  1663. format="json",
  1664. )
  1665. assert response.status_code == 200, response.content
  1666. data = response.data
  1667. assert len(data) == 3
  1668. for index, event in enumerate(self.events[:3]):
  1669. message = event.message or event.transaction
  1670. results = data[message]
  1671. assert results["order"] == index
  1672. assert [{"count": self.event_data[index]["count"]}] in [
  1673. attrs for time, attrs in results["data"]
  1674. ]
  1675. @pytest.mark.xfail(reason="There's only 2 rows total, which mean there shouldn't be other")
  1676. def test_top_events_with_to_other(self):
  1677. version = "version -@'\" 1.2,3+(4)"
  1678. version_escaped = "version -@'\\\" 1.2,3+(4)"
  1679. # every symbol is replaced with a underscore to make the alias
  1680. version_alias = "version_______1_2_3__4_"
  1681. # add an event in the current release
  1682. event = self.event_data[0]
  1683. event_data = event["data"].copy()
  1684. event_data["event_id"] = uuid4().hex
  1685. event_data["release"] = version
  1686. self.store_event(event_data, project_id=event["project"].id)
  1687. with self.feature(self.enabled_features):
  1688. response = self.client.get(
  1689. self.url,
  1690. data={
  1691. "start": iso_format(self.day_ago),
  1692. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1693. "interval": "1h",
  1694. "yAxis": "count()",
  1695. # the double underscores around the version alias is because of a comma and quote
  1696. "orderby": [f"-to_other_release__{version_alias}__others_current"],
  1697. "field": [
  1698. "count()",
  1699. f'to_other(release,"{version_escaped}",others,current)',
  1700. ],
  1701. "topEvents": 2,
  1702. },
  1703. format="json",
  1704. )
  1705. assert response.status_code == 200, response.content
  1706. data = response.data
  1707. assert len(data) == 2
  1708. current = data["current"]
  1709. assert current["order"] == 1
  1710. assert sum(attrs[0]["count"] for _, attrs in current["data"]) == 1
  1711. others = data["others"]
  1712. assert others["order"] == 0
  1713. assert sum(attrs[0]["count"] for _, attrs in others["data"]) == sum(
  1714. event_data["count"] for event_data in self.event_data
  1715. )
  1716. def test_top_events_with_equations(self):
  1717. with self.feature(self.enabled_features):
  1718. response = self.client.get(
  1719. self.url,
  1720. data={
  1721. "start": iso_format(self.day_ago),
  1722. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1723. "interval": "1h",
  1724. "yAxis": "equation|count() / 100",
  1725. "orderby": ["-count()"],
  1726. "field": ["count()", "message", "user.email", "equation|count() / 100"],
  1727. "topEvents": 5,
  1728. },
  1729. format="json",
  1730. )
  1731. data = response.data
  1732. assert response.status_code == 200, response.content
  1733. assert len(data) == 6
  1734. for index, event in enumerate(self.events[:5]):
  1735. message = event.message or event.transaction
  1736. results = data[
  1737. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1738. ]
  1739. assert results["order"] == index
  1740. assert [{"count": self.event_data[index]["count"] / 100}] in [
  1741. attrs for time, attrs in results["data"]
  1742. ]
  1743. other = data["Other"]
  1744. assert other["order"] == 5
  1745. assert [{"count": 0.03}] in [attrs for _, attrs in other["data"]]
  1746. @mock.patch("sentry.snuba.discover.bulk_snql_query", return_value=[{"data": [], "meta": []}])
  1747. @mock.patch(
  1748. "sentry.search.events.builder.raw_snql_query", return_value={"data": [], "meta": []}
  1749. )
  1750. def test_invalid_interval(self, mock_raw_query, mock_bulk_query):
  1751. with self.feature(self.enabled_features):
  1752. response = self.client.get(
  1753. self.url,
  1754. format="json",
  1755. data={
  1756. "end": iso_format(before_now()),
  1757. # 7,200 points for each event
  1758. "start": iso_format(before_now(seconds=7200)),
  1759. "field": ["count()", "issue"],
  1760. "query": "",
  1761. "interval": "1s",
  1762. "yAxis": "count()",
  1763. },
  1764. )
  1765. assert response.status_code == 200
  1766. assert mock_bulk_query.call_count == 1
  1767. with self.feature(self.enabled_features):
  1768. response = self.client.get(
  1769. self.url,
  1770. format="json",
  1771. data={
  1772. "end": iso_format(before_now()),
  1773. "start": iso_format(before_now(seconds=7200)),
  1774. "field": ["count()", "issue"],
  1775. "query": "",
  1776. "interval": "1s",
  1777. "yAxis": "count()",
  1778. # 7,200 points for each event * 2, should error
  1779. "topEvents": 2,
  1780. },
  1781. )
  1782. assert response.status_code == 200
  1783. assert mock_raw_query.call_count == 2
  1784. # Should've reset to the default for between 1 and 24h
  1785. assert mock_raw_query.mock_calls[1].args[0].query.granularity.granularity == 300
  1786. with self.feature(self.enabled_features):
  1787. response = self.client.get(
  1788. self.url,
  1789. format="json",
  1790. data={
  1791. "end": iso_format(before_now()),
  1792. # 1999 points * 5 events should just be enough to not error
  1793. "start": iso_format(before_now(seconds=1999)),
  1794. "field": ["count()", "issue"],
  1795. "query": "",
  1796. "interval": "1s",
  1797. "yAxis": "count()",
  1798. "topEvents": 5,
  1799. },
  1800. )
  1801. assert response.status_code == 200
  1802. assert mock_raw_query.call_count == 4
  1803. # Should've left the interval alone since we're just below the limit
  1804. assert mock_raw_query.mock_calls[3].args[0].query.granularity.granularity == 1
  1805. with self.feature(self.enabled_features):
  1806. response = self.client.get(
  1807. self.url,
  1808. format="json",
  1809. data={
  1810. "end": iso_format(before_now()),
  1811. "start": iso_format(before_now(hours=24)),
  1812. "field": ["count()", "issue"],
  1813. "query": "",
  1814. "interval": "0d",
  1815. "yAxis": "count()",
  1816. "topEvents": 5,
  1817. },
  1818. )
  1819. assert response.status_code == 200
  1820. assert mock_raw_query.call_count == 6
  1821. # Should've default to 24h's default of 5m
  1822. assert mock_raw_query.mock_calls[5].args[0].query.granularity.granularity == 300
  1823. def test_top_events_timestamp_fields(self):
  1824. with self.feature(self.enabled_features):
  1825. response = self.client.get(
  1826. self.url,
  1827. format="json",
  1828. data={
  1829. "start": iso_format(self.day_ago),
  1830. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1831. "interval": "1h",
  1832. "yAxis": "count()",
  1833. "orderby": ["-count()"],
  1834. "field": ["count()", "timestamp", "timestamp.to_hour", "timestamp.to_day"],
  1835. "topEvents": 5,
  1836. },
  1837. )
  1838. assert response.status_code == 200
  1839. data = response.data
  1840. assert len(data) == 3
  1841. # these are the timestamps corresponding to the events stored
  1842. timestamps = [
  1843. self.day_ago + timedelta(minutes=2),
  1844. self.day_ago + timedelta(hours=1, minutes=2),
  1845. self.day_ago + timedelta(minutes=4),
  1846. ]
  1847. timestamp_hours = [timestamp.replace(minute=0, second=0) for timestamp in timestamps]
  1848. timestamp_days = [timestamp.replace(hour=0, minute=0, second=0) for timestamp in timestamps]
  1849. for ts, ts_hr, ts_day in zip(timestamps, timestamp_hours, timestamp_days):
  1850. key = f"{iso_format(ts)}+00:00,{iso_format(ts_day)}+00:00,{iso_format(ts_hr)}+00:00"
  1851. count = sum(
  1852. e["count"] for e in self.event_data if e["data"]["timestamp"] == iso_format(ts)
  1853. )
  1854. results = data[key]
  1855. assert [{"count": count}] in [attrs for time, attrs in results["data"]]
  1856. def test_top_events_other_with_matching_columns(self):
  1857. with self.feature(self.enabled_features):
  1858. response = self.client.get(
  1859. self.url,
  1860. data={
  1861. "start": iso_format(self.day_ago),
  1862. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1863. "interval": "1h",
  1864. "yAxis": "count()",
  1865. "orderby": ["-count()"],
  1866. "field": ["count()", "tags[shared-tag]", "message"],
  1867. "topEvents": 5,
  1868. },
  1869. format="json",
  1870. )
  1871. data = response.data
  1872. assert response.status_code == 200, response.content
  1873. assert len(data) == 6
  1874. for index, event in enumerate(self.events[:5]):
  1875. message = event.message or event.transaction
  1876. results = data[",".join([message, "yup"])]
  1877. assert results["order"] == index
  1878. assert [{"count": self.event_data[index]["count"]}] in [
  1879. attrs for _, attrs in results["data"]
  1880. ]
  1881. other = data["Other"]
  1882. assert other["order"] == 5
  1883. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1884. def test_top_events_with_field_overlapping_other_key(self):
  1885. transaction_data = load_data("transaction")
  1886. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1887. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  1888. transaction_data["transaction"] = OTHER_KEY
  1889. for i in range(5):
  1890. data = transaction_data.copy()
  1891. data["event_id"] = "ab" + f"{i}" * 30
  1892. data["contexts"]["trace"]["span_id"] = "ab" + f"{i}" * 14
  1893. self.store_event(data, project_id=self.project.id)
  1894. with self.feature(self.enabled_features):
  1895. response = self.client.get(
  1896. self.url,
  1897. data={
  1898. "start": iso_format(self.day_ago),
  1899. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1900. "interval": "1h",
  1901. "yAxis": "count()",
  1902. "orderby": ["-count()"],
  1903. "field": ["count()", "message"],
  1904. "topEvents": 5,
  1905. },
  1906. format="json",
  1907. )
  1908. data = response.data
  1909. assert response.status_code == 200, response.content
  1910. assert len(data) == 6
  1911. assert f"{OTHER_KEY} (message)" in data
  1912. results = data[f"{OTHER_KEY} (message)"]
  1913. assert [{"count": 5}] in [attrs for _, attrs in results["data"]]
  1914. other = data["Other"]
  1915. assert other["order"] == 5
  1916. assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
  1917. def test_top_events_can_exclude_other_series(self):
  1918. with self.feature(self.enabled_features):
  1919. response = self.client.get(
  1920. self.url,
  1921. data={
  1922. "start": iso_format(self.day_ago),
  1923. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1924. "interval": "1h",
  1925. "yAxis": "count()",
  1926. "orderby": ["count()"],
  1927. "field": ["count()", "message"],
  1928. "topEvents": 5,
  1929. "excludeOther": "1",
  1930. },
  1931. format="json",
  1932. )
  1933. data = response.data
  1934. assert response.status_code == 200, response.content
  1935. assert len(data) == 5
  1936. assert "Other" not in response.data
  1937. def test_top_events_with_equation_including_unselected_fields_passes_field_validation(self):
  1938. with self.feature(self.enabled_features):
  1939. response = self.client.get(
  1940. self.url,
  1941. data={
  1942. "start": iso_format(self.day_ago),
  1943. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1944. "interval": "1h",
  1945. "yAxis": "count()",
  1946. "orderby": ["-equation[0]"],
  1947. "field": ["count()", "message", "equation|count_unique(user) * 2"],
  1948. "topEvents": 5,
  1949. },
  1950. format="json",
  1951. )
  1952. data = response.data
  1953. assert response.status_code == 200, response.content
  1954. assert len(data) == 6
  1955. other = data["Other"]
  1956. assert other["order"] == 5
  1957. assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
  1958. def test_top_events_boolean_condition_and_project_field(self):
  1959. with self.feature(self.enabled_features):
  1960. response = self.client.get(
  1961. self.url,
  1962. data={
  1963. "start": iso_format(self.day_ago),
  1964. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1965. "interval": "1h",
  1966. "yAxis": "count()",
  1967. "orderby": ["-count()"],
  1968. "field": ["project", "count()"],
  1969. "topEvents": 5,
  1970. "query": "event.type:transaction (transaction:*a OR transaction:b*)",
  1971. },
  1972. format="json",
  1973. )
  1974. assert response.status_code == 200